上周有位做资料整理的朋友丢过来一个压缩包,里面两百多份PDF,问怎么把PDF转TXT最快,因为他要在全部文本里做关键词检索。我随手打开第一个文件,鼠标一拖能选中文字,说明这是文本型PDF,直接抽字符就行;再打开第二个,拖了半天全是空白选区,这是扫描件,必须走图像识别那条路。同一批文件里两种结构混着,如果上来就随便找个工具点一下"转换",结果一定是有的文件干干净净、有的文件一片空白,还得回头返工重来。
PDF转TXT看着是"点个按钮"的事,真正让人返工的坑其实就两个:一是判断错文件类型,把扫描件丢进文本抽取工具,抽出来一堆空行还以为是工具坏了;二是完全不管排版,双栏论文抽出来左右栏交错穿插,读起来像乱码。这篇就把我这些年用过的六种方案摊开讲清楚,从零门槛的界面操作到能跑批量的脚本,每种都说明白它适合什么文件、参数怎么调、哪里会翻车。不管你是偶尔转一两个文件的普通用户,还是要处理成百上千份文档的开发者,都能在里面找到能直接抄的那一套。
1. 先判断PDF里装的是什么,再谈选哪个方案
很多人一上来就问"哪个工具最好用",这个问题本身问错了。PDF是一个容器格式,里面可以装排版好的文字流,也可以装一张扫描出来的图片,甚至同一份文件里前几页是文字、后几页是扫描件。选方案的前提是搞清楚容器里到底装了什么,否则再好的工具用在错误的文件上也是白费。
1.1 三个动作,三十秒判断文件类型
最直接的办法是用阅读器打开,按住鼠标在正文上拖动。如果文字能被蓝色高亮选中,复制出来还有内容,那就是文本型PDF,走字符抽取的路子,速度快、准确率高、几乎不需要校对。如果拖过去毫无反应,或者只能框选出一个大矩形,说明页面上是一张位图,文字只是像素,任何抽取工具都读不出内容,必须先用OCR把图像识别成文字。
第二个辅助判断是看文件体积和页数的比例。纯文字排版的PDF压缩率很高,一页通常只有几十KB;扫描件是按图像存的,一页动辄几百KB甚至上兆。一份五十页的文件如果只有两三百KB,基本可以确定是文本型;如果超过五十兆,八成是三百dpi的扫描图。这个判断不绝对,但作为快速筛查非常管用。
第三个是命令行手段,装了poppler工具集之后直接跑pdffonts input.pdf,它会列出文件里嵌入的所有字体。文本型PDF必然有字体列表,哪怕只有一个子集化的中文字体;如果输出是空的表头、一行字体都没有,那这文件里根本没有文字对象,只可能是图。我在批量处理前习惯先跑一遍这个命令,把整个目录的文件分两堆,后面就不用反复试错了。
| 判据 | 文本型PDF | 图像型PDF |
|---|---|---|
| 鼠标能否选中文字 | 能,且复制有内容 | 不能,只能框选整块 |
| 单页平均体积 | 几十KB | 数百KB到数MB |
pdffonts输出 | 有字体列表 | 空表头 |
| 处理路线 | 字符抽取 | 图像渲染 + OCR |
1.2 文本型PDF抽出来也乱,问题出在字体编码上
搞清楚类型只是第一步。我见过太多人确认是文本型之后兴冲冲跑完转换,打开TXT一看全是"锟斤拷"或者一堆问号方块,就断定工具不行。实际上乱码的根源在PDF内部的字体编码映射上,跟工具关系不大。
PDF里的文字并不是直接存成"你好"这样的字符,而是存成字形编号(glyph id),再靠一张叫ToUnicode的映射表把编号翻译回真正的字符。生成PDF的软件如果在导出时没写这张表,抽取工具就只能拿到一堆编号,翻译不出来自然就乱码。用某些老版本排版软件、某些打印机驱动"打印成PDF"生成的文件,这个问题特别常见。判断方法很简单:用阅读器能正常看到字、能选中复制,但抽取出来是乱码,那基本就是缺映射表。
另一种常见情况是中文双栏排版。PDF的内容流顺序未必等于人眼阅读顺序,有的文件按"左栏全部、右栏全部"存,有的按"第一行左、第一行右"交错存。抽取工具默认按内容流顺序读,遇到后者就会抽出左右栏混在一起的句子。这时候需要的是能拿到字符坐标、允许你按区域切分的工具,而不是简单地换个软件再试一次。
提示:判断文件能不能直接抽,别靠"看起来像不像扫描件",一定要实际选中一段文字试试。有些扫描件外面套了一层看不见的文字层(OCR过的),能选中但选中的是识别结果,质量参差不齐,这种情况抽出来能用但必须校对。
2. 方案一:阅读器与办公套件的"导出为纯文本"
这是门槛最低的一条路,适合手头只有一两个文件、不想装任何额外工具的场合。主流PDF阅读器和办公套件基本都带"导出为"或"另存为其他格式"的功能,在菜单里找"文件—导出—纯文本"这一类的入口,选好输出目录点确定就行。整个过程不需要写一行代码,也不需要联网。
2.1 单文件操作的几个细节
操作本身没什么可说的,但有几个设置项值得留意。导出对话框里通常会让你选页码范围,如果只需要其中某几章,填上范围能省不少时间;有的工具还提供"保留布局"的勾选项,勾上之后输出的TXT会用大量空格模拟原文的物理位置,好处是表格看起来还对得上,坏处是每行前后全是空白、做检索时匹配不上连续的句子。我的建议是:要做全文检索就取消保留布局,要做人工比对就勾上。
编码选项也容易被忽略。老一些的工具默认导出可能是本地编码,遇到生僻字会变成问号,导出时如果有编码下拉框,一律选UTF-8,后面不管喂给什么程序都不会出问题。至于换行符,Windows下默认是CRLF,拿去Linux服务器上处理时可能出现行尾多一个不可见字符,做正则匹配时要记得兼容。
2.2 批量导出:把整个文件夹一次处理完
界面方案最大的短板是不支持批量,几十个文件一个个点会疯掉。变通办法是利用办公套件的宏或者批处理命令。以带宏支持的办公套件为例,写一段几十行的宏,遍历指定目录下所有PDF文件,逐个打开、导出为TXT、关闭,整个流程可以无人值守跑完。思路是这样的:先取目录里的文件列表,用循环过滤出扩展名为pdf的项,调用文档对象的导出方法指定输出路径,最后记得关闭文档释放句柄,否则处理到几十个之后会提示文件被占用。
这条路的好处是图形界面友好、出错时能直观看到是哪一页出的问题;缺点是速度慢,每打开一个文件都要加载渲染引擎,处理两百个文件可能要跑十几分钟,而且后台运行时偶尔会弹窗打断流程。文件数量超过一百,我还是更推荐后面的命令行方案。
2.3 这个方案的天花板在哪里
界面导出在三种情况下会明显吃力。第一种是文件里有大量表格,导出的TXT会把表格拆成一堆散落的词,列与列的对应关系全丢;第二种是页眉页脚和页码,每个文件都会把这些重复内容原样搬进正文,清洗时得单独处理;第三种是加密文件,导出前会强制要求输入密码,批量场景下没法自动跳过。
3. 方案二:命令行 pdftotext,批量处理的性价比之王
如果只让我保留一种PDF转TXT的工具,我会选pdftotext。它是poppler工具集里的一个命令行程序,安装包很小,Linux下一条包管理命令就能装好,Windows和macOS也有对应的预编译版本。它的核心优势是快、稳定、支持通配符批量、可以嵌进任何脚本里,而且输出的文本质量在处理常规文本型PDF时不输给商业软件。
3.1 最小可用命令与常用参数
最基础的用法只有两个参数:输入文件和输出文件。
pdftotext -enc UTF-8 input.pdf output.txt-enc UTF-8建议每次都带上,明确指定输出编码,避免在不同系统上出现编码不一致。不加这个参数时,工具会按本地环境推断,跨平台搬运时容易出问题。
真正决定输出质量的是下面三个参数,它们的取舍直接决定你拿到的TXT长什么样:
-layout:尽量保留原文的物理布局,用空格填补位置。适合表格、目录、带缩进的公文。缺点是行首行尾会有一堆空格,做字符串匹配前得先trim,长句子还可能被中间截断。-raw:完全按内容流顺序输出,不做任何重排。速度快,但双栏文档会交错,一般只在排查问题时用。-nopgbrk:默认情况下工具会在每页之间插入一个换页符(\f),加上这个参数就不插了。如果你后续要按页切分做统计,就别加;如果只是要一整段连续文本,加上更省事。
还有一个组合很实用:-f 1 -l 10指定只处理第1到第10页。处理几百页的大部头时,先抽前十页看看效果,确认参数调对了再跑全量,能省下大量等待时间。
3.2 一次处理整个目录
命令行的价值在批量时才真正体现出来。下面这段在bash里跑,把当前目录所有PDF转成同名TXT:
for f in *.pdf; do pdftotext -enc UTF-8 -layout "$f" "${f%.pdf}.txt" done${f%.pdf}是去掉扩展名的写法,保证输出文件名和原文件对应。如果目录结构有多层,换成find更稳妥:
find ./docs -name "*.pdf" -print0 | while IFS= read -r -d '' f; do pdftotext -enc UTF-8 "$f" "${f%.pdf}.txt" done这里用-print0和read -d ''是为了兼容文件名里带空格或中文的情况,很多人的脚本在遇到"报告 (1).pdf"这种名字时会直接断掉,就是没处理分隔符。
3.3 加密文件和坏页的兜底
批量跑最怕的是中途卡住。加密PDF会让工具停在密码提示上,无人值守时整个流程就挂在那里。解决办法是在循环里加超时控制,或者先用pdfinfo批量探测一遍,把带Encrypted: yes标记的文件挑出来单独处理。
坏页的处理思路类似。有些文件某几页的结构损坏,工具会报错退出,但前面已经转换的页其实是有内容的。可以用-q关闭警告输出,再配合判断返回码,失败的文件记进日志跳过,不要让它阻断整批任务。我一般的做法是维护一个fail.log,跑完之后人工看一遍这几个文件,多数是扫描件混在里面,转去走OCR方案就行。
4. 方案三:pdfplumber,多栏排版和表格的精细处理
pdftotext解决的是"快",pdfplumber解决的是"准"。这是一个Python库,它的工作方式和命令行工具完全不同:它把每一页拆成一个个字符对象,每个字符带着自己的坐标、字体、大小、颜色信息。你能拿到这些坐标,就能做区域裁剪、按栏切分、按行聚类,处理双栏论文、复杂表格、带侧边栏的报告时优势非常明显。
4.1 拿到字符坐标之后能做什么
先看一段最小代码,把一页的文字抽出来:
import pdfplumber with pdfplumber.open("paper.pdf") as pdf: for i, page in enumerate(pdf.pages): text = page.extract_text(x_tolerance=2, y_tolerance=3) print(f"--- page {i+1} ---") print(text)x_tolerance和y_tolerance是两个关键参数,控制"横向和纵向相距多远的字符算同一行"。默认值对英文排版够用,中文文档经常需要调小,因为中文字符间距小,默认阈值会把两行并成一行。我处理中文材料时一般从y_tolerance=2开始试,观察输出有没有把标题和正文粘在一起,再微调。
双栏文档的处理思路是先切区域再抽取。页面的宽度可以从page.width拿到,把页面从中间切成左右两块,分别抽再拼接:
left = page.crop((0, 0, page.width / 2, page.height)) right = page.crop((page.width / 2, 0, page.width, page.height)) text = left.extract_text() + "\n" + right.extract_text()这样抽出来的顺序就是先读完左栏再读右栏,符合人的阅读习惯。栏宽不一定是精确的一半,遇到带侧边栏的排版,可以先抽一页看看字符的x坐标分布,找一个明显的空白间隔当分割线,比硬编码一半靠谱得多。
4.2 表格单独抽出来,别混进正文
PDF里的表格对文本抽取来说是天敌。表格本质上是靠位置对齐的视觉结构,转换成一维文本后列与列的对应关系就丢了。pdfplumber提供了extract_tables(),它会尝试识别表格线并把单元格内容还原成二维列表:
with pdfplumber.open("report.pdf") as pdf: for page in pdf.pages: for table in page.extract_tables(): for row in table: print(" | ".join(cell or "" for cell in row))这段代码能直接输出用竖线分隔的行,拿去存CSV或者喂给数据处理脚本都行。识别的准确率取决于原表格有没有清晰的框线,纯靠空白对齐的"隐形表格"识别率会掉不少,遇到那种情况建议把table_settings里的vertical_strategy改成text,让工具靠字符间距推断列边界。
4.3 这个方案的成本核算
pdfplumber不是快的方案。它要解析每个字符的坐标,处理一份两百页的文件可能要几十秒到几分钟,比pdftotext慢一个数量级。所以我的用法是分工:先用pdftotext全量跑一遍,抽出文字长度异常短的文件(比如一页只有几十个字符),这些就是排版复杂或者图表为主的,再单独用pdfplumber精修。这样既保证速度,又保证质量。
5. 方案四:PyMuPDF,能扛住异常文件的批量流水线
PyMuPDF(导入名是fitz)是另一个Python库,它的定位和pdfplumber略有不同:速度更快、对损坏文件的容错更好、内置了渲染成图片的能力。如果你要搭一条从PDF到TXT的自动化流水线,它是很合适的主力。
5.1 抽取模式的选择
它提供多种抽取模式,get_text("text")输出纯文本,get_text("blocks")按文本块输出并附带坐标,get_text("dict")给到最细的字符级信息。日常转换用第一种就够:
import fitz doc = fitz.open("book.pdf") for page in doc: text = page.get_text("text", sort=True) print(text)sort=True这个参数值得强调。默认情况下它按内容流顺序输出,双栏文档会交错;加上排序之后会按坐标从上到下、从左到右重排,绝大多数情况下更接近人眼看到的顺序。这个参数是我从踩坑里学来的,早期不知道有它,处理期刊论文时抽出来的句子前后不搭,白白浪费了一下午排查。
5.2 加密、损坏、超大文件的处理
流水线最怕中途崩溃,所以每一步都要有兜底。加密文件先检查doc.needs_pass属性,为真就调doc.authenticate(密码),密码从配置表里读,读不到就记进失败日志跳过。损坏文件用try包住整个处理过程,单个文件失败不影响整批。超大文件(几百兆、上千页)建议边处理边写盘,不要全部攒在内存里一次性输出,否则内存占用会很快飙上去。
import fitz, os, traceback def convert(src, dst): try: doc = fitz.open(src) if doc.needs_pass and not doc.authenticate(PASSWORD_MAP.get(src, "")): raise RuntimeError("需要密码") with open(dst, "w", encoding="utf-8") as out: for page in doc: out.write(page.get_text("text", sort=True)) out.write("\n") doc.close() return True except Exception: with open("fail.log", "a", encoding="utf-8") as log: log.write(f"{src}\t{traceback.format_exc()}\n") return False5.3 断点续跑和输出命名
批量跑到一半断电或者被手动中断是常事,所以脚本要支持续跑:处理前先检查目标文件是否存在且大小大于零,存在就跳过。输出文件名要处理冲突,不同子目录下可能有同名PDF,直接把TXT平铺到一个目录里会互相覆盖,稳妥的做法是保留目录层级,或者在文件名前拼上原目录名的哈希前缀。
这一套搭好之后,日常批量转换基本不用操心,跑完看一眼fail.log里剩几个文件,再针对性处理。
6. 方案五:扫描件走OCR,从渲染图片到可检索文字
前面四种方案全部建立在"PDF里有文字对象"这个前提上。扫描件打破了这个前提,页面上只有像素,任何抽取工具都读不出内容,必须用OCR把图像识别成文字。这条路成本最高,但也是唯一的路。
6.1 渲染DPI决定了识别的上限
OCR的准确率上限在渲染那一刻就定死了。分辨率太低,笔画粘连,识别结果惨不忍睹;分辨率太高,处理速度成倍下降,收益却趋于平缓。实践中300dpi是甜点,普通印刷体文档在这个分辨率下识别率已经很高,再往上提到600dpi提升有限但耗时翻倍。只有遇到小字号(比如脚注、表格里的小字)时,才值得单独提到400dpi以上。
用PyMuPDF把页面渲染成图片很简单:
import fitz doc = fitz.open("scan.pdf") for i, page in enumerate(doc): pix = page.get_pixmap(dpi=300) pix.save(f"page_{i+1:04d}.png")也可以用命令行工具pdftoppm -r 300 -png scan.pdf page,效果一致,适合纯脚本流水线,不用装Python环境。
6.2 中文识别模型和参数配置
开源OCR引擎里Tesseract用得最广,装好引擎本体之后还要单独装中文语言包,识别时用-l chi_sim+eng同时启用中英文,混排文档能一起处理。页面分割模式(--psm)对结果影响很大:整页统一排版用默认的自动分割(psm 3)就行;如果整页是一大段连续文字、没有分栏和表格,用psm 6能减少误切;如果是单行文字(比如表格里的某一格裁出来的图),用psm 7最准。
tesseract page_0001.png out -l chi_sim+eng --psm 6识别结果会写到out.txt。批量的话把上面这条包进循环,注意每个文件指定不同的输出前缀,否则会互相覆盖。
6.3 版面分析:先切块再识别效果更好
整页丢进去识别,遇到双栏、带图表、带页眉页脚的版面,引擎容易把顺序搞乱。更稳的做法是先做版面分析,把页面切成若干文本区域,逐块识别再按坐标顺序拼接。轻量的做法是自己按水平投影切:统计每一行像素的墨迹密度,连续多行空白就是段落分隔,据此把页面切成上下若干块,块内再整块识别。这种做法实现简单,对付规整的双栏排版够用。
注意:OCR结果一定要保留页码和坐标信息。后期校对时,能定位到"第几页第几块"比拿到一个几万字的纯文本有用得多,尤其是要跟原文对照的时候。
6.4 校对成本要提前算进去
OCR不是免费的午餐,中文识别即使准确率到了98%,一篇五千字的文章也有上百处错误,散落在"己"和"已"、"末"和"未"这些形近字上,肉眼扫一遍很难全找出来。如果只是做全文检索、不要求逐字准确,可以不校对,靠模糊匹配容忍错字;如果要做语料或者出版,就必须逐句核对,这个工作量往往比转换本身还大。所以我的原则是:能走字符抽取就绝不走OCR,只有确认没有文字层时才用。
7. 方案六:在线转换和浏览器里的土办法
前面五种都要装工具。偶尔在别人的电脑上、或者手机上临时要转一个文件,在线转换站点和浏览器自带的功能就能救急。但这两个方案有明确的适用边界,用错了会出问题。
7.1 哪些文件可以上传,哪些绝对不能
在线转换的原理很简单:你把文件传上去,服务器转好再让你下载。这意味着文件内容离开了你的设备。合同、身份材料、内部报表、未公开的研究数据,这类东西一律不要上传,不管站点声称多久之后删除。判断标准就一条:这份文件如果明天出现在公开网络上,你会不会有麻烦。会,就别传。
可以传的是公开资料、自己写的草稿、已经发布过的文档。上传前顺手做两件事:一是删掉PDF里的元数据(作者、创建软件、原始路径都可能泄露信息),二是把无关页面裁掉,只传需要的那几页,减少暴露面。
7.2 站点限制和压缩陷阱
免费站点普遍有页数限制(常见是每次十页到五十页)和体积限制(十兆到二十兆)。超过限制时它们会提示你"压缩后重试",这里要留个心眼:压缩往往意味着降低图片分辨率,扫描件被压过之后OCR准确率会明显下降,转出来的文字错得更多。与其压文件,不如拆分页面上传,质量更有保障。
转换完成的TXT还要检查一下编码。有些站点导出的是带BOM的UTF-8,用普通文本编辑器打开看不出问题,但喂给脚本做字符串比较时会莫名匹配失败,处理前记得先去掉BOM头。
7.3 浏览器里的两个应急手段
第一个是直接复制粘贴。文本型PDF在浏览器里打开后全选复制,粘到记事本里存成TXT,效果和用工具导出差不多,缺点是段落间的换行会按视觉行断开,粘出来每行只有十几个字,得再跑一遍合并。第二个是打印成文件,某些场景下把网页或者文档"打印"为纯文本格式的虚拟打印机,能绕过一些工具的限制。
这两个办法的共同问题是排版丢失严重、无法批量,只适合救急。真正长期要处理文档,还是老老实实把pdftotext或者PyMuPDF装好,写个脚本一劳永逸。
8. 转换完成之后的清洗与验收
工具跑完只是完成了一半,直接拿去用的TXT几乎总是脏的。中文断行、页眉页脚、连字符、多余空行,这几类问题在所有方案里都会出现,清洗环节省不掉。
8.1 三类高频脏数据的处理写法
中文断行是最普遍的。PDF按视觉行存储,一句完整的话被切成好几行,转成TXT后每行末尾都有换行符。合并的思路是:如果换行符前后都是中文字符,就把换行删掉。
import re text = re.sub(r'(?<=[\u4e00-\u9fa5])\n(?=[\u4e00-\u9fa5])', '', text)这个正则要小心,它会把真正该保留的段落分隔也吃掉,因为段落之间往往也是中文字符夹着换行。稳妥的做法是先按空行切段,段内做合并,段间保留。英文文档还有连字符问题,行末的"develop-\nment"要合并成"development",规则是把"-\n"替换成空字符串,但要注意保留本来就连字符的词(比如"well-\nknown"其实该保留连字符),这种情况下只能靠词典或者人工抽查。
页眉页脚的处理靠统计。把每页的第一行和最后一行抽出来做词频统计,出现次数超过总页数一半的,基本可以判定是页眉或页码,统一删掉。这个方法对付规整的文档非常有效,我处理几百页的技术手册时,靠它一次性清掉了页眉里的章节名和页脚里的页码。
8.2 一份可以直接照着走的验收清单
转换结果别急着交付,按下面几条过一遍,能挡掉九成的问题:
| 检查项 | 判断方法 | 不合格时的处理 |
|---|---|---|
| 总字数是否合理 | 与页数×每页平均字数对比 | 明显偏少说明是扫描件或抽取失败 |
| 是否有乱码字符 | 搜索"锟斤拷"、问号方块、替换符 | 换编码或换工具重抽 |
| 中文是否断行 | 随机抽三段读一遍 | 跑断行合并脚本 |
| 页眉页脚是否残留 | 统计每页首末行词频 | 按频次批量删除 |
| 表格是否可读 | 抽查含表格的页面 | 单独用表格抽取导出CSV |
| 特殊符号是否丢失 | 搜索公式、上下标所在位置 | 这部分通常只能人工补 |
最后再分享一个我自己的习惯:转换脚本跑完之后,随机抽三个文件从头读一百个字。这一步花不了两分钟,但能立刻发现编码、断行、顺序这类系统性问题。比起全部处理完再回头返工,先抽检再放量永远是更划算的做法。