fix: 恢复原始补位逻辑+迭代式名称质量检查

核心改动:恢复为原始找下方最近文本补位逻辑,不再使用评分系统。

保留的有效修补:
1. code_map 替代 seen(重复代码去重)
2. 摘要期号过滤(J22年第9期等)
3. 排除管理字段(过账/审核/Manager等)
4. 迭代式名称质量检查——如果最近文本提取的名称可疑
   (<3字、以/结尾、园区名结尾、含[A-Z]\d楼号、已知坏名)
   自动尝试下一个候选,最多10次
This commit is contained in:
xiaoxue_admin 2026-06-23 00:39:37 +08:00
parent cb1a458966
commit 3e23e399e8
1 changed files with 72 additions and 111 deletions

View File

@ -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