diff --git a/core/excel_writer.py b/core/excel_writer.py index 429b002..c85d797 100644 --- a/core/excel_writer.py +++ b/core/excel_writer.py @@ -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): diff --git a/core/parser.py b/core/parser.py index 323c0cf..84405c4 100644 --- a/core/parser.py +++ b/core/parser.py @@ -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 = [] diff --git a/kocr.py b/kocr.py index d12cbb8..2a2fae9 100644 --- a/kocr.py +++ b/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)