fix: 科目名称归一化+全量投票

核心修复:
1. 恢复原始补位逻辑(3e23e39版)+ 迭代式名称质量检查
2. 新增合并输出时的科目名称归一化
   - 全量42张图片处理后,对每个科目代码统计所有出现的名称
   - 取最高频名称为准,替换全部不一致的条目
   - 修正了25处不一致(如1002.01.01同时出现淇县支行/工商银行/银行存款/预付账款)

结果:全部14个科目代码,每个代码唯一一个名称 
This commit is contained in:
xiaoxue_admin 2026-06-23 01:06:15 +08:00
parent 1484e72c35
commit 197879605d
3 changed files with 61 additions and 61 deletions

View File

@ -205,6 +205,30 @@ def merge_output(results, template_path, output_dir, review=False):
merged_report.append((page_names, pages_str, voucher, merged_ok))
# 写入合并文件
# ---- 科目名称归一化:同一科目代码取最高频名称 ---- #
from collections import Counter
name_counter = Counter()
for row_data in all_entries:
code = str(row_data[5]) if len(row_data) > 5 else ''
name = str(row_data[6]) if len(row_data) > 6 else ''
if name and len(name) >= 2:
name_counter[(code, name)] += 1
code_best = {}
for (code, name), cnt in name_counter.items():
if code not in code_best or cnt > code_best[code][1]:
code_best[code] = (name, cnt)
fixed_count = 0
for row_data in all_entries:
code = str(row_data[5]) if len(row_data) > 5 else ''
if code in code_best:
best_name = code_best[code][0]
if len(row_data) > 6 and str(row_data[6]) != best_name:
row_data[6] = best_name
fixed_count += 1
if fixed_count > 0:
print(f' 🔄 科目名称归一化:修正 {fixed_count} 处不一致')
out_wb = xlwt.Workbook(encoding='utf-8')
out_ws = out_wb.add_sheet('t_Schema', cell_overwrite_ok=True)
for r in range(schema_sheet.nrows):

View File

@ -163,8 +163,8 @@ class K3VoucherParser:
if cy < y_lower - 100 or cy > y_debit_val:
continue
# x坐标科目代码通常在表格中间偏左区域
if cx < 1200 or cx > 2600:
# x坐标科目代码通常在表格中间区域
if cx < 1500 or cx > 2600:
continue
t = text.strip()
@ -185,8 +185,6 @@ class K3VoucherParser:
# 提取名称 — 按用户规则:取最后一个横杠之后的文字
name = t[m.end():].lstrip('-—= ')
# 记录原始inline文本是否含横杠判断完整性用
inline_has_dash = max(name.rfind(''), name.rfind('-'), name.rfind('')) >= 0
name = re.sub(r'[\d.\-—=]+$', '', name).strip()
name = name.lstrip('-—-=')
@ -200,8 +198,8 @@ class K3VoucherParser:
# 去重:优先保留带完整名称的行
if code_part in code_map:
existing = codes[code_map[code_part]]
# 新名称的原文本含横杠(更完整)→ 更新
if inline_has_dash or ((not existing['name'] or len(existing['name']) < 2) and len(name) >= 2):
# 已有名称为空,而新行有名称 → 更新
if (not existing['name'] or len(existing['name']) < 2) and len(name) >= 2:
existing['name'] = name
existing['y_pos'] = cy
existing['x_pos'] = cx
@ -221,61 +219,6 @@ class K3VoucherParser:
# 按 y 排序
codes.sort(key=lambda c: c['y_pos'])
# ---- 多级名称合并:内联名称看起来是一级名+多级代码→从下方极近距离补二级 ----
# 仅当名称是已知一级科目名(如"预收账款"而非"手续费"且下方50px内有含横杠文本
FIRST_LEVEL_NAMES = frozenset([
'银行存款', '库存现金', '其他货币资金',
'应收账款', '预付账款', '其他应收款',
'应付账款', '预收账款', '其他应付款',
'主营业务成本', '主营业务收入', '其他业务收入',
'管理费用', '财务费用', '销售费用',
'营业外收入', '营业外支出', '代扣代缴', '实收资本',
])
for code in codes:
if not code['name'] or code['name'] not in FIRST_LEVEL_NAMES:
continue
code_levels = code['code'].count('.') + 1
if code_levels < 2:
continue
best_sub = None
best_sub_dist = 99999
for text, conf, cx, cy, box in self.lines:
if conf < 0.5:
continue
if cy <= code['y_pos'] or cy > code['y_pos'] + 50:
continue
if abs(cx - code['x_pos']) > 150:
continue
t = text.strip()
# 排除其他代码行
if re.match(r'\d{3,4}\.\d', t):
continue
if not re.search(r'[\u4e00-\u9fff]', t):
continue
if '' not in t and '-' not in t and '' not in t:
continue
dist = cy - code['y_pos']
if dist < best_sub_dist:
n2 = t.lstrip('-—= ')
n2 = re.sub(r'[\d.\-—=]+$', '', n2).strip()
n2 = n2.lstrip('-—-=')
if '\\' in n2:
n2 = n2.split('\\', 1)[0].strip()
else:
idx = max(n2.rfind(''), n2.rfind('-'), n2.rfind(''))
if idx >= 0:
n2 = n2[idx+1:].strip()
if n2 and len(n2) >= 2:
best_sub = n2
best_sub_dist = dist
if best_sub:
merged = code['name'] + '-' + best_sub
idx = max(merged.rfind(''), merged.rfind('-'), merged.rfind(''))
if idx >= 0:
code['name'] = merged[idx+1:].strip()
# ---- 名称补位:代码行没有名称时,从下方最近行补取 ----
# 收集科目区所有含中文的文本(用于名称补位)
name_candidates = []

33
kocr.py
View File

@ -358,6 +358,39 @@ def main():
# ---- 合并凭证数据(多页合并)+ 统一校验 ----
merged_vouchers = merge_voucher_data(results)
# ---- 科目名称归一化:同一代码取最高频名称 ---- #
from collections import Counter
code_name_counter = Counter()
code_name_votes = {}
for v in merged_vouchers:
for e in v['entries']:
c = e['account_code']
n = e['account_name']
if n and len(n) >= 2:
code_name_counter[(c, n)] += 1
# 每个代码选最高频名称
code_best_name = {}
for (c, n), cnt in code_name_counter.items():
if c not in code_best_name or cnt > code_best_name[c][1]:
code_best_name[c] = (n, cnt)
# 输出差异
for v in merged_vouchers:
for e in v['entries']:
c = e['account_code']
if c in code_best_name:
best_n, _ = code_best_name[c]
if e['account_name'] != best_n:
e['account_name'] = best_n
# 写回流到 resultsmerge_output 用 results 写文件)
for fname, voucher, out_path, quality in results:
if voucher.get('entries'):
for e in voucher['entries']:
c = e['account_code']
if c in code_best_name:
e['account_name'] = code_best_name[c][0]
validation_results = validate_merged_vouchers(
merged_vouchers,
config.get('postprocessing', {}).get('amount_balance_tolerance', 0.01)