电子发票通常以PDF格式存储,包含发票代码、发票号码、开票日期、金额、购买方信息、销售方信息等多类结构化数据,但PDF格式本身不支持数据直接提取,XML作为结构化数据交换的标准格式,能够被财务系统、税务系统直接解析,因此实现电子发票PDF到XML的转换是财务数字化的核心环节之一。
常用转换方案分类
1. 基于固定模板的规则解析方案
该方案适用于版式固定的电子发票,比如增值税发票电子普通发票,其PDF内容的排版位置相对固定,可通过定位关键字所在坐标提取对应数据。
实现步骤如下:
- 使用PDF解析库读取PDF的文本内容及每个文本块的坐标信息
- 预设关键字与数据字段的映射关系,比如找到"发票代码"关键字后,提取其右侧固定区域的文本作为发票代码值
- 将提取到的字段按照XML格式规范组装成完整的XML文档
以下是使用Python的pdfplumber库实现的示例代码:
import pdfplumber
import xml.etree.ElementTree as ET
def pdf_to_xml_by_rule(pdf_path, xml_path):
# 创建XML根节点
root = ET.Element("invoice")
with pdfplumber.open(pdf_path) as pdf:
page = pdf.pages[0]
# 提取所有文本块及坐标
words = page.extract_words()
# 定义关键字映射规则
keyword_map = {
"发票代码": "invoice_code",
"发票号码": "invoice_number",
"开票日期": "invoice_date",
"合计金额": "total_amount"
}
for word in words:
text = word["text"]
for keyword, field_name in keyword_map.items():
if keyword in text:
# 提取关键字后的文本作为字段值,这里简化为取同一行后续文本
# 实际场景需要根据坐标调整提取范围
field_value = text.split(keyword)[-1].strip()
child = ET.SubElement(root, field_name)
child.text = field_value
break
# 生成XML文件
tree = ET.ElementTree(root)
tree.write(xml_path, encoding="utf-8", xml_declaration=True)
# 调用示例
pdf_to_xml_by_rule("test_invoice.pdf", "output.xml")
2. OCR识别结合模板映射方案
当电子发票PDF是扫描件或者版式不固定时,规则解析方案容易失效,此时可结合OCR技术识别PDF中的文本内容,再通过模板映射提取数据。
核心实现逻辑:
- 使用OCR工具将PDF的每一页转换为文本,同时保留文本的大致位置信息
- 通过正则表达式匹配发票相关的字段格式,比如发票代码是10位数字,发票号码是8位数字
- 将匹配到的内容映射到对应的XML字段中,对于识别误差的内容可增加人工校验环节
以下是使用pytesseract和pdf2image实现的示例代码:
import pytesseract
from pdf2image import convert_from_path
import re
import xml.etree.ElementTree as ET
def pdf_to_xml_by_ocr(pdf_path, xml_path):
root = ET.Element("invoice")
# 将PDF转换为图片
images = convert_from_path(pdf_path)
full_text = ""
for img in images:
# OCR识别图片文本
text = pytesseract.image_to_string(img, lang="chi_sim")
full_text += text
# 正则匹配发票字段
invoice_code_pattern = re.compile(r"发票代码[::]?s*(d{10})")
invoice_number_pattern = re.compile(r"发票号码[::]?s*(d{8})")
amount_pattern = re.compile(r"合计金额[::]?s*(d+.d{2})")
invoice_code_match = invoice_code_pattern.search(full_text)
if invoice_code_match:
child = ET.SubElement(root, "invoice_code")
child.text = invoice_code_match.group(1)
invoice_number_match = invoice_number_pattern.search(full_text)
if invoice_number_match:
child = ET.SubElement(root, "invoice_number")
child.text = invoice_number_match.group(1)
amount_match = amount_pattern.search(full_text)
if amount_match:
child = ET.SubElement(root, "total_amount")
child.text = amount_match.group(1)
tree = ET.ElementTree(root)
tree.write(xml_path, encoding="utf-8", xml_declaration=True)
# 调用示例
pdf_to_xml_by_ocr("scan_invoice.pdf", "ocr_output.xml")
3. 开源工具二次开发方案
目前有不少开源的电子发票处理工具已经实现了PDF解析、数据提取的基础能力,可基于这些工具进行二次开发,快速实现PDF转XML的功能。
以开源电子发票解析库为例,其通常已经内置了多种类型电子发票的解析模板,可直接调用对应接口提取结构化数据,再转换为XML格式,开发成本较低。
方案对比与选择建议
不同方案的适用场景和优缺点如下:
| 方案类型 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 固定模板规则解析 | 版式固定的电子发票PDF | 转换速度快,准确率高,无需额外依赖 | 适配性差,版式变化后需要重新调整规则 |
| OCR结合模板映射 | 扫描件、版式不固定的电子发票PDF | 适配性强,可处理多种类型发票 | 识别准确率受PDF清晰度影响,转换速度较慢 |
| 开源工具二次开发 | 快速落地需求的场景 | 开发成本低,内置多类发票模板 | 定制化能力弱,复杂场景需要修改源码 |
转换注意事项
- 电子发票中的特殊字符如&、<、>在写入XML时需要进行转义,避免XML格式错误
- 对于金额类字段,需要校验数值的合法性,避免提取到错误数据
- 批量转换时可增加日志功能,记录转换失败的发票路径和失败原因,方便后续排查问题
- 敏感数据如购买方税号、销售方税号在传输和存储过程中需要做好加密处理
实际落地时可根据企业的发票类型占比、转换频率、准确率要求综合选择方案,也可将多种方案结合,比如优先使用规则解析,解析失败后再调用OCR方案兜底,提升整体转换成功率。