PDF 抽出来的是文字,不是表格:19 页样本四处实证

发布时间:2026/10/11 7:57:49

PDF 抽出来的是文字,不是表格:19 页样本四处实证 版权与内容来源声明本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容均在附表 A 中标注来源引用官方原文保持原样不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式也不对任何收益结果作承诺。转载请注明出处。一、先看现场同一张表抽出来少了一行一份 19 页的排版论文用 pypdf 抽成纯文本之后表 2 里的 5 行数据只剩 4 行。不是有谁把它删了是其中两行粘成了一行。下面是抽取结果里原样的一行RAG-Tok. 17.3 22.2 40.1 41.5 72.5 89.5RAG-Seq. 14.7 21.4 40.8 44.289.5后面紧跟着RAG-Seq.中间连一个空格都没有。可它在原 PDF 里是两条独立的行前半段属于 RAG-Tok.后半段属于 RAG-Seq.。抽成文本后这两条行被拼进了同一行。这件事对下游的影响是具体的。你把这个 PDF 切块入库然后问「RAG-Seq. 的分数是多少」。检索会命中这一段——因为RAG-Seq.这串字符确实出现在这里但这一块里同时躺着 RAG-Tok. 的数字。模型看到的是一串混在一起的数很容易把前一组当成后一组。抽出来的不是表格是摆得像表格的一段文字结构在抽取那一步就已经丢了。1.1 为什么少一行比少几个字更危险如果抽取漏掉几个字肉眼能发现搜一下关键词空空如也你就知道要换方法了。但行边界的丢失是静默的——每一个数字都在每一个字母都对只有哪些数字属于同一行这个信息没了。而下游的 RAG 恰恰靠这个信息做判断。切分器按换行、按标点切它看不出89.5RAG-Seq.里的89.5属于上一行检索器按字符命中它只关心RAG-Seq.这几个字符在不在生成器拿到这个块也没有额外信息去分行。所以这类错误不报错、不告警只是把一份结构已经错位的输入安静地喂给模型。1.2 四处丢失一次列清把这次 19 页样本的抽取结果翻一遍能看到不止一处。它们分属四种我并列在一张表里证据抽取里看到的对下游 RAG 的影响连字 fi / flfine-tuning搜不到fine-tuning才搜得到关键词检索直接漏召行内公式上下标、间隔被压平成同一行公式类问答拿不到正确的符号与层级表格行边界两行数据粘成一行数字归属错位取到别的行的值列间空格列与列之间的空格丢失切分粒度失真一块里挤进多列内容这四处不是同一个原因造成的能不能补救也不是同一个答案。第二章先把它们跑成一段可复现的代码第三章再说哪几处救得回来。二、把这件事跑成十几行代码上面那四处能在本地用一小段脚本复现。真正做抽取的只有两三行——PdfReader读入再对同一页调两次extract_text其余行数都是围绕四处证据做的检查与打印。2.1 最小复现四个检查拼成一段环境如下样本是一份公开论文的排版 PDF。 实测环境Python 3.13.12 / macOS / pypdf 6.20.0# -*- coding: utf-8 -*-实验01PDF 文本抽取到底丢了什么pypdf样本 一篇真实排版论文importrefrompypdfimportPdfReader SRCwork/exp1011/sample_layout.pdfrPdfReader(SRC)print(样本%d 页Producer%s%(len(r.pages),r.metadata.get(/Producer)))print()p0r.pages[0]plainp0.extract_text()layoutp0.extract_text(extraction_modelayout)print(第 1 页默认模式 %d 字符layout 模式 %d 字符%(len(plain),len(layout)))print()print( 证据 1连字fi / fl在纯文本里是另一个码位 )print( fine-tuning 能搜到吗,fine-tuninginplain)print( fine-tuning 能搜到吗,fine-tuninginplain)print()print( 证据 2行内公式被压平上下标、间隔全丢)t4r.pages[3].extract_text()mre.search(rp′θ\(yi\|x,t4)segt4[m.start()-150:m.start()260]ifmelset4[:300]print( 抽取结果片段)print( seg.replace(\n, / ))print()print( 证据 3表格行边界丢失两行数据粘成一行)t6r.pages[5].extract_text()merged[lnforlnint6.split(\n)ifre.match(r^[\w\-\.\ ]\[\d\].*\d\.\d,ln)]forlninmerged[:6]:print( ln)stuck[lnforlnint6.split(\n)ifre.search(r\d\.\dRAG,ln)orre.search(r\d\.\d[A-Z]{3},ln)]print( 行被粘连的样本)forlninstuck[:3]:print( ln)print()print( 证据 4同一张表格layout 模式只是补了空格语义仍未恢复 )p5r.pages[5]lpp5.extract_text(extraction_modelayout)m3re.search(rRAG-Token,lp)ifm3:print( layout 模式截取 6 行)forlninlp[m3.start()-20:m3.start()260].split(\n)[:6]:print( repr(ln))print( 默认模式同一区域截取 6 行)m4re.search(rRAG-Token,t6)forlnint6[m4.start()-20:m4.start()260].split(\n)[:6]:print( repr(ln))print()cnt_default0cnt_layout0forpginr.pages:apg.extract_text()orbpg.extract_text(extraction_modelayout)orcnt_defaultlen(a)cnt_layoutlen(b)print(全文合计默认模式 %d 字符layout 模式 %d 字符相差 %d%(cnt_default,cnt_layout,cnt_layout-cnt_default))原样输出未做任何美化含 pypdf 自己打印的两行旋转告警Rotated text discovered. Output will be incomplete. Rotated text discovered. Output will be incomplete. 样本19 页ProducerpdfTeX-1.40.21 第 1 页默认模式 2898 字符layout 模式 3281 字符 证据 1连字fi / fl在纯文本里是另一个码位 fine-tuning 能搜到吗 False fine-tuning 能搜到吗 True 证据 2行内公式被压平上下标、间隔全丢 抽取结果片段 o approximatearg maxyp(y|x). / RAG-Token The RAG-Token model can be seen as a standard, autoregressive seq2seq genera- / tor with transition probability: p′θ(yi|x,y 1:i−1) ∑z∈top-k(p(·|x))pη(zi|x)pθ(yi|x,zi,y 1:i−1) To / decode, we can plugp′θ(yi|x,y 1:i−1) into a standard beam decoder. / RAG-Sequence For RAG-Sequence, the likelihoodp(y|x) does not break into a conventional per- / token likelihood, hence we cannot 证据 3表格行边界丢失两行数据粘成一行 T5-11B [52] 34.5 - /50.1 37.4 - T5-11BSSM[52] 36.6 - /60.5 44.7 - REALM [20] 40.4 - / - 40.7 46.8 DPR [26] 41.5 57.9/ - 41.1 50.6 行被粘连的样本 RAG-Tok. 17.3 22.2 40.1 41.5 72.5 89.5RAG-Seq. 14.7 21.4 40.8 44.2 证据 4同一张表格layout 模式只是补了空格语义仍未恢复 layout 模式截取 6 行 Table 2 shows that RAG-Token performs better than RAG-Sequence on Jeopardy question generation, with both models outperforming BART on Q-BLEU-1. 4 shows human evaluation results, over 452 pairs of generations from BA 默认模式同一区域截取 6 行 5 57.9/ - 41.1 50.6 RAG-Token 44.1 55.2/66.1 45.5 50.0 RAG-Seq. 44.5 56.8/68.0 45.2 52.2 Table 2: Generation and classification Test Scores. MS-MARCO SotA is [4], FEVER-3 is [68] and FEVER-2 is [ 57] *Uses gold context/evidence. 全文合计默认模式 69044 字符layout 模式 76797 字符相差 77532.2 输出里最该看的三行第一行最重要的不是数字是那句ProducerpdfTeX-1.40.21。它说明这份样本是 LaTeX 排出来的原生数字文档不是扫描件——连字体和坐标都由程序精确摆放的文档抽出来都会丢结构。指望换成扫描件会更好方向就错了。第二行是证据 1 的两个布尔值。同一个fine-tuning写成平时的拼法搜不到写成带连字的形式才搜得到。这不是你检索词写错了是抽取出来的文本里那个fi根本不是一个f加一个i。第三行是证据 3 里那行粘连。它和第一章开头贴的是同一行说明第一章那次失败不是偶发——这段脚本每跑一次都会稳定地把它抽出来。顺带看一眼输出顶上的两行Rotated text discovered. Output will be incomplete.。这不是我下的判断是 pypdf 自己打的只要文档里有旋转的文字它就明说自己的输出不完整。抽取器自己在承认会丢内容。2.3 一个反直觉读数字符更多不等于结构更好注意最后一行默认模式 69044 字符layout 模式 76797 字符多了 7753。直觉上抽出来的东西更多应该更完整但这里多出来的主要是列与列之间的空格不是被找回的结构。证据 4 的对照已经把话说清楚了换到 layout 模式后同一段区域的文本里Table 2 shows that...那句话被拉开成三行、塞进大量空格可第一章那行粘连在默认模式里依旧粘着。字符数是一个数量指标结构是一个关系指标两者不能互相代替。下一步就把这条缝说透四处丢失里到底哪几处能靠参数救回来。三、四处丢失里只有两处能靠参数救回来先给结论连字码位和列间空格这两处能救前者靠一次 Unicode 规范化后者靠extraction_modelayout行内公式和表格行边界这两处救不回来——它们不是抽取参数能解决的问题。3.1 救点一连字码位用 Unicode 规范化收回连字fi是排版为了好看把f和i拼成的一个独立字符它在文本里占的是一个单独的位置不是两个字母拼在一起。所以只要把这段文本做一次 NFKC 规范化它就会退回成普通的fi原关键词立刻可搜。 实测环境Python 3.13.12 / macOS / pypdf 6.20.0importunicodedatafrompypdfimportPdfReader plainPdfReader(work/exp1011/sample_layout.pdf).pages[0].extract_text()fixedunicodedata.normalize(NFKC,plain)print(规范化前 原始关键词能搜到吗,fine-tuninginplain)print(规范化前 连字形式能搜到吗,fine-tuninginplain)print(规范化后 原始关键词能搜到吗,fine-tuninginfixed)print(规范化后 连字形式还在吗,fine-tuninginfixed)规范化前 原始关键词能搜到吗 False 规范化前 连字形式能搜到吗 True 规范化后 原始关键词能搜到吗 True 规范化后 连字形式还在吗 False看第三、四行的反转规范化之后原来的fine-tuning能搜到了连字形式反而消失了。这一步不依赖任何 PDF 库凡是抽取出来的文本入库前都可以先过一遍规范化——成本几乎为零却能堵住一整类漏召。3.2 救点二列间空格靠 layout 模式收回一部分第二处能救的是列间距。把默认抽取结果摊进下面这张表对照取数口径写清楚 实测口径样本为 19 页真实排版论文ProducerpdfTeX-1.40.21/ 工具 pypdf 6.20.0 / 取数日 2026-10-11指标默认模式layout 模式差额多出来的主要是什么第 1 页字符数28983281383列间空格全文 19 页字符数69044767977753列间空格表格两行是否粘连粘连89.5RAG-Seq.仍然无法判定行的归属未解决——口径一句话样本是同一份 19 页 PDF两次抽取只换extraction_mode这一个参数其余不变layout模式交给我们的是一份按固定宽度贴近版面的文本多出来的字符基本是它补的列间空格。这张表最该记住的是第三行。空格补回来了看起来更像表格了但两行数据的归属依旧没有恢复——89.5到底属于哪一行layout 模式同样不告诉你。补空格能改善观感改善不了结构。3.3 参数救不回来的那两处把四处损失的补救路径摊开看结论一目了然丢失能否靠参数或一步处理救回手段为什么连字码位能unicodedata.normalize(NFKC, ...)属于码位映射与排版无关列间空格能部分extraction_modelayout按横坐标把空格补回来行内公式不能——上下标的层级关系在文本流里已消失表格行边界不能需换结构化解析抽取器不判断哪几个字符属于同一行行内公式丢的是层级p′θ(yi|x,y1:i−1)抽出来是一串平铺的字符谁在底下、谁是指标抽取器不会告诉你。表格行边界丢的是分组它抽对了每一个数字但没有告诉你这些数字怎么分行。这两处都不是换个参数能修好的想拿回来得在抽取之外做结构判断。排除掉这两处之后剩下的问题就很集中了为什么一个看得见每一个字的抽取器偏偏看不见结构四、抽取器为什么看得见字看不见结构因为 PDF 文件里本来就没有表格这个东西。它存的是某个字形被放在某个坐标上不是这是第 3 行第 2 列。抽取器把带坐标的文字按阅读顺序串成一段文本流结构是它自己猜的猜不准的部分就丢掉了。4.1 PDF 里没有表格只有坐标上的字一份 PDF 描述的是排版结果一段文字就是一组字形 位置 字号。至于这些字排列得像不像一张表文件本身不存这个判断。所谓表格只是很多字被摆得很整齐让人觉得它是表格在文件的层面它和旁边一段普通段落没有本质区别。所以抽取器拿到的原始信息是一堆带着坐标的文字。想还原出行和列它只能靠位置去反推——而反推是会失败的。4.2 抽取器的目标是文本流不是结构pypdf 默认的抽取是按内容流的顺序把字符拼起来尽量给你一段连续的文本。在这个目标下行和列并不是它的产出物。证据 3 里两行数据粘在一起用文本流的视角看很自然它们本来就是连续的字符中间没有任何东西提示这里该断开。一旦文档里有旋转、多栏或公式这种靠位置猜就更容易失败。第二章那两行告警就是证据——Rotated text discovered. Output will be incomplete.pypdf 在遇到旋转文字时直接声明自己的输出不完整。4.3 官方文档也只承诺贴近版面不承诺还原结构这一点在 pypdf 官方《Extract Text from a PDF》里写得很直接。extraction_modelayout的定义是「a fixed width format that closely adheres to the rendered layout in the source pdf」——它给的是一份尽量贴近原始版面的固定宽度文本承诺的是版面的样子不是结构。同一页文档还介绍了按方向抽取的参数extract_text(0)按默认方向取extract_text((0, 90))按指定方向取。这恰好解释了两件事默认抽取会因为文字方向而丢内容想要结构得在抽取之外再补一层判断。把这一章收到一句可执行的判断上只要你的抽取调用里没有专门做结构还原的那一层你拿到的就只是文字流结构一直是缺的。那到底什么时候必须补这一层、什么时候可以直接用最后一章给三条判据。AI 大模型知识库在线可查上面这套「PDF 里没有表格、只有坐标上的字」的判断在资料包的在线知识库里配了 Agent Skills、Claude Skills 等专题方便你对照资料抽取与切分里常见的坑。放在资料包里扫码即可获取五、什么时候该停手三条判据不要一看到抽出来有点乱就上重型结构化解析。先用两条能当场量出来的判据确认真丢了结构再用第三条判据看丢失落在不在你真正要检索的字段上最后才决定停手、换解析还是转人工。5.1 判据一量行是否粘连做法很简单对抽取结果按行扫一遍找数字紧贴字母的行——比如用正则\d\.\d[A-Z]去匹配89.5R...这种形态命中就说明行边界已经丢了。落到一个可操作的版本随机抽 20 行数据行数一数有多少行出现上一行结尾的数字直接贴上下一行开头的标签。命中越多说明你的表格越依赖行结构越不能就这么入库。5.2 判据二量连字是否可搜拿你真正的检索关键词原形直接在抽取结果里搜一遍搜不到时把关键词里的连字形式再搜一遍。两次结果不一致就说明码位没归一检索一定会漏。修法已经在前一章给过入库前先做一次 NFKC 规范化再做检索。5.3 判据三看丢失落在不在关键字段上前两条判据回答有没有丢结构第三条回答要不要管。如果被粘连、被压平的是你真正会检索和计算的字段——分数、金额、型号、日期那就必须处理如果只是正文散文里偶尔连了一行调一调切分粒度就够不必为它上重型方案。判据怎么量通过的样子不通过时一、行是否粘连正则扫数字紧贴字母的行未见粘连表结构不可信数字归属存疑二、连字是否可搜关键词原形与连字形式各搜一次两次结果一致码位未归一先做 NFKC三、丢失是否落在关键字段看被粘连或被压平的字段是否参与检索只影响正文必须换结构化解析或转人工停手条件也给到可执行的粒度判据一、判据二都通过说明现成抽取器够用直接去做切分任一不通过、且判据三指向关键字段就停手换布局解析或表格识别必要时转人工校对任一不通过、但只影响正文先补一次 NFKC 规范化加上调整切分粒度即可别急着上重型方案。核心判断就一句先量行是否粘连 连字是否可搜再决定要不要投入结构化解析抽取字符数变多并不等于结构保住了。大模型学习路线图这一章这套先量再决定的判据我在资料包里配了一份从零基础到能自己动手做 Agent 的学习路线按阶段说明每一步该学什么、哪些可以先跳过。放在资料包里扫码即可获取附表 A本文引用事实与出处对照表事实出处文档名 发布方 链接本文位置extraction_modelayout的定义为「a fixed width format that closely adheres to the rendered layout in the source pdf」pypdf 官方《Extract Text from a PDF》pypdf 项目https://pypdf.readthedocs.io/en/stable/user/extract-text.html第三章 3.2、第四章 4.3extract_text(0)与extract_text((0, 90))为按方向抽取的参数同上pypdf 官方《Extract Text from a PDF》第四章 4.3样本为 19 页真实排版论文ProducerpdfTeX-1.40.21本文实测脚本work/exp1011/exp01_pdf_extract.pypypdf 6.20.0第一章、第二章 2.2抽取过程打印Rotated text discovered. Output will be incomplete.表明旋转文字会导致输出不完整本文实测原样输出第二章 2.1、2.2、第四章 4.2全文默认模式 69044 字符、layout 模式 76797 字符相差 7753本文实测第二章 2.3、第三章 3.2连字与行粘连的具体读数fine-tuning搜不到、89.5RAG-Seq.粘连本文实测第一章、第三章 3.1、3.2写在最后这篇用到的资料写这篇文章时把相关的官方文档和源码又翻了一遍顺手也整理了几份配套的东西大模型学习路线图从零基础到能自己动手做 Agent按阶段说明每一步该学什么、哪些可以先跳过《LangChain LangGraph MCP 智能体开发实战》视频课7 个模块从私有化部署、EmbeddingRAG 到 MCPAgent 全流程AI 大模型知识库在线可查Agent Skills 从入门到落地、Claude Skills 完全指南等专题按目录浏览即可640 套 AI 大模型行业报告 经典 PDF 书籍看行业落地案例和别人怎么做的时候用得上大模型零基础到精通教学视频跟着敲一遍比只读文档快得多资料是我自己整理的放在下面这个码上扫码即可获取添加时备注「AI」优先通过。资料按「先路线、再动手、最后查漏」的顺序整理好了建议先看学习路线那一份照着它挑一条适合自己当前基础的路径再往下看。
延伸阅读

