You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用正则表达式(regex)实现PDF发票文本的结构化分组解析

PDF发票提取文本的正则分组解析方案

前置预处理

正则匹配前必须先做文本归一化,否则字段间隔不固定的问题会大幅降低匹配成功率:

  • 统一替换所有全角空格、不间断空格(\u00A0)、制表符为半角空格
  • 将连续3个及以上的半角空格压缩为2个半角空格作为字段分隔标记,避免单空格把描述内容拆碎
  • 提前过滤页眉、页脚、总金额合计行、税率说明行等非明细内容,按行拆分待匹配文本

分场景正则规则

所有正则默认开启多行匹配模式,金额匹配兼容千分位逗号、2位小数的常见发票格式,站点编号规则可根据实际字符范围调整:

1. 带日期的常规服务记录

这类行首为日期字段,用如下正则匹配:

^(?P<Date>\d{4}[-/]\d{1,2}[-/]\d{1,2})\s+(?P<Site>[A-Z0-9-]+)\s+(?P<Description>[\u4e00-\u9fa5\w\s]+?)(?:\s{2,}(?P<Description_2>[\u4e00-\u9fa5\w\s]+?))?\s+(?P<Qty>\d+(?:\.\d+)?)\s+(?P<Price>\d{1,3}(?:,\d{3})*(?:\.\d{2})?)\s+(?P<Total>\d{1,3}(?:,\d{3})*(?:\.\d{2})?)$

分组说明:

  • Date:锚定行首的YYYY-MM-DD/YYYY/MM/DD格式日期
  • Site:匹配紧跟日期的站点编号,默认支持大写字母、数字、横杠组合
  • Description:非贪婪匹配核心服务描述,遇到连续2个空格(补充描述分隔位)或后续数字字段自动终止
  • Description_2:可选分组,匹配不定长的补充描述,无补充内容时返回空值
  • Qty/Price/Total:分别匹配数量、单价、总价,兼容整数、小数、带千分位的金额格式

2. 无日期的站点租赁类记录

这类行首无日期,第一个字段为站点编号,用如下正则匹配:

^(?P<Site>[A-Z0-9-]+)\s+(?P<Description>站点租赁[\u4e00-\u9fa5\w\s]*?)(?:\s{2,}(?P<Description_2>[\u4e00-\u9fa5\w\s]+?))?\s+(?P<Qty>\d+(?:\.\d+)?)\s+(?P<Price>\d{1,3}(?:,\d{3})*(?:\.\d{2})?)\s+(?P<Total>\d{1,3}(?:,\d{3})*(?:\.\d{2})?)$

匹配到这类行时,Date字段可统一填充对应账期的日期,默认留空即可。

匹配优化要点

  • 不要尝试用单个正则覆盖所有行类型,分场景写正则的容错率远高于单条复杂正则
  • 描述类字段必须用非贪婪匹配,靠后续数量、金额这类固定格式的数字字段做锚定终止,才能适配任意长度的描述内容
  • 所有可选分组匹配后统一设置空值默认值,避免字段缺失导致程序报错
  • 匹配完成后增加数值校验:Qty * Price和Total的差值小于0.01(兼容浮点计算误差)则判定为匹配有效,校验不通过的行单独捞出人工复核,可覆盖99%的匹配异常

参考实现(Python)

import re

def preprocess(raw_text: str) -> list[str]:
    # 替换特殊空白字符
    text = raw_text.replace('\u00A0', ' ').replace('\t', ' ')
    # 保留2个连续空格作为字段分隔
    text = re.sub(r' {3,}', '  ', text)
    # 拆分非空行
    return [line.strip() for line in text.splitlines() if line.strip()]

# 预编译正则
re_with_date = re.compile(r'^(?P<Date>\d{4}[-/]\d{1,2}[-/]\d{1,2})\s+(?P<Site>[A-Z0-9-]+)\s+(?P<Description>[\u4e00-\u9fa5\w\s]+?)(?:\s{2,}(?P<Description_2>[\u4e00-\u9fa5\w\s]+?))?\s+(?P<Qty>\d+(?:\.\d+)?)\s+(?P<Price>\d{1,3}(?:,\d{3})*(?:\.\d{2})?)\s+(?P<Total>\d{1,3}(?:,\d{3})*(?:\.\d{2})?)$')
re_no_date = re.compile(r'^(?P<Site>[A-Z0-9-]+)\s+(?P<Description>站点租赁[\u4e00-\u9fa5\w\s]*?)(?:\s{2,}(?P<Description_2>[\u4e00-\u9fa5\w\s]+?))?\s+(?P<Qty>\d+(?:\.\d+)?)\s+(?P<Price>\d{1,3}(?:,\d{3})*(?:\.\d{2})?)\s+(?P<Total>\d{1,3}(?:,\d{3})*(?:\.\d{2})?)$')

def parse_invoice_line(line: str) -> dict | None:
    # 优先匹配带日期行
    match = re_with_date.match(line)
    if match:
        res = match.groupdict()
        res.setdefault('Description_2', '')
        return res
    # 匹配无日期租赁行
    match = re_no_date.match(line)
    if match:
        res = match.groupdict()
        res['Date'] = ''
        res.setdefault('Description_2', '')
        return res
    # 匹配失败返回空,待人工处理
    return None

内容的提问来源于stack exchange,提问作者Rander

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 06:27:25