fix: 科目名称提取v2 + 摘要修复

1. 摘要:排除期号文本(J22年第9期/22年第9期等)
2. 科目补位v2:
   - 先提取清洗后名称再评分(而非评分原始OCR文本)
   - 评分窗口±30px~+500px(下方优先,允许同行偏移)
   - 始终运行补位,与内联名称比分(内联基线10分)
   - y距惩罚加强至1/10(防跨行误抓)
   - 排除非科目文本(房号模式\d-\d{3,4}、未到账等)
   - 最小名称长度3(排除假台等噪声)
3. 之前code_map去重保留(Bug 1)
This commit is contained in:
xiaoxue_admin 2026-06-23 00:10:47 +08:00
parent f1041972a7
commit d2257b0fce
1 changed files with 76 additions and 42 deletions

View File

@ -240,54 +240,88 @@ class K3VoucherParser:
name_candidates.sort(key=lambda x: x[1])
for code in codes:
# 对所有代码行(即使已有内联名称)运行补位评分
candidates = []
# 基线:内联名称(如已存在,分数加成)
if code['name'] and len(code['name']) >= 2:
continue # 已有名称,跳过
# 评分制选取最佳名称补位:优先含全角横杠的科目名称文本
scored = []
candidates.append({
'name': code['name'],
'is_inline': True,
'y': code['y_pos'],
'x': code['x_pos'],
'raw': '',
})
for t, cy, cx in name_candidates:
if cy <= code['y_pos']:
continue # 只找下方的
# 只找下方 ±30px允许同行偏移但不找上方无关文本
if cy < code['y_pos'] - 30:
continue
if cy > code['y_pos'] + 500:
continue
if abs(cx - code['x_pos']) > 600:
continue
dist = cy - code['y_pos']
if dist >= 600:
# 先提取清洗后的名称
n = t.lstrip('-—= ')
n = re.sub(r'[\d.\-—=]+$', '', n).strip()
n = n.lstrip('-—-=')
if '\\' in n:
n = n.split('\\', 1)[0].strip()
else:
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 any(k in t for k in ['未到账', '过账', 'Manager', '出纳', '制单', '审核', '经办']):
continue
# 排除房号/楼栋模式文本(如 "假日里C1二单元假日重C1-2-0201"
if re.search(r'\d-\d{3,4}', t):
continue
# 评分:高的优先
score = 0
if '' in t or '-' in t:
score += 10 # 含横杠(全角/ASCII→ 科目名称信号
if re.search(r'\d{10,}', t):
score -= 30 # 银行账号10位以上数字→ 强烈惩罚
if len(t) >= 4:
score += 3 # 长文本更可能是有效科目名
# 同列优先(科目名称通常在代码同一列下方)
x_dist = abs(cx - code['x_pos'])
if x_dist < 50:
score += 25
elif x_dist < 150:
score += 15
elif x_dist < 300:
score += 5
score -= dist / 30 # 近的优先tiebreaker
scored.append((score, t, cy, cx))
candidates.append({
'name': n,
'is_inline': False,
'y': cy,
'x': cx,
'raw': t,
})
if not scored:
if not candidates:
continue
scored.sort(key=lambda x: -x[0])
t, cy, cx = scored[0][1], scored[0][2], scored[0][3]
# 按用户规则提取名称(先清洗尾部全角横杠,防止取到最后无内容)
n = t.lstrip('-—= ')
n = re.sub(r'[\d.\-—=]+$', '', n).strip()
n = n.lstrip('-—-=')
if '\\' in n:
n = n.split('\\', 1)[0].strip()
else:
idx = max(n.rfind(''), n.rfind('-'), n.rfind(''))
if idx >= 0:
n = n[idx+1:].strip()
if n and len(n) >= 2:
code['name'] = n
# 评分:基于清洗后的名称,而非原始 OCR 文本
for cand in candidates:
score = 0
if cand['is_inline']:
score = 10 # 内联名称基线分(有实际来源,更高可信度)
else:
if '' in cand['raw'] or '-' in cand['raw']:
score += 10 # 含横杠信号
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
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']
return codes
@ -384,9 +418,9 @@ class K3VoucherParser:
# 按 y 排序,取最靠上且有意义的候选
candidates.sort(key=lambda x: x[1])
# 过滤掉日期/期号类文本
# 过滤掉日期/期号类文本(如"J22年第9期"、"022年第9期"
real_summaries = [t for t, y in candidates
if not re.match(r'\d{3}年第\d+', t)
if not re.match(r'.*\d{1,4}年第?\d{1,2}', t)
and not re.match(r'[:]\d+', t)
and '日期' not in t
and len(t) >= 6]