更多相关文章

2026/10/11 7:57:49

基于SpringBoot+Vue的图书管理系统毕设开发全流程解析

毕业设计做图书管理系统,几乎是计算机专业的“保留节目”了。每年一到三四月份,总有人问我:想做个管理系统当毕设,Java后端配Vue前端,还要能在答辩现场跑起来,到底怎么从零搞出来?说实话&#x…

2026/10/11 7:52:48

持续交付与持续部署:一字之差,天壤之别

持续交付和持续部署,这两个词放在一起,大概是把人绕晕最多的技术概念之一。每年我面试候选人,问起“你们团队的CD做到什么程度”,十个人里有六个会说自己上了持续部署,再追问细节,发现所谓自动部署其实只是…

2026/10/11 7:52:48

【单片机毕业设计】基于单片机的自行车骑行定位与运动数据监测装置设计 基于物联网的骑行速度里程与定位追踪监测系统设计(030205)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

2026/10/11 9:02:52

镀锌桥架采购常见问题解答 新明电气 大厂直供 降低采购成本

镀锌桥架作为电缆敷设体系中的基础支撑构件,凭借热镀锌工艺带来的防锈防腐能力与较高的经济性,长期占据工业与基建项目线缆配套市场的重要位置。然而在实际采购过程中,不少项目采购人员由于对产品工艺、规格体系、供货周期缺乏系统了解&#…

2026/10/11 9:02:52

自研GEMM内核DeepGEMM:从性能剖析到算子级调优实战

我在做推理优化的时候,用性能分析工具看了下整个计算图,发现一个很扎心的事实:一个普通的矩阵乘法算子,就能吃掉单次迭代接近四成的时间。当时第一反应是换参数、调库、换格式,折腾一圈之后发现,通用数学库…

2026/10/11 9:02:52

DeepGEMM:GPU矩阵乘法算子级极致优化实战指南

1. 项目概述:DeepGEMM不是新模型,而是GPU计算底层的“肌肉强化术”如果你最近在高性能计算、AI训练加速或CUDA开发相关的技术社区里刷到“DeepGEMM”这个词,第一反应可能是——又一个大模型?还是某家新出的推理框架?其…

2026/10/11 9:02:52

基于YOLOv8的植物健康状态二分类系统实战

1. 项目概述:为什么一个“健康/患病”二分类检测系统值得花两周时间重做三遍?去年在某高校实验室带一个植物图像分析的模拟项目X时,我第一次接到需求:“用YOLOv8做个植物病害识别”。当时想得很简单——网上搜个预训练权重、换掉最…

2026/10/11 8:57:52

什么是 SRC?手把手教新手在 SRC 提交漏洞,拿第一个证书 / 奖金

什么是 SRC?手把手教新手在 SRC 提交漏洞,拿第一个证书 / 奖金 免责声明:本文仅用于网络安全知识科普、白帽漏洞挖掘合规学习。**所有漏洞挖掘操作,仅能在厂商 SRC 明确授权范围内开展测试,严禁对未授权网站、系统、AP…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