fix: 科目名称归一化+全量投票
核心修复:
1. 恢复原始补位逻辑(3e23e39版)+ 迭代式名称质量检查
2. 新增合并输出时的科目名称归一化
- 全量42张图片处理后,对每个科目代码统计所有出现的名称
- 取最高频名称为准,替换全部不一致的条目
- 修正了25处不一致(如1002.01.01同时出现淇县支行/工商银行/银行存款/预付账款)
结果:全部14个科目代码,每个代码唯一一个名称 ✅
This commit is contained in:
parent
1484e72c35
commit
197879605d
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
33
kocr.py
|
|
@ -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
|
||||
|
||||
# 写回流到 results(merge_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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue