diff --git a/core/parser.py b/core/parser.py index bc9e76a..84405c4 100644 --- a/core/parser.py +++ b/core/parser.py @@ -184,16 +184,16 @@ class K3VoucherParser: continue # 提取名称 — 按用户规则:取最后一个横杠之后的文字 - # 先取最后横杠之后的内容再清洗(防止尾随横杠被提前消除→"银行存款-"变"银行存款") name = t[m.end():].lstrip('--—= ') - idx = max(name.rfind('-'), name.rfind('-'), name.rfind('—')) - if idx >= 0: - name = name[idx+1:].strip() - # 清洗尾部数字/符号 - name = re.sub(r'[\d.\-—=-]+$', '', name).strip() + name = re.sub(r'[\d.\-—=]+$', '', name).strip() name = name.lstrip('-—-=') + if '\\' in name: name = name.split('\\', 1)[0].strip() + else: + idx = max(name.rfind('-'), name.rfind('-'), name.rfind('—')) + if idx >= 0: + name = name[idx+1:].strip() # 去重:优先保留带完整名称的行 if code_part in code_map: @@ -219,7 +219,7 @@ class K3VoucherParser: # 按 y 排序 codes.sort(key=lambda c: c['y_pos']) - # ---- 名称补位:代码行没有名称时,从附近行补取 ---- + # ---- 名称补位:代码行没有名称时,从下方最近行补取 ---- # 收集科目区所有含中文的文本(用于名称补位) name_candidates = [] for text, conf, cx, cy, box in self.lines: @@ -232,45 +232,36 @@ class K3VoucherParser: continue if t in ('摘要', '科目', '借方', '贷方', '合计', '日期', '会计'): continue + # 排除明显的非科目名称文本 + if any(k in t for k in ['过账', '经办', '审核', '出纳', '制单', 'Manager']): + continue # 排除本身就是科目代码的行 if re.match(r'\d{3,4}\.\d', t): continue name_candidates.append((t, cy, cx)) - # 额外过滤:排除园区名/房号类文本(含"里景苑园"且有数字) - name_candidates = [(t, cy, cx) for t, cy, cx in name_candidates - if not re.search(r'[\u4e00-\u9fff]*[里景苑园]\w*\d', t) - and not t.endswith('/') - and not re.search(r'各类统筹|装修押金|电费收入|本收入|办公费', t)] - name_candidates.sort(key=lambda x: x[1]) for code in codes: - # 对所有代码行(即使已有内联名称)运行补位评分 - candidates = [] - - # 基线:内联名称(如已存在,分数加成) if code['name'] and len(code['name']) >= 2: - candidates.append({ - 'name': code['name'], - 'is_inline': True, - 'y': code['y_pos'], - 'x': code['x_pos'], - 'raw': '', - }) - + continue # 已有名称,跳过 + # 找代码行下方最近的含中文文本(原始逻辑:只找下方,不搜上方) + best = None + best_dist = 99999 for t, cy, cx in name_candidates: - # 只找下方 ±30px(允许同行偏移,但不找上方无关文本) - if cy < code['y_pos'] - 30: - continue - if cy > code['y_pos'] + 500: - continue + if cy <= code['y_pos']: + continue # 只找下方的 if abs(cx - code['x_pos']) > 600: continue - - # 先提取清洗后的名称 + dist = cy - code['y_pos'] + if dist < best_dist and dist < 600: + best = (t, cy, cx) + best_dist = dist + if best: + t, cy, cx = best + # 按用户规则提取名称(原始逻辑:先清洗后取最后横杠) n = t.lstrip('--—= ') - n = re.sub(r'[\d.\-—=-]+$', '', n).strip() + n = re.sub(r'[\d.\-—=]+$', '', n).strip() n = n.lstrip('-—-=') if '\\' in n: n = n.split('\\', 1)[0].strip() @@ -278,84 +269,54 @@ class K3VoucherParser: idx = max(n.rfind('-'), n.rfind('-'), n.rfind('—')) if idx >= 0: n = n[idx+1:].strip() - if not n or len(n) < 3: - continue - # 排除园区名/行政地名作为科目名称(如"假日里""假日风景") - if re.search(r'[里景苑园]$', n): - continue - # 排除公司名/单位名(如"鹤壁科赛物业服务有限公司"等) - if re.search(r'公司|有限|税务|供电|电力', t): - continue - # 排除明显的非科目名称文本 - if any(k in t for k in ['未到账', '过账', 'Manager', '出纳', '制单', '审核', '经办']): - continue - # 排除房号/楼栋模式文本(如 "假日里C1二单元假日重C1-2-0201") - if re.search(r'\d-\d{3,4}', t): - continue - - candidates.append({ - 'name': n, - 'is_inline': False, - 'y': cy, - 'x': cx, - 'raw': t, - }) - - if not candidates: - continue - - # 评分:基于清洗后的名称,而非原始 OCR 文本 - for cand in candidates: - score = 0 - if cand['is_inline']: - score = 10 # 内联名称基线分 - # 多级代码配一级名称→降权(内联可能不完整,让补位有机会) - if code['code'].count('.') >= 1 and cand['name'] in ( - '银行存款', '库存现金', '其他货币资金', - '应收账款', '预付账款', '其他应收款', - '应付账款', '预收账款', '其他应付款', - '主营业务成本', '主营业务收入', '其他业务收入', - '管理费用', '财务费用', '销售费用', - '营业外收入', '营业外支出', '代扣代缴', - ): - score -= 15 - else: - if any(d in cand['raw'] for d in ['-', '-', '—']): - score += 10 # 含横杠信号(含全角/ASCII/长破折号) - if len(cand['name']) >= 4: - score += 3 # 长名称更可信 - x_dist = abs(cand['x'] - code['x_pos']) - if x_dist < 50: - score += 25 - elif x_dist < 150: - score += 15 - elif x_dist < 300: - score += 5 - if re.search(r'\d{10,}', cand['raw']): - score -= 15 # 原始文本含长数字串则轻度惩罚 - # 非科目名称特征文本 - if any(k in cand['raw'] for k in ['未到账', '过账', 'Manager']): - score -= 20 - # 已知一级科目名+多级代码→名称应是末级而非一级 - code_levels = code['code'].count('.') + 1 - if code_levels >= 2 and cand['name'] in ( - '银行存款', '库存现金', '其他货币资金', - '应收账款', '预付账款', '其他应收款', - '应付账款', '预收账款', '其他应付款', - '主营业务成本', '主营业务收入', '其他业务收入', - '管理费用', '财务费用', '销售费用', - '营业外收入', '营业外支出', - '代扣代缴', '实收资本', '本年利润', - ): - score -= 25 # 多级代码配一级名称→明显不对 - score -= abs(cand['y'] - code['y_pos']) / 10 # 近的优先(y差10px扣1分,防止跨行误抓) - cand['score'] = score - - candidates.sort(key=lambda c: -c['score']) - best = candidates[0] - - if not best['is_inline']: - code['name'] = best['name'] + if n and len(n) >= 2: + # 名称质量检查:如果提取结果可疑,试下一个候选 + bad_name = (len(n) < 3 or n.endswith('/') + or n in ('里C', '里A', '假台', '里C3', '里A3') + or re.search(r'[里景苑园]$', n) + or re.search(r'[A-Z]\d', n)) + if bad_name: + # 移除当前候选,再找下一个 + name_candidates = [(ct, ccy, ccx) for ct, ccy, ccx in name_candidates + if ct != t or ccy != cy or ccx != cx] + # 递归尝试下一个(限制深度) + for _ in range(10): + next_best = None + next_dist = 99999 + for nt, ncy, ncx in name_candidates: + if ncy <= code['y_pos']: + continue + if abs(ncx - code['x_pos']) > 600: + continue + ndist = ncy - code['y_pos'] + if ndist < next_dist and ndist < 600: + next_best = (nt, ncy, ncx) + next_dist = ndist + if not next_best: + break + nt, ncy, ncx = next_best + nn = nt.lstrip('--—= ') + nn = re.sub(r'[\d.\-—=]+$', '', nn).strip() + nn = nn.lstrip('-—-=') + if '\\' in nn: + nn = nn.split('\\', 1)[0].strip() + else: + nidx = max(nn.rfind('-'), nn.rfind('-'), nn.rfind('—')) + if nidx >= 0: + nn = nn[nidx+1:].strip() + if nn and len(nn) >= 2 and not ( + len(nn) < 3 or nn.endswith('/') + or nn in ('里C', '里A', '假台', '里C3', '里A3') + or re.search(r'[里景苑园]$', nn) + or re.search(r'[A-Z]\d', nn) + ): + code['name'] = nn + break + # 这个也不行,继续下一个 + name_candidates = [(ct, ccy, ccx) for ct, ccy, ccx in name_candidates + if ct != nt or ccy != ncy or ccx != ncx] + else: + code['name'] = n return codes