fix: 恢复原始补位逻辑+迭代式名称质量检查
核心改动:恢复为原始找下方最近文本补位逻辑,不再使用评分系统。 保留的有效修补: 1. code_map 替代 seen(重复代码去重) 2. 摘要期号过滤(J22年第9期等) 3. 排除管理字段(过账/审核/Manager等) 4. 迭代式名称质量检查——如果最近文本提取的名称可疑 (<3字、以/结尾、园区名结尾、含[A-Z]\d楼号、已知坏名) 自动尝试下一个候选,最多10次
This commit is contained in:
parent
cb1a458966
commit
3e23e399e8
183
core/parser.py
183
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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue