采购明细、对账单里的表格要进入业务系统,关键是让编号、项目、金额和备注保持对应。得到一段文字或一个 JSON 文件,只是开始;还要回到原文检查漏行、错列和空白。
下面使用 ComPDF DocSlight 云端 SDK,演示有线框、无边框和图片 PDF 的表格解析,提供 Python 接入代码、结果核对方法和可复现的教学样例。
1. 用三份可核对的 PDF 开始
想从头复现,先运行文末的 make_samples.py,生成三份 PDF 和核对用的 expected.json;再按第 2、3 节安装 SDK 并提交文件。也可以直接使用一份可用于云端测试的 PDF。
三份文件使用同一组教学示例采购数据,包含 6 条明细:
| 序号 | 项目 | 金额(元) | 备注 |
|---|---|---|---|
| A-001 | 零件A | 1,200.00 | |
| A-002 | 零件B | 300.00 | 含运费 |
| A-003 | 零件C | 85.50 | |
| A-004 | 零件D | 1,200.00 | 待复核 |
| A-005 | 零件E | 234.50 | |
| A-006 | 零件F | 80.00 |
ruled.pdf 有文本层和表格线,明细分在两页;borderless.pdf 去掉表格线,6 行在一页;image_only.pdf 把有线框样本的页面转成图片,再封装为两页 PDF,没有原来的文本层。
图片版本用于检查图片内容的识别,不包含真实扫描件的倾斜、噪点、模糊或手写。金额合计为 3,100.00 元,但合计一致不能单独证明每行正确,必须同时核对行列关系。
2. 准备云端 SDK 环境
本次使用 Python 3.12.14、docslight==0.1.5,实际调用的类是 DocSlight,输出方法是 to_markdown() 和 to_json()。
以下安装方式适用于本文的 Python 3.12 云端接入:先安装云端需要的基础依赖,再安装 SDK。这样可以避免同时安装本次不使用的本地 OCR、Office 和 Web 依赖。它不是本地解析模式的完整安装方式。
python -m pip install requests==2.34.2 pydantic==2.14.0 typing-extensions==4.16.0
python -m pip install --no-deps docslight==0.1.5
在 ComPDF 控制台 的"API → 密钥与项目"取得项目公钥 publicKey,设置当前终端的 DOCSLIGHT_API_KEY。不要将真实凭据写进公开代码、文章或截图。
macOS/Linux 终端:
export DOCSLIGHT_API_KEY="替换为你自己的项目公钥"
Windows PowerShell:
$env:DOCSLIGHT_API_KEY="替换为你自己的项目公钥"
云端模式会上传文件处理。先用可用于云端测试的文件,并查看账号当前额度与计费说明。
3. 提交 PDF,保留原始解析结果
将下面代码保存为 parse_with_docslight.py。它调用自家 SDK 的云端解析入口,保存 Markdown 和 SDK JSON;响应带有原始 ZIP 时也一并保留。

图 1:DocSlight 云端接入流程示意。先保留解析输出,再按原文核验。
import argparse
import json
import os
from pathlib import Path
from docslight import DocSlight
def main():
parser = argparse.ArgumentParser()
parser.add_argument("pdf")
parser.add_argument("--output-dir", default="parsed")
args = parser.parse_args()
api_key = os.environ.get("DOCSLIGHT_API_KEY")
if not api_key:
raise SystemExit("请先在当前终端设置 DOCSLIGHT_API_KEY。")
client = DocSlight(mode="cloud", api_key=api_key, timeout=120)
result = client.parse(Path(args.pdf))
output = Path(args.output_dir)
output.mkdir(parents=True, exist_ok=True)
(output / "result.md").write_text(result.to_markdown(), encoding="utf-8")
(output / "result.json").write_text(
json.dumps(result.to_json(), ensure_ascii=False, indent=2), encoding="utf-8"
)
if result.raw_archive is not None:
(output / "raw_result.zip").write_bytes(result.raw_archive)
print(f"解析结果已保存到 {output}")
if __name__ == "__main__":
main()
依次运行三份样本:
python parse_with_docslight.py ruled.pdf --output-dir parsed/ruled
python parse_with_docslight.py borderless.pdf --output-dir parsed/borderless
python parse_with_docslight.py image_only.pdf --output-dir parsed/image_only
| 文件 | 用途 |
|---|---|
result.md |
查看文字、标题及表格结构 |
result.json |
保存 SDK 的解析结果,供程序读取 |
raw_result.zip |
响应包含归档时保留原始输出 |
本次 result.md 的表格以 HTML <table> 表达,并非所有内容都使用 Markdown 竖线表格。result.json 包含 markdown、pages、metadata 等内容;页面的 parsing_res_list 中可查看 block_type、block_content 和 block_bbox。
这些是本次版本和响应中的结构。写业务映射前应先检查自己拿到的输出;不要把自行整理的业务 JSON 误认为产品原生格式。位置字段也应结合该响应的页面宽高解释,不能直接套用其他 PDF 工具的坐标单位。
4. 本次实际得到什么

图 2:ComPDF 在线演示的实际处理界面,左侧为教学 PDF,右侧为解析结果。
三份 PDF 均由上述 SDK 云端解析流程处理,逐单元格与原样本核对:
| 文件 | SDK 返回页数 | 结果中的表格片段 | 明细核对结果 |
|---|---|---|---|
ruled.pdf |
2 | 2 | 6 行、24 个数据单元格与样本一致,空白备注保留 |
borderless.pdf |
1 | 1 | 6 行、24 个数据单元格与样本一致,空白备注保留 |
image_only.pdf |
2 | 2 | 6 行、24 个数据单元格与样本一致,空白备注保留 |
每份结果的金额合计为 3,100.00 元。这里的"24 个数据单元格"是 6 行 × 4 列,不包含表头;我们检查了每行的编号、项目、金额和备注,不只检查行数或合计。
上述结果来自这三份教学样本。评估实际业务文件时,建议增加倾斜、噪点、合并单元格等有代表性的样本,并按下节的检查清单逐项核对。
例如 A-002 对应的金额仍是 300.00,备注仍是"含运费";A-003 对应 85.50,备注为空。空白没有被补成 0,也没有导致后面的内容向左移位。
两份两页文件的输出仍有两个表格片段,不能据此宣称本次已自动合并跨页表格。接入业务系统时,应先保存页级来源,再依据单据标识、表头含义、单位和续表关系决定是否合并。
我们也在 ComPDF AI 文档解析演示 上传相同文件核对。图片样本开启"图片文字识别(OCR)"后首次显示"解析失败,请重试",重试后成功,表格明细与样本一致;SDK 的本次图片样本调用成功。两种入口的设置和执行过程分别记录,不能把一次成功理解为任何文件、任何设置下都会成功。
5. 换成自己的文件,先检查这六项
| 检查项 | 具体检查 |
|---|---|
| 行数 | 有没有漏行、重复行,或把标题当成明细 |
| 列关系 | 编号、项目、金额是否属于同一行 |
| 金额 | 千分位、小数、负号、币种和单位是否正确 |
| 空白 | 空值是否仍在原列,没有擅自填补或继承 |
| 表头与跨页 | 多级表头、重复表头和续表关系是否需要处理 |
| 原文对应 | 能否回到来源文件与页面核对有疑问的内容 |
先保留原始字符串,例如 "1,200.00"。确认单位和业务规则后,再转换为金额类型。不要一开始就删除空单元格或自动拼接所有同名表头。
如果失败,先用一页文件缩小范围,记录使用的 SDK 版本、处理模式、错误提示和文件特征。区分请求或凭据问题、解析失败,以及"解析成功但关键值不正确";这三种情况的处理方法不同。
需要保留文档内容和结构时,可从文档解析开始;只需要合同号、日期、金额等指定信息时,再评估字段抽取。本文实测的是解析流程,没有执行字段抽取,也没有验证检索或问答效果。
接入资料:DocSlight 官方项目、ComPDF 文档解析 API 文档、ComPDF AI 文档解析说明。
6. 下一步:用自己的样本验证,再决定如何接入
想先看处理结果,可以在电脑端打开 ComPDF 官网文档解析演示,登录后上传一页脱敏样本,核对编号、金额、空白和表头。图片 PDF 可在设置中开启"图片文字识别(OCR)",再查看结果。
确认输出符合业务要求后,再用上面的 SDK 代码接入。需要批量处理、私有化部署或特殊版式评估时,可以通过 ComPDF 官网咨询入口说明文件类型、处理量、部署要求和已发现的问题,便于评估适合的方案。
附:生成本文教学样本
以下代码用于生成教学样例 PDF 和核对数据。表格解析由前面的 ComPDF DocSlight 完成。
python -m pip install reportlab==4.4.9 pypdfium2==5.13.0
python make_samples.py
将下面代码保存为 make_samples.py:
import json
from pathlib import Path
import pypdfium2 as pdfium
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
from reportlab.pdfgen import canvas
OUT = Path(".")
OUT.mkdir(parents=True, exist_ok=True)
pdfmetrics.registerFont(UnicodeCIDFont("STSong-Light"))
HEADERS = ["序号", "项目", "金额(元)", "备注"]
ROWS = [
["A-001", "零件A", "1,200.00", ""],
["A-002", "零件B", "300.00", "含运费"],
["A-003", "零件C", "85.50", ""],
["A-004", "零件D", "1,200.00", "待复核"],
["A-005", "零件E", "234.50", ""],
["A-006", "零件F", "80.00", ""],
]
WIDTH, HEIGHT = 595, 842
XS = [48, 138, 298, 423, 547]
def page(c, rows, page_number, ruled):
c.setFillColorRGB(0.12, 0.20, 0.28)
c.setFont("STSong-Light", 20)
c.drawString(48, 783, "教学采购明细")
c.setFont("STSong-Light", 10)
c.drawString(48, 758, "采购数据用于教学演示与结果核对。")
c.drawString(48, 738, f"采购单 DEMO-001 / 第 {page_number} 页")
top = 706
row_height = 30
if ruled:
c.setFillColorRGB(0.91, 0.95, 0.97)
c.rect(XS[0], top - row_height, XS[-1] - XS[0], row_height, fill=1, stroke=0)
c.setStrokeColorRGB(0.55, 0.63, 0.68)
c.setLineWidth(0.6)
bottom = top - row_height * (len(rows) + 1)
for x in XS:
c.line(x, bottom, x, top)
for i in range(len(rows) + 2):
y = top - i * row_height
c.line(XS[0], y, XS[-1], y)
c.setFillColorRGB(0.12, 0.20, 0.28)
c.setFont("STSong-Light", 11)
for i, row in enumerate([HEADERS, *rows]):
for x, value in zip(XS, row):
c.drawString(x + 10, top - 20 - i * row_height, value)
c.setFont("STSong-Light", 10)
c.drawString(48, top - row_height * (len(rows) + 1) - 30, "单位:人民币元;空白备注按原样保留。")
c.drawString(48, 52, "本样本由 ReportLab 生成;页面编号供结果核对使用。")
c.showPage()
c = canvas.Canvas(str(OUT / "ruled.pdf"), pagesize=(WIDTH, HEIGHT))
page(c, ROWS[:4], 1, True)
page(c, ROWS[4:], 2, True)
c.save()
c = canvas.Canvas(str(OUT / "borderless.pdf"), pagesize=(WIDTH, HEIGHT))
page(c, ROWS, 1, False)
c.save()
doc = pdfium.PdfDocument(str(OUT / "ruled.pdf"))
c = canvas.Canvas(str(OUT / "image_only.pdf"), pagesize=(WIDTH, HEIGHT))
for i in range(len(doc)):
image = doc[i].render(scale=2).to_pil()
c.drawInlineImage(image, 0, 0, width=WIDTH, height=HEIGHT)
c.showPage()
c.save()
(OUT / "expected.json").write_text(json.dumps(
{"headers": HEADERS, "rows": ROWS, "total_cny": "3100.00",
"crop_bbox": [48, 136, 547, 346]},
ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Created three teaching PDFs in {OUT}")