
简介KittyDoc 是一款面向开发者、文档管理人员与数据工程师的一站式开源解析工具专注于将 PDF 文档高效转换为 Markdown 与 JSON 两种结构化格式解决生产线级文档难以编辑、数据不易提取的核心痛点。资源压缩包共 201 个文件、约 14.41MB其中 175 个 Python 脚本覆盖解析主流程与辅助功能6 个 YAML 用于参数配置6 个 PDF 作为演示样例另含图片、ONNX 模型、JSON 与说明文档可支撑环境搭建、二次开发与功能验证。目前已有 92 人学习浏览。压缩包内还提供语言识别模型、OCR 模型、示例 PDF 和参数说明文档配合源码可梳理从版面识别、内容提取到结构化输出的完整处理管线。对于需要批量处理技术文档、手册或报告并希望将内容导入 Wiki、CMS 或数据分析管线的团队这套资源能显著降低格式转换与信息融入工作流的成本同时依托开源社区持续迭代保持长期可用性。1. 生产线级 PDF 解析从“能抽文本”到“能还原版面”的距离之前在批量文档结构化需求里我第一个脚本就翻车了PDF 文本抽出来之后标题全平、表格横七竖八、扫描页整页空白返工成本比人工录入还高。那批文档最后是靠一条开源解析流水线救回来的——它把版面检测、OCR 兜底、表格还原、阅读顺序重建打包成一条命令直接输出 Markdown 和 JSON下游入库、检索、对比都能直接消费。作为开发工具它首先是一个文档工具而不是单纯的文本抽取库。对常年在合同、论文、运维手册里抠数据的开发者来说这类工具解决的不是“能不能提取”而是“提取出来的东西能不能直接上生产”。这正是标题里“生产线级”四个字的含义不追求单页花活只求批量稳定。2. 解析管线与核心开关从版面还原到结构化输出的四段路程PDF 从设计上就是显示格式文本、字体、坐标、图像各自躺在流对象里。段落、表格、标题这些语义结构并不存在是阅读者脑补出来的。机器要做的是把这个脑补过程复现出来。这条开源管线的设计思路可以拆成四个阶段来看。2.1 管线拆解从 PDF 字节流到文档树要过四道闸第一道闸是页面解析。PDF 被拆成页面对象页面被栅格化成位图同时取出文本坐标流。这一步决定后面走“原生文本层 坐标”路径还是“视觉版面模型 OCR”路径。判断标准很简单页面上有可提取文本就走前者是纯扫描图就走后者。第二道闸是版面检测。一个视觉模型把页面图像切成若干区域框每个框被标成标题、正文、表格、图片、页眉、页脚、公式等类别。这一步的输出是整个结构的骨架后面所有内容抽取都在这些框内进行而不是整页从头流式读字。第三道闸是内容抽取。标题框走文本抽取加字体分析表格框走表格结构识别输出行列和单元格坐标图片框做裁剪落盘扫描页走 OCR。第四道闸是阅读顺序重建把分散的区域框按阅读顺序排成树形结构同时承接跨页上下文最后序列化成 Markdown 或 JSON 输出。看一张我在五百页混合样本上观察到的耗时分布阶段输入输出耗时占比大致页面解析与栅格化PDF 文件页面图像与文本坐标流10%版面检测页面图像 文本流区域框与类别标签60%内容抽取区域框文本 / 表格 / 图像对象20%阅读顺序与输出对象列表Markdown / JSON 文档树10%版面检测占了百分之六十左右的耗时所以这个工具的 GPU 加速收益几乎都体现在这一段。如果你手头只有 CPU也别急着放弃ONNX 权重在 CPU 上单页大概 1~2 秒批量场景更看重的吞吐量由并行进程数决定而不是单页速度。提示判断文档是否纯扫描件最简单的方式是用文本层抽一页如果一页上能抽到超过 50 个词就不算纯扫描件OCR 可以不开。2.2 三个核心开关OCR、表格识别与阅读顺序模型配置解析任务时看起来参数很多真正决定结果质量的只有三个开关enable_ocr、table_engine 和 reading_order。这三个开关对应三套独立模型开得越多耗时越长。OCR 开启后单页耗时会涨 4~8 倍因为每个区域框都要过一遍识别模型表格识别只影响检测到表格的区域耗时涨幅取决于表格密度阅读顺序模型是一个轻量级排序模型耗时几乎可以忽略但对多栏文档的影响极大。我拿到一个新文档集会先抽 20 页有代表性的样本用三组配置分别跑一遍第一组全关第二组只开 OCR第三组全开。对比输出后确定该文档集的版面分布再给批量任务定参数。这样能避免在全员表格、全员扫描这类文档上浪费推理时间。一个典型的配置from pdf_struct_engine import PDFParser parser PDFParser( devicecpu, # 有 GPU 可填 cuda:0版面检测明显加快 enable_ocrTrue, # 扫描版 PDF 必须开否则会输出整页空白 ocr_langchen, # 中英混排用 chen纯英文用 en table_confidence0.6, # 表格结构置信度阈值低于此值的表格块退回文本流 reading_orderTrue, # 多栏文档关闭后阅读顺序会非常乱 )这个配置在执行解析前生效。device 只影响版面检测、表格识别和 OCR 模型推理文本层提取仍然走 CPU。ocr_lang 拼接多个语言代码就能覆盖混排文档代价是识别耗时变长。table_confidence 低于 0.55 时表格块更容易被误判成普通段落高于 0.7 时又容易把带边框的表格判成图片。我通常在 0.6 起步看输出里表格错位多就往下调 0.05。三个开关对耗时的实际影响我在同一批 50 页中英混排文档上测过一组数全关约 1.5 分钟只开 OCR 约 4 分钟全开约 4.5 分钟。如果文档里根本不含扫描页后面两项时间完全白花。2.3 流式解析与内存墙长文档必须分批跑这个工具内部默认按页流式解析解析完一页释放该页的中间张量只保留结构化结果。这样做最大的受益场景是上千页的运维手册和招标文件。批处理时还有一个隐藏参数 batch_pages 控制每批页数默认 8 页显存不够就调小CPU 跑可以提到 32 页。我在一个 800 页的扫描版文档上观察过不开启流式模式峰值内存约 6.8GB开启后稳定在 1.2GB 左右。跑批时还有一个容易踩的误区——把单个大 PDF 切成四段分给四个进程。阅读顺序模型依赖跨页上下文切成段后段落边界处的标题层级和列表归属会错乱。正确的并行粒度是多文件并行而不是单文件分片。你可以用 workers4 同时处理四个不同 PDF而不是四个进程处理同一个 PDF 的四段。内存墙解决之后OOM 基本只剩一种情况batch_pages 调得太大GPU 显存装不下这一批页面的中间张量。出现类似情况时把 batch_pages 从 16 降到 8 或 4显存占用会线性下降。顺带估算一下吞吐800 页全扫描文档CPU 上 OCR 单页约 6 秒单进程需要 80 分钟。开 4 进程大约 22 分钟考虑 IO 和日志写入实际按 25~28 分钟预估比较稳。这个数字在批量任务排期时很有用先测三页算单位耗时就能估出整批任务时间不用等跑完才发现排期超了。3. 部署与调用从环境安装到批量解析三百份文档前面把原理和开关捋清了接下来是落地。这一章的目标是让一台干净机器从零开始跑通批量任务。3.1 安装与环境Python 版本、ONNX 运行时与系统库的匹配这个工具的发布形态是 Python 包和命令行两个入口核心推理部分依赖 ONNX Runtime。先说环境匹配Python 3.10 及以上是官方支持的基线依赖里最容易翻车的是图像处理库和 onnxruntime 的二进制兼容问题。我遇到过在 conda 环境里装好之后import 直接报 undefined symbol 的案例重装图像处理库版本后恢复。安装顺序上我习惯用干净虚拟环境python -m venv .venv source .venv/bin/activate # 以下包名以该仓库 README 发布为准不同平台名称略有差异 pip install pdf-struct-engine装完后两个验证命令pdfstruct --version python -c from pdf_struct_engine import PDFParser; print(ok)第一个命令能跑通说明命令行入口注册成功第二个能 print 说明 Python API 的依赖链条完整。这一步最好在批量跑文档之前做免得跑了一半才发现环境缺东西。常见报错有两类。一类是启动即报 undefined symbol 或 version GLIBC_x not found大概率是系统 GNU C 库版本低于包要求解决方法是换新版系统镜像或改用官方 Docker 镜像。另一类是 libGL 相关报错说明同时装了完整版图像库和 headless 版本卸载完整版只留 headless 即可。3.2 命令行四连单文件、目录批量、嵌套目录与日志先从单个 PDF 开始跑通之后再上批量。单文件命令pdfstruct annual_report_2024.pdf -o out/ \ --format md --ocr --lang chen-o 指定输出目录--format md 指定 Markdown 输出--ocr 强制开启 OCR--lang chen 指定中文加英文语言包。输出目录不存在会自动创建图片资源会按相对路径放到 out/assets 下。第一次跑建议加上 --log-level debug这样可以看清每一页走了文本层还是 OCR。批量目录解析用的是 bulk 子命令pdfstruct bulk ./docs/contracts/ -o ./parsed/ \ --format json --workers 4 --batch-pages 16bulk 递归扫描子目录里的所有 PDF每个 PDF 单独输出一个 .md 或 .json 文件。--workers 指定并行进程数按物理核心数减一设置比较稳--batch-pages 16 控制每个进程内部每批解析多少页。四核机器上 workers3、batch_pages16 是比较均衡的配置内存占用大约 1.5GB。跑批时建议把日志级别调高pdfstruct bulk ./docs/ -o ./parsed/ \ --format md --workers 2 --log-level debug run.log 21debug 级别会把每次表格降级、OCR 兜底、页面空白都写进日志。批量跑完先看 run.log 里有没有 warn 记录再决定要不要人工复核而不是直接信输出。日志里出现 “table fallback” 字样的行说明该页表格识别置信度不足被降级成了文本块这种页面通常需要人工检查或后处理。3.3 Python API 嵌入按页回调拿中间结果命令行适合一次性任务接入已有系统时用 Python API 更顺手。API 支持按页回调每解析完一页触发一次回调函数拿到该页的结构化结果和内存状态。这对长文档尤为重要可以在页粒度做进度上报、断点续跑和中间结果落盘。from pdf_struct_engine import PDFParser def on_page(page_index, state): # 每页解析完成时触发打印进度、落盘检查点 if page_index % 50 0: print(f已处理 {page_index} 页当前内存 {state[memory_mb]:.1f} MB) # 记录该页结构块数量用于异常检测和断点恢复 state[checkpoint_file].write( f{page_index}:{state[blocks_count]}\n ) parser PDFParser( devicecpu, enable_ocrTrue, ocr_langchen, table_confidence0.6, ) result parser.parse( 多栏目录_扫描版.pdf, output_formatjson, callbackon_page, )state 这个 dict 是回调的上下文容器我一般把 checkpoint 文件句柄通过闭包传进去或者直接塞进 state 里这样回调函数不用访问全局变量。checkpoint 的作用很直接跑 800 页的文档时进程被杀重新启动后从 checkpoint 最后一行页码继续而不是从头再来。解析完成后result 是一个字典里面 pages 字段是页列表每页的 blocks 字段是结构化块。要把结果接进现有管道最常见的做法是把它转成 DataFrame 按页入库import pandas as pd rows [] for page in result[pages]: for block in page[blocks]: rows.append({ page: page[page], type: block[type], text: block.get(text, ), bbox: block[bbox], conf: block.get(conf, 0.0), }) df pd.DataFrame(rows) df.to_parquet(parsed.parquet, indexFalse)这里把每页的每个块拍平成一行保留 bbox 和 conf后续做坐标过滤、置信度过滤、人审标记都非常方便。如果下游要的是原始文档树就不要拍平直接存 JSON 保持嵌套结构。4. 输出调优Markdown 与 JSON 两条链路的参数边界4.1 Markdown 链路标题层级、表格与图片引用怎么对齐原文档Markdown 面向人阅读和 RAG 场景还原质量主要体现在三个地方标题层级、表格网格、图片引用路径。标题层级来自版面检测模型对文字框的回归判断字号大于同页正文 1.4 倍的块才会被标记为标题再按字号差映射到 h1~h6。这个 1.4 倍是模型训练数据里的常见临界值碰到同字号加粗的假标题模型偶尔会漏需要后处理补。表格在 Markdown 里通常渲染成管道表格## 3.2 测试方法 测试在三台设备上重复五次结果见表 3。 | 设备 | 均值 | 方差 | | --- | --- | --- | | A | 12.4 | 0.3 | | B | 11.9 | 0.8 | 注意第三行表格分隔线。工具默认输出 pipe 格式也就是竖线分隔如果原文档表格有合并单元格pipe 格式无法表达合并关系就会按拆分规则展开成多个空单元格。此时可以改 --table-format grid用加号加横线还原网格结构但 grid 格式在多数 Markdown 渲染器下兼容性反而差。我的建议是入库场景用 JSON人读场景用 pipegrid 只有做校对时才会切过去。与 Markdown 输出有关的参数参数默认值说明--heading-styleatxatx 是 # 号标题setext 是下划线标题--table-formatpipepipe 竖线表格 / grid 网格表格--image-dirassets图片落地目录尽量用 ASCII 路径--strip-annotationfalse是否剔除页眉页脚、批注框文本图片引用这块容易出问题的是路径编码。输出 markdown 里的图片路径和实际落盘目录必须同源否则 markdown 渲染时全是红叉。我一般显式指定 --image-dir ./assets并确认该目录是相对输出根目录的而不是相对当前工作目录。4.2 JSON 链路文档树结构、置信度字段与后处理过滤JSON 输出面向程序消费核心价值是每个结构块都带坐标和置信度。文档树结构大致是 page → blocks → block每个 block 有 type、text、bbox、conf表格块还有 cells 数组保存行列结构。下面是一段实际输出的简化结构{ page: 3, width: 595.2, height: 841.8, order: 7, blocks: [ { type: heading, level: 2, text: 3.2 测试方法, bbox: [72, 96, 300, 118], conf: 0.94 }, { type: table, bbox: [72, 132, 523, 220], conf: 0.61, cells: [ [设备, 均值, 方差], [A, 12.4, 0.3], [B, 11.9, 0.8] ] } ] }bbox 是 [x0, y0, x1, y1]坐标原点在页面左上角单位是 PDF 点。conf 是模型置信度取值范围 0~1不是概率是模型校准后的得分所以不要拿它和准确率直接划等号。生产环境里我一般按 conf 分三档低于 0.5 的块直接丢弃只把坐标写进日志备查0.5~0.7 之间的块保留并打上 pending_review 标记入库后进入人工复核队列高于 0.7 的块直接进库。页面带有旋转属性时JSON 里的 bbox 坐标和页面图像不一定对得上。解决办法是在解析前统一开启 --auto-rotate或者在后处理里读取页面 rotation 字段做坐标变换二选一不能两个都做否则坐标会二次偏移。4.3 阈值调参置信度、字号比例与页面异常检测的配合阈值调参是这项工具最玄学的部分但我把它拆成两个独立维度后就清晰了区域模型阈值和表格模型阈值不能混用。区域模型阈值控制标题、正文、图片这些块的分类。文本块本身置信度普遍不高0.3~0.5 之间都算正常低于 0.3 才需要考虑是版面过脏还是模型没见过的版式。表格模型阈值控制表格结构识别的输出低于阈值时整块退化为文本流表格就废了。这个阈值我建议从 0.6 起步每 0.05 一档往下调观察失败样本的置信度分布再确定最终值。低质量扫描件、旋转页面、水印覆盖等输入问题都会拉低置信度。遇到大范围低置信度时不要硬调阈值先检查是不是 OCR 语言包没覆盖、页面方向没校正或者原文档本身是特殊双栏版式模型没见过。这里给一个快速检查方法从输出 JSON 中导出所有低置信度块的 bbox叠加在原页面图像上散点看一遍。如果低置信度块集中分布在页边距和水印位置说明模型没理解页眉页脚考虑开 strip_annotation 或过滤如果集中分布在正文区域说明版面检测本身有问题需要检查输入图像质量。5. 避坑与排查格式还原、OCR 兜底与并行批量的七个现场这章记录的都是我在批量跑文档时踩过的坑按“现象 → 原因 → 解决”整理遇到类似问题可以直接照方抓药。说是七个现场其实背后是同一类问题的不同表现把输出当成“抽取结果”而不是“模型推理结果”。5.1 扫描版 PDF 直出空文本日志里没有一条 OCR 记录现象命令跑完Markdown 文件存在里面是空的只有几个空标题。原因扫描版 PDF 没有文本层工具默认走原生文本层提取OCR 开关没有打开导致整页零输出。解决确认文档来源是扫描件后把 enable_ocr 置为 True并核对 ocr_lang 是否包含页面实际语言。中英混排扫描件只配 “en” 会导致中文部分识别成乱码配 “chen” 才能在速度和准确率上平衡。5.2 双栏论文转出后上下段穿插阅读顺序像洗牌现象左右两栏的内容交替出现在输出中一段在左栏底部下一段跳到右栏顶部顺序完全错乱。原因阅读顺序模型在多栏场景下依赖版面检测框先给出版心区域页眉页脚文字若被分进正文区域也会干扰排序。解决开启阅读顺序模型并在命令行用 --content-area 明确主体区域边界或者用 JSON 输出里的 bbox 过滤掉页眉页脚块后再排序。5.3 表格跨行跨列丢失单元格内容错位现象Markdown 表格行列数不对合并单元格内容被塞到另一个格子里。原因表格结构识别模型输出的是单元格坐标和合并关系当置信度低于阈值时退化为文本流文本流没有行列语义内容必然错位。解决先把 table_confidence 调到 0.6 以上重跑若效果还不行保留 JSON 输出里的 conf 字段对低置信度表格单独做后处理——用 bbox 的 y 坐标聚类还原行结构再用 x 坐标排序恢复列顺序。5.4 批量跑 800 页文档内存持续上涨进程被杀现象workers4 跑批跑到第三份文档时内存占用超过 8GB进程被系统杀掉。原因batch_pages 被设置成 64每批页面图像全部缓存在内存里栅格化中间张量没有及时释放。解决维持默认 batch_pages8或按页面像素密度调整流式按页回调模式下回调函数里不要持有页面图像的引用处理完就释放。5.5 Markdown 里图片引用存在但资源目录里没有对应文件现象输出 markdown 中有但 out/assets 下找不到文件。原因图片目录拼接用的是相对路径而输出目录是通过 -o 参数指定的绝对路径两者不一致资源文件落到了别处。解决显式指定 --image-dir 为输出目录下的 ASCII 路径如 ./assets并在批量跑批后检查该目录文件数与 markdown 中图片引用数是否一致。5.6 同一个页面内容出现两次另一页内容消失现象输出文档里第 3 页内容出现两次第 4 页内容找不到。原因PDF 页面对象带 /Rotate 旋转属性工具没做旋转校正时栅格化图像和文本坐标错位同一页面被重复解析。解决开启 --auto-rotate 参数或者在 PDF 预处理阶段统一旋转后转正再喂给解析器。5.7 并行跑批日志互相覆盖排查问题时没有现场可查现象workers4 跑批所有进程的 warn 日志混在一个文件里无法定位是哪份文档出了问题。原因日志句柄被多个进程共享写入时交错。解决为每个进程单独指定日志文件或者在 shell 里按文件名分组重定向更稳妥的是在 API 回调里把 page_id 和 file_id 一并写进日志行保证单行日志自带上下文。6. 进阶把解析质量跑成可量化的回归验收脚本解析工具的麻烦在于每次换版本、换模型权重、改阈值参数输出都可能变化。靠肉眼抽查几十页文档根本发现不了标题覆盖率从 90% 掉到 60%。我后来强迫自己养成了一个习惯维护一个固定的样本集和验收脚本任何改动都先跑一遍回归再决定要不要上生产批量。验收脚本的核心指标是“有效页率”和“平均块数”。有效页率统计有多少页产出了非页眉页脚的有效内容平均块数反映版面还原的完整度。下面是我常用的简化版本import glob import json from pdf_struct_engine import PDFParser parser PDFParser( devicecpu, enable_ocrTrue, ocr_langchen, table_confidence0.6, ) sample_files glob.glob(./samples/**/*.pdf, recursiveTrue) empty_pages 0 total_blocks 0 valid_pages 0 for pdf in sample_files: result parser.parse(pdf, output_formatjson) for page in result[pages]: # 页眉页脚和批注框不计入有效内容只统计正文块 blocks [ b for b in page[blocks] if b[type] not in {header, footer, annotation} ] if not blocks: empty_pages 1 continue valid_pages 1 total_blocks len(blocks) print(f样本文件数: {len(sample_files)}) print(f有效页数: {valid_pages}) print(f空内容页: {empty_pages}) print(f平均块数/有效页: {total_blocks / max(valid_pages, 1):.1f})这个脚本的过滤逻辑很关键——把页眉页脚和批注框从统计里剔除因为这些块几乎每页都有不剔除会掩盖正文缺失的问题。空内容页占比一旦超过 1.5%就说明这批样本里有模型没见过版式或者是 OCR 语言包没覆盖不能直接上批量。另一个指标是标题覆盖率。做法是把 PDF 自带的书签目录解析成标题列表再与 JSON 输出里的 heading 块做匹配。两者数量一对比覆盖率一目了然。我见过一次因为模型权重更新标题检测阈值整体偏移目录有 48 条只检出 40 条覆盖率从 92% 掉到 83%——这种变化肉眼根本看不出来。后来我在脚本里把这组数字也加进回归报告每次改动都留档对比。样本集怎么选也值得说一句不要只放几个标准排版文档要把你生产环境里出现过的每一类版式放 5~10 个进去。扫描版、双栏、表格密集、公式密集、带页眉页脚、旋转页面、多级标题每类都覆盖到。样本集越大回归报告越有参考价值。从那以后我每次换版本、换权重、调阈值都强制走一遍验收脚本哪怕改动再小也要跑。写过这个脚本之后我再也没在批量解析的中途大规模返过工。如果你手上也有这类批量解析需求值得把仓库里的脚本和样本配置拿下来先跑一遍验证通过再放量。希望这个习惯也能帮到你。本文还有配套的精品资源点击获取