ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PDF转Markdown换行混乱?一文搞定自动合并与后处理

PDF转Markdown换行混乱?一文搞定自动合并与后处理 PDF 转 Markdown 这事儿看着简单做起来最烦人的不是表格乱、公式糊而是转出来的文本满屏都是换行符。每一行后面都挂着个软回车段落和段落之间被拆得七零八落想引用一段文字还得一行一行手工接起来遇到上百页的 PDF手都能删到抽筋。这篇文章就专门解决这一个问题怎么让 PDF 转 Markdown 之后不需要手工删换行从根源上把“换行干扰”处理干净。我会按实际处理的顺序来写先讲清楚换行为什么会出现再按 PDF 类型给工具选型然后给一套自动化后处理脚本最后用三个完整案例把整条链路串起来。适合正在折腾论文转笔记、技术文档整理、或者批量把电子书转成 Markdown 的人参考。1. 先搞清楚PDF里的“换行”到底是谁产生的1.1 两种换行只有一种需要删PDF 转出来之后常见的换行其实是两种东西混在一起一种是视觉换行另一种是段落换行。视觉换行是 PDF 排版引擎根据页面宽度自动折行产生的它只表示“这一行放不下换个位置继续显示”跟文字内容本身没有关系段落换行是真的表示一段内容的结束对应文档里的一个段落分隔。绝大多数 PDF 转 Markdown 的工具默认会把这两种换行都转成\n。于是你看到的结果就是一个段落被拆成四五行每行后面都有一个换行符。如果这段文字恰好还是英文论文行尾还带连字符断词情况就更糟——一个单词被拆成两半插着换行符复制出来全是断的。手工删这种换行本质上是在帮工具补做“语义判断”区分哪些换行是排版造成的哪些是段落结束。这件事完全可以交给规则和脚本去干。1.2 为什么不同工具转出来的结果差别那么大关于这一点先理解一个基础概念PDF 本质上是一种“版面描述格式”不是“流式文档格式”。Word 保存的是文字的阅读顺序和段落结构而 PDF 保存的是每个字符在页面上的坐标。也就是说PDF 自己并不知道哪几行字构成一个段落它只知道自己应该把某些字符画在某个位置。这就是为什么不同工具的表现差异很大。pdfplumber 这类库是沿着坐标从左到右、从上到下提取字符碰到行尾就加个换行Pandoc 对部分 PDF 会尝试做段落重组效果取决于 PDF 里的字体和编码是否规范MinerU 这类专业解析工具会先做版面分析识别标题、段落、表格、公式再按逻辑树输出换行。工具的能力差异本质上是“有没有做版面语义分析”的差异。另外还要看 PDF 是哪种类型。数字生成的 PDF用 Word、LaTeX、浏览器打印出来的内部有文本层字符信息完整扫描版的 PDF 只有图片没有文本层任何直接提取的方式都拿不到文字必须先走 OCR。处理换行问题之前先认清 PDF 类型能少走一半弯路。2. 工具选型按PDF类型选方案换行问题先解决一半2.1 有文本层的PDFPandoc、pdftotext、pdfplumber怎么选如果 PDF 有文本层常用的免费路线有三条Pandoc、Poppler 的 pdftotext、Python 的 pdfplumber 或 PyMuPDF。三者定位不同。Pandoc 的强项在于它能把一部分结构信息转出来比如标题、加粗、部分表格而且输出的 Markdown 语法是标准的适合直接进笔记库。但它对中文的段落合并处理不稳定碰到中英文混排和复杂样式时效果会飘。pdftotext 是命令行的轻量工具速度非常快适合批量粗提取但它基本不做排版语义理解输出就是“行行换行”的原始文本后处理工作量比较大。pdfplumber 适合需要精确控制提取逻辑的情况它把每个字符的坐标、字体、大小都暴露给你等于把“判断哪里该换行”的主动权交到了你手里但代价是你得自己写规则。我的建议是不追求复杂排版时先用 pdftotext 快速看一页输出如果文字完整、只是换行多走脚本合并如果 PDF 里有很多标题层级需要保留用 Pandoc如果以后要长期处理大量不同类型的 PDF直接学会用 pdfplumber 或 PyMuPDF 做定制化提取一劳永逸。2.2 扫描版PDFOCR才是正确路线扫描件没有文本层任何“提取”操作都拿不到字。有人会把扫描 PDF 硬塞给 Pandoc结果输出一片空白这不是工具的锅是数据源的问题。正确的做法是走 OCR 管线。本地免费的 OCR 工具里Tesseract 是老牌选择支持中文需要额外下载语言包识别精度一般胜在免费离线、部署简单PaddleOCR 对中文的识别效果明显更好支持版面分析也能输出每个文本框的坐标适合自己写后处理逻辑。云服务比如 Mathpix公式识别能力突出但收费且涉及数据隐私问题不适合敏感文档。现在也有不少专门做“文档解析”的开源项目底层基于深度模型做版面检测加 OCR对扫描 PDF 的支持已经比较成熟后续案例部分会提到。OCR 输出通常比文本提取更乱它逐行识别图片上的文字遇到图片里的折行就会输出换行还会顺手把页眉页脚、图片注释、表格里的数字都当成正文文本混进来。所以 OCR 之后的换行清理只是第一步通常还需要配合版面后处理把标题、正文、表格分清楚。简单场景用正则脚本能解决复杂场景建议直接选用带版面分析的解析工具。2.3 双栏、公式、复杂表格直接上专业级方案排版简单的 PDF上面几条路线都能对付。真正要命的是双栏论文、带大量数学公式和复杂表格的文档。双栏 PDF 按坐标提取时阅读顺序会从左栏第一行跳到右栏第一行再跳回左栏第二行输出的文字完全没法读。公式在 PDF 里是一堆特殊字体符号直接提取只会得到乱码。表格则受限于单元格框线是否完整框线不完整时提取结果就是一堆数字串。这类场景就别在基础工具上硬扛了专业级的文档解析工具更合适。它们通常做三件事版面分析把页面切成不同区域模型识别标题、正文、表格、公式最后按阅读顺序拼接成 Markdown 结构。输出的换行是经过版面语义判断的段落内部不会残留视觉换行代码块内部的换行反而保留得很准。遇到复杂排版时用这类工具能直接省掉大量后处理时间。3. 自动化后处理把“删换行”从手工变成脚本3.1 三条判断规则决定换行删还是不删不管用什么工具提取最终的文本可能还是带着一堆换行。这时候就需要一个后处理程序来自动决定哪些换行该删、哪些该留。核心规则其实可以归结成三条第一如果某个换行符的前后两侧都属于同一段正文那么这个换行就该删。判断“同一段正文”的常见信号是前一行末尾不是段落结束标点句号、问号、感叹号、冒号下一行开头是正常的小写字母或汉字开头而不是大写标题、编号、列表符。第二如果换行符出现在空行旁边或者它本身是双换行中的一部分那是段落边界必须保留。第三如果行的开头是 Markdown 结构特征比如数字序号、圆点列表符、#标题前缀、|表格分隔符、反引号代码块标记那么这个换行必须保留因为它分隔的是两个不同的语义单元。把这三条规则转换成正则和条件分支就是一套够用的自动换行清理器。英文还要多考虑一件事行尾如果是连字符通常是断词合并时要删掉连字符并把两侧单词拼在一起比如com-加puter要合并为computer。但要小心如果一行是- item这样的列表项这行不能和上一行合并因为列表结构优先级更高。3.2 一个够用的Python脚本保护段落合并断行基于常见实践我提供一个可以直接拿来改的 Python 脚本核心部分。它的思路很简单先把文本按行拆开遇到空行就切一个段落遇到代码块、表格行、列表项就先保护起来其余正文行按规则合并。整体逻辑就是“不碰结构行只处理正文块中的软换行”。import re def is_list_item(line: str) - bool: # 识别markdown列表项、有序列表和任务列表 return bool(re.match(r^\s*([-*]|\d[.、)]|\[[ xX]\])\s, line)) def is_table_or_code(line: str) - bool: return line.strip().startswith(|) or line.strip().startswith((, ~~~)) def is_heading(line: str) - bool: return line.lstrip().startswith(#) def should_keep_break(prev: str, curr: str) - bool: # 上一行是段落结束标点则保留换行 if re.search(r[。!?;:]$, prev.strip()): return True # 当前行明显是结构开头保留换行 if (curr.strip() and curr.strip()[0].isupper()) or is_list_item(curr) or is_heading(curr): return True # 当前行以中文标点开头比如引号、破折号位置特殊保守处理为保留 if curr.strip() and curr.strip()[0] in “‘【《: return True return False def merge_soft_breaks(text: str) - str: # 先把整个文本按空行切成段落块 blocks re.split(r\n\s*\n, text) new_blocks [] for block in blocks: lines block.split(\n) if len(lines) 1: new_blocks.append(block) continue merged [] for i, line in enumerate(lines): stripped line.rstrip() if not stripped: continue merged.append(stripped) # 对纯正文行做换行合并 final_lines [] for line in merged: if not final_lines: final_lines.append(line) continue prev final_lines[-1] # 结构行直接追加 if (is_table_or_code(line) or is_list_item(line) or is_heading(line)): final_lines.append(line) continue # 优先保留结构行上的断行 if is_table_or_code(prev) or is_list_item(prev) or is_heading(prev): final_lines.append(line) continue # 判断正文内部软换行 if should_keep_break(prev, line): final_lines.append(\n line) else: # 正文内部合并英文断词场景去掉连字符 if prev.endswith(-) and not is_list_item(prev) and not is_table_or_code(prev): final_lines[-1] prev[:-1] line else: final_lines[-1] prev line new_blocks.append(\n.join(final_lines)) return \n\n.join(new_blocks) if __name__ __main__: raw_text open(output_raw.txt, encodingutf-8).read() cleaned merge_soft_breaks(raw_text) open(output_clean.md, w, encodingutf-8).write(cleaned)这里有几个细节值得展开。should_keep_break里的第一条规则用到了段落结束标点这在中文里是可靠的因为中文句子结束一定会用句号问号感叹号英文同理。但要特别注意英文缩写比如etc.出现在行尾时如果后面还有下一行正文单纯看到句号就保留换行会把一个段落拆成两截所以更精细的做法是配合“下一行是否小写开头”来兜底。第二和第三条规则覆盖了标题和列表的常见情况优先级很高。整个后处理脚本是建立在一个前提上的原始提取器已经保留了空行作为段落分隔。如果工具输出完全没有空行需要先用pdftotext -layout加参数或者在提取时手动识别段落间距补充空行这一步不能省。3.3 中文场景的特殊处理标点、引号与全角字符中文 PDF 转 Markdown 后处理时换行合并本身很简单因为中文不像英文那样有空格去掉换行直接把前后拼接就行。麻烦的是两类问题一是段落首行的全角引号二是行尾和行首的标点挤压。中文排版规范里全角引号不能出现在行首顿号、逗号、句号不能出现在行首。从 PDF 提取时因为换行位置正好卡在标点附近会导致某些标点被顶到下一行开头。这时如果脚本往上合并会得到“他说”加上下一行开头的“你好”这种正常结果但如果不加处理就可能出现“他说”和“你好”被拆开。解决方案是在合并前先做一轮标点规整把行首的右引号、逗号、句号、顿号拼到上一行末尾同时删掉补进来的换行。在脚本里加一段预处理text re.sub(r\n([。、」』”’】]), r\1, text) text re.sub(r([「『“‘【])\n, r\1, text)第一行解决“行首是终止标点”的情况第二行解决“行尾是起始引号”的情况。这两行加在merge_soft_breaks之前执行可以明显减少中文段落里的残留断行。3.4 代码块和公式怎么不被误伤自动换行清理最大的风险其实是误伤代码块和公式。比如一段 Python 代码每一行逻辑都是独立的如果后处理脚本把代码块的换行合并了代码就变成一行巨长的字符串完全没法用。数学公式同理多行公式里的换行是格式的一部分删掉品面就塌了。所以处理顺序很重要先把代码块和公式“保护”起来再做换行合并最后恢复。具体做法是先用正则提取出所有被反引号包裹的代码块、被$$包裹的公式块存入一个临时列表并在原位置放一个占位符比如__CODE_BLOCK_0__。等正文的换行合并完成之后再把占位符替换回原始内容。这里有一个容易被忽略的细节占位符不能放在真实文本里容易被正文合并吞掉的位置比如普通单词连接处。建议用带独特前缀的占位符并且占位符所在的行要强制保留换行否则代码块边界会被合并掉。4. 三个实操案例从PDF到干净Markdown的完整管道4.1 案例一英文论文PDF文本层完整我拿一篇 arXiv 上的英文论文做了测试。这种 PDF 是 LaTeX 生成的文本层完整、字体清晰但段落内部依然有大量折行。操作分三步先用 PyMuPDF 快速提取全文参数上设置sortTrue让字符按阅读顺序排列输出到paper_raw.txt然后跑上面的后处理脚本。第一次跑完发现一个问题论文里的参考文献部分全是连续的段但有的行以[12]开头被正则当成了有序列表导致参考文献列表被拆得稀碎。解决办法是加一个规则——[数字]这种引用标记只有出现在行首时才触发列表保护而且后面必须跟空格或制表符。另外英文论文常见的断词连字符在合并时如果只做prev[:-1] line会漏掉一种情况断词符本身是-但前面的词被额外加了下划线或斜体标记比如*com-*接*puter*。针对这种情况我在合并前先用正则去掉行尾的-只保留单词根再拼接。最终那篇论文的正文段落完整标题层级也保留住了只有页眉页脚需要单独过滤。页眉页脚的过滤逻辑很简单在所有页面里重复出现的相同短文本行直接删除。4.2 案例二中文教材扫描版OCR管道手头有一本扫描版的中文教材没有文本层PDF 每页都是一张图。处理这类文件我的流程是先用工具把 PDF 按页渲染成图片然后调用 PaddleOCR 的版面分析能力得到识别文本、文本框坐标和版面信息接着按“从上到下、从左到右”排列最后套用换行合并脚本。这个流程里最关键的一步是版面分析和阅读顺序。教材通常是单栏排版但页面上会有很多图注、页眉、练习题编号。OCR 结果里每页可能输出四五十行文本很多是图注和页眉需要根据坐标过滤。我的经验是基于 y 坐标分组把 y 坐标相近的文本归为同一行再把 x 坐标排序页眉页脚通常位于页面顶部和底部固定区域直接按阈值剔除。OCR 识别出的段落行与行之间没有空行信息我会根据行间距来自动插入空行——行间距明显大于正文行距的那条边就是段落边界。这套流程跑下来中文教材的正文段落基本能完整还原。有一个反复出现的坑是OCR 会把“第 1 章”识别成“第1章”中间的空格丢了后处理时需要针对这类格式做一次规整。另一个更麻烦的问题是表格识别PaddleOCR 可以输出表格结构但和正文的衔接偶尔会乱我的做法是表格单独处理识别完生成 Markdown 表格放在正文中对应的位置其他部分的换行清理不受影响。4.3 案例三含表格与公式的技术手册技术手册这种 PDF排版密度高往往同时包含代码块、公式表格和双栏局部。我的建议是直接上专业级解析工具而不是从底层挨个字符抠。实测下来这类工具的价值不仅在于换行更在于它能输出相对干净的结构化 Markdown 块正文段落正常合并代码块保持原样公式输出 LaTeX表格生成标准 Markdown 表格。对工具输出的结果我依然会用后处理脚本做一次“兜底清理”。因为这个场景里的公式会以$...$和$$...$$形式出现为防止误删公式内部的换行我在脚本里先保护公式块。代码是这样的用re.findall(r\$\$.*?\$\$, text, flagsre.S)把所有公式块先摘出来替换成占位符再跑merge_soft_breaks跑完后恢复。表格行以|开头本身就命中结构保护逻辑不用额外处理但表格里单元格如果内容很长自动换行出现在单元格内部需要将这行合并成一行再放进表格否则 Markdown 表格会断掉。这一步我在后处理时做了特殊分支当一行以|开头且下一行也是|开头时直接把两行拼接。4.4 批量处理把单文件流程固化成管道案例三跑通之后我发现经常有批量处理多个 PDF 的需求于是把流程固化成了一个小工具链输入目录→检查文本层→分流派处理文本层走提取扫描版走 OCR→统一后处理→输出 Markdown。批量文本提取用 Python 的fitz库循环处理就行几十个文件也就几分钟。批量 OCR 则要控制并发太多并发容易把内存打爆按页分片处理是更稳妥的做法。批量处理还有一个容易被忽视的问题原始提取和 OCR 的中间文本最好都保留。因为我经常调后处理规则比如把某个正则改得更严谨重新跑一遍后处理就完事了不用重新 OCR。保留中间产物等于给自己的工作流留了缓存省掉大量重复计算。5. 常见问题排查与避坑记录5.1 问题速查表我把实际操作中遇到的高频问题整理成了一张速查表方便定位。症状常见原因解决办法英文单词被拆成两半中间多了换行英文断词连字符旧版提取器保留-合并时去掉行尾-把两段拼接中文段落里行首出现逗号句号PDF 字符坐标提取顺序不稳定加标点归位预处理将行首标点合并到上一行段落始终被拆成两段但看不出结构差别上一行末尾是句号脚本按“段落结束”保留换行结合下一行是否小写开头综合判断列表项全被合并进正文段落行首数字结尾带了中文顿号正则没识别将数字顿号纳入列表项识别规则代码块里的换行全没了代码变成长字符串后处理没有先保护代码块先提取代码块为占位符处理完再恢复Markdown 表格排版错乱列对不齐表格单元格内部出现软换行将同一行的多个片段合并后再输出页眉页脚混进正文提取器把页面固定区域当正文输出按坐标过滤掉页眉页脚区间或按重复行过滤OCR 把标题和正文混在一起OCR 版面分析不完整用版面识别结果区分标题块标题单独输出公式变成一堆特殊符号或乱码PDF 公式字体没有文本层提取拿不到语义使用带公式识别能力的工具或云服务双栏 PDF 阅读顺序错乱按简单坐标排序时左右栏交叉用专业解析工具或先做列区域分割再排序5.2 批量处理时的几个实用习惯最后分享几个我踩过几次坑之后养成的习惯。第一个习惯是拿到 PDF 先抽样看十页不要直接全量开跑。很多问题只在特定页出现比如某页插图多某页表格跨页某页是目录页。先抽查能及时发现工具选型是否有问题避免跑完几百页才发现全废了。第二个习惯是默认保留中间产物具体做法是每个阶段都输出独立文件原始提取文本、OCR 结果、后处理脚本的参数配置、最终 Markdown。后处理规则调整是常态没有中间产物就只能重新提取浪费时间。第三个习惯是写后处理规则时尽量让规则“可解释”。比如正则里的每个分支都加注释说明它对应什么问题这样下次遇到新文档时只要看注释就能判断该调整哪里不用从零排查。最后一个习惯是不要迷信单一工具。同一份 PDF我会用两个工具各出一份结果比较哪个更干净然后选择其中一条路线操作。毕竟 PDF 千奇百怪没有万能的工具只有不断适配的工作流。我个人现在处理 PDF 的第一步就是判断类型能复制文字的走文本提取纯图片的走 OCR复杂排版的直接上专业解析工具。换行问题的本质其实是“文档结构理解”问题手工删换行之所以痛苦是因为所有结构判断全堆在手上。把这些判断拆成规则交给脚本一次解决之后无论遇到多少 PDF 都只需要微调参数。如果只是日常整理资料、做笔记我更推荐直接用带版面分析的工具省心得多。希望这篇整理能帮你把“删换行”这个动作从手动列表里永久划掉。
返回列表