kocr/core/postprocessor.py

269 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""KOCR v4 — 后处理模块
包含:字符纠错、大写金额转数字、大小写交叉校验、借贷平衡验证
"""
import os
import re
# ======== 财务字符纠错字典 ========
FINANCE_CHAR_FIX = {
"": "", "": "", "": "", "": "", "": "",
"O": "0", "o": "0", "l": "1", "I": "1", "Z": "2", "z": "2",
"S": "5", "s": "5", "B": "8", "b": "8", "": ""
}
def char_correct(text: str) -> str:
"""财务字符纠错"""
for wrong, right in FINANCE_CHAR_FIX.items():
text = text.replace(wrong, right)
return text
# ======== 大写金额转数字 ========
CN_NUM = {
'': 0, '': 1, '': 2, '': 3, '': 4,
'': 5, '': 6, '': 7, '': 8, '': 9,
'': 0, '': 0, # 易误读的常见替换
}
CN_UNIT = {
'': 10, '': 100, '': 1000,
'': 10000, '亿': 100000000,
'': 1, '': 0.1, '': 0.01,
'': 0,
}
def chinese_amount_to_number(cn_str: str) -> float:
"""大写金额转阿拉伯数字
支持格式:
- 壹万贰仟叁佰肆拾伍元陆角柒分 → 12345.67
- 壹仟贰佰叁拾肆元整 → 1234.00
- 拾元整 → 10.00
- 壹佰贰拾叁元零角零分 → 123.00
Args:
cn_str: 中文大写金额字符串
Returns:
转换后的数字,失败返回 0.0
"""
if not cn_str:
return 0.0
# 清理字符串
cn_str = cn_str.strip().replace(' ', '').replace('', '').replace('', '')
# 去掉前缀(如 "合计:"
for prefix in ['合计', '合计大写', '人民币', 'RMB', '', '¥']:
if cn_str.startswith(prefix):
cn_str = cn_str[len(prefix):]
break
total = 0.0
temp = 0.0 # 当前节的值(万以下)
i = 0
while i < len(cn_str):
char = cn_str[i]
if char in CN_NUM:
temp = CN_NUM[char] # 暂存数字
elif char in CN_UNIT:
unit = CN_UNIT[char]
if char in ('', '亿'):
# 万/亿:整节结算
total = (total + temp) * unit if temp > 0 else total * unit
temp = 0.0
elif char == '':
# 元:加整数部分
total += temp if temp > 0 else 0
temp = 0.0
elif char in ('', ''):
# 角/分:加小数部分
total += temp * unit
temp = 0.0
else:
# 拾/佰/仟:节内进位
if temp == 0:
temp = 1 # "拾元" → 10元
total += temp * unit
temp = 0.0
elif char == '':
# 零:跳过,继续
if i + 1 < len(cn_str) and cn_str[i + 1] in CN_UNIT:
# 零后面有单位 → 如"壹万零伍佰"
pass
temp = 0
elif char == '':
# 整:结束
break
i += 1
# 处理末尾剩余
if temp > 0:
total += temp
return round(total, 2)
def amount_check(amount_num: float, amount_cn: str) -> bool:
"""大小写金额交叉验证
检查 OCR 识别的阿拉伯数字合计与中文大写合计是否匹配
Args:
amount_num: 阿拉伯数字合计(从 OCR 金额列解析)
amount_cn: 中文大写合计(从 OCR 文本栏解析)
Returns:
True if 差值 ≤ 0.01
"""
if not amount_cn or amount_num <= 0:
return True # 没有大写则跳过校验
cn_num = chinese_amount_to_number(amount_cn)
if cn_num <= 0:
return True # 大写解析失败,跳过
diff = abs(amount_num - cn_num)
return diff <= 0.01
# ======== 数据验证 ========
def validate_voucher(voucher_data: dict, balance_tolerance: float = 0.01) -> tuple:
"""验证 OCR 识别结果的完整性和正确性
Args:
voucher_data: K3VoucherParser.parse() 的输出
balance_tolerance: 借贷平衡允许误差(元)
Returns:
(is_clean, issues, summary_text)
"""
issues = []
warnings = []
entries = voucher_data.get('entries', [])
if not entries:
return False, ['❌ 未识别到任何分录'], '无分录'
# 1. 借贷平衡检查
total_dr = sum(e.get('debit', 0) or 0 for e in entries)
total_cr = sum(e.get('credit', 0) or 0 for e in entries)
balance_diff = abs(total_dr - total_cr)
if balance_diff > balance_tolerance:
issues.append(f'❌ 借贷不平衡: 借 {total_dr:.2f} ≠ 贷 {total_cr:.2f} (差 {balance_diff:.2f})')
# 2. 大小写金额交叉验证(如有可能)
total_str = voucher_data.get('total_str', '')
if total_str and total_dr > 0:
# 从合计字符串中提取大写金额
cn_part = re.sub(r'[\d,.\s¥¥]', '', total_str)
if cn_part:
cn_ok = amount_check(total_dr, cn_part)
if not cn_ok:
cn_val = chinese_amount_to_number(cn_part)
issues.append(f'❌ 大小写金额不匹配: 小写 {total_dr:.2f} ≠ 大写 {cn_part}(={cn_val:.2f})')
else:
warnings.append(f'✅ 大小写金额验证通过')
# 3. 金额合理性检查
for e in entries:
for side in ['debit', 'credit']:
val = e.get(side, 0) or 0
if val <= 0:
continue
if val > 99999999:
issues.append(f'⚠️ 金额异常过大: {e["account_code"]} {side}={val:.2f}')
if val < 0.01 and val > 0:
warnings.append(f' 金额异常小: {e["account_code"]} {side}={val:.2f}')
# 4. 科目代码检查
for e in entries:
code = e.get('account_code', '')
if not code or len(code) < 4:
warnings.append(f' 科目代码异常: {code} {e.get("account_name","")}')
# 构建摘要
is_clean = len(issues) == 0
summary_lines = []
if is_clean and not warnings:
summary_lines.append('✅ 验证通过(借贷平衡 + 金额合理)')
else:
summary_lines.extend(issues)
summary_lines.extend(warnings)
return is_clean, issues + warnings, '\n'.join(summary_lines)
def generate_review_file(results: list, output_dir: str) -> str:
"""生成批量处理的校验报告"""
from datetime import datetime
review_path = os.path.join(output_dir, '校验报告.txt')
lines = []
lines.append('=' * 50)
lines.append('KOCR v4 数据校验报告')
lines.append(f'生成时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
lines.append(f'处理张数: {len(results)}')
lines.append('=' * 50)
lines.append('')
all_clean = True
for fname, voucher, out_path, quality in results:
is_clean, issues, summary = validate_voucher(voucher)
if not is_clean:
all_clean = False
date_str = voucher['date'].strftime('%Y-%m-%d') if voucher.get('date') else '?'
vno = voucher.get('voucher_no', '?')
lines.append(f'📷 {fname}')
lines.append(f' 凭证: 记-{vno} 日期: {date_str}')
if quality:
lines.append(f' 类型: {quality.photo_type} 亮度:{quality.brightness} 模糊:{quality.blur_score}')
lines.append(f' {summary}')
lines.append('')
if all_clean:
lines.append('🎉 全部凭证验证通过')
else:
lines.append('⚠️ 存在需要人工复核的数据,请对照原始凭证核对')
lines.append('=' * 50)
with open(review_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
return review_path
def validate_merged_vouchers(merged_list, balance_tolerance=0.01):
"""对合并后的凭证列表做完整校验
每笔凭证(多页已合并)统一做借贷平衡 + 大小写金额验证。
这才是最终的业务校验结果。
Args:
merged_list: merge_voucher_data() 的输出
balance_tolerance: 借贷平衡容差
Returns:
[(voucher, is_clean, issues, summary_text), ...]
"""
results = []
for voucher in merged_list:
is_clean, issues, summary = validate_voucher(voucher, balance_tolerance)
results.append((voucher, is_clean, issues, summary))
return results