发票号/订单号批量提取查重:跨文件抓票号,重复报销一眼现形

2026-10-06 · 喵窝资源铺

报销单、凭证、台账分散在好几个文件里,票号格式还五花八门:144012309876、1440-1230-9878、1 4401 2309 881。肉眼对票号防重复报销,几十条就已经眼花。这篇给一个 60 行脚本:批量扫 Excel 备注列和文本凭证,正则提取票号,归一化后跨文件查重,重复的直接列出「首见在哪、重复在哪」。代码实跑验证,真实输出贴在下面。

输入输出长什么样

3 个测试文件:报销单 Excel(含带横杠/空格分隔的票号、8 位短号、无票号行)、文本凭证(两行同票号=跨文件重复)、数字单元格 Excel。跑完:

=== 提取结果:共 13 个票号,去重后 8 个 ===
  144012309876  @baoxiao.xlsx:2  报销 办公用品 发票号144012309876 广场酒店
  144012309877  @baoxiao.xlsx:3  住宿  ticket no. 144012309877
  144012309878  @baoxiao.xlsx:5  发票号码:1440-1230-9878 带横杠
  144012309879  @baoxiao.xlsx:7  票号144012309879(电子票)
  144012309876  @baoxiao.xlsx:8  重复报销 发票号144012309876
  144012309880  @baoxiao.xlsx:10  缺日期 有票号144012309880
  144012309881  @baoxiao.xlsx:11  增值税  1 4401 2309 881  空格分隔
  144012309882  @vouchers.txt:1  报销凭证 张三 2026-09-12 发票代码144012309882 打印费12元
  144012309882  @vouchers.txt:2  差旅 王五 144012309882 出租车  46.00
  144012309876  @numcell.xlsx:2  144012309876
  144012309876  @numcell.xlsx:3  144012309876
  144012309876  @numcell.xlsx:4  144012309876
  @TRUNCATED@14401230987612350464  @numcell.xlsx:5  1.440123098761235e+19

=== 跨文件/跨行重复 5 处 ===
  144012309876: 首见 baoxiao.xlsx:2 | 重复 baoxiao.xlsx:8
  144012309882: 首见 vouchers.txt:1 | 重复 vouchers.txt:2
  144012309876: 首见 baoxiao.xlsx:2 | 重复 numcell.xlsx:2
  144012309876: 首见 baoxiao.xlsx:2 | 重复 numcell.xlsx:3
  144012309876: 首见 baoxiao.xlsx:2 | 重复 numcell.xlsx:4

13 个票号提取后 8 个唯一值,5 处重复全揪出(含同一票号在报销单里出现两次——典型的重复报销线索)。「编号12345」这种 8 位短号和无票号的「餐饮招待」行都没误报。

完整脚本

# -*- coding: utf-8 -*-
"""发票号/订单号批量提取查重:扫 Excel 备注列 + 文本凭证,正则抓票号,跨文件查重。"""
import re, sys, os
from openpyxl import load_workbook

# 票号特征:8位号码(按你的票号位数改 {8}),兼容空格/横杠分隔
RE_TICKET = re.compile(r'(?=8位整数)说明被Excel转科学计数、尾数已丢——报错不是猜。"""
    if isinstance(v, bool):
        return None
    if isinstance(v, int):
        return str(v) if RE_INT_CELL.match(str(v)) else None
    if isinstance(v, float):
        if v == int(v) and len(str(int(v))) >= 8:
            return '@TRUNCATED@' + str(int(v))  # 精度已丢,标记转人工
    return None

def normalize(s):
    """票号统一去空格和横杠,用于查重比对。"""
    return re.sub(r'[-\s]', '', s)

def scan_text(path):
    found = []
    with open(path, encoding='utf-8', errors='ignore') as f:
        for i, line in enumerate(f, 1):
            for m in RE_TICKET.finditer(line):
                found.append((os.path.basename(path), i, normalize(m.group(1)), line.strip()))
    return found

def scan_excel(path, col_names=('备注', '事由', '摘要', '说明', '发票号', '票号', '订单号')):
    wb = load_workbook(path, data_only=True)
    ws = wb.active
    hdr = [c.value for c in ws[1]]
    cols = [i for i, h in enumerate(hdr) if h in col_names]
    found = []
    for rno, row in enumerate(ws.iter_rows(min_row=2, values_only=True), 2):
        for ci in cols if cols else range(1, len(row)):
            v = row[ci]
            if v is None:
                continue
            s = str(v)
            if not s.strip():
                continue
            matched = False
            for m in RE_TICKET.finditer(s):
                found.append((os.path.basename(path), rno, normalize(m.group(1)), s.strip()))
                matched = True
            if not matched:
                n = cell_num(v)
                if n:
                    found.append((os.path.basename(path), rno, n, s.strip() or '(数字单元格)'))
    return found

if __name__ == '__main__':
    items = []
    for p in sys.argv[1:]:
        if p.endswith('.xlsx'):
            items += scan_excel(p)
        else:
            items += scan_text(p)
    seen = {}
    dup = []
    for src, loc, num, ctx in items:
        if num in seen:
            dup.append((num, seen[num], f'{src}:{loc}'))
        else:
            seen[num] = f'{src}:{loc}'
    print(f'=== 提取结果:共 {len(items)} 个票号,去重后 {len(seen)} 个 ===')
    for src, loc, num, ctx in items:
        print(f'  {num}  @{src}:{loc}  {ctx[:40]}')
    print(f'\n=== 跨文件/跨行重复 {len(dup)} 处 ===')
    for num, first, second in dup:
        print(f'  {num}: 首见 {first} | 重复 {second}')

实跑揪出来的三个真坑

怎么用到自己文件上

python invoice_dedup.py 报销单.xlsx 凭证.txt 台账.xlsx 一次传多个文件即可。票号位数在 RE_TICKET 的 {8} 处改;列名不匹配时会自动扫所有列(首行表头匹配 备注/事由/摘要/发票号/票号/订单号)。重复不一定是报销造假——也可能是同一张票跨系统录入,但都该人工看一眼。如果你要的是按内容归集而不是查重,可以看本站的报销单按事由归集科目。

不想自己跑?把活儿给我。把你的报销单/凭证文件发我,我写好脚本、用你的真实文件实测后交付(¥19.9 表格处理 / ¥29.9 脚本定制)。
→ 去下单页看看 | 虚拟物品交付后不退款;未收到或未达承诺免费补发/重做。

相关文章:报销单按事由关键词归集科目 · 身份证号批量提取去重 · 多Excel按关键字搜客户

← 更多实跑干货 | 喵窝资源铺