ARTICLE DETAIL

资讯详情

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

Python处理PDF全攻略:从库选型到OCR与踩坑实战

Python处理PDF全攻略:从库选型到OCR与踩坑实战 前阵子接手一个批量处理标书的活儿几百份PDF要提取关键信息、合并盖章、转档分发我第一反应是用Python快速搞定。结果一上手才发现PDF这玩意儿看着人畜无害实际处理起来坑多到让人怀疑人生有的库提取中文乱码有的表格一跨页就断有的扫描件压根没字符层。折腾了几天把主流的处理方案捋了个遍也踩了不少雷。这篇就把我的实际经验摊开来讲从库选型到具体操作再到那些文档里从来不写的老坑一次性说清楚。1. 先选对库PDF处理的工具矩阵与选型逻辑处理PDF第一件事不是写代码而是想清楚你到底要干嘛。PDF这个格式特殊在它是个最终态文档——设计目标就是无论什么设备打开都长得一模一样所以它内部存的是精确的坐标、字体、图形对象而不是像Word那样存段落和样式。这意味着你想编辑PDF本质上是在改一堆绘制指令想提取内容就得从绘制指令里反推文本流和结构。不同库在这件事上的侧重点完全不同。我用下来主流方案基本分成四类库名定位强项短板pdfplumber内容提取文本、表格提取精度高速度偏慢不适合大型文件pypdf / PyPDF2文档操作合并、拆分、加密、旋转等轻量操作文本/表格提取能力弱PyMuPDF(fitz)全能选手极快能渲染图片、提取文字、操作页面API风格偏底层需要适应ReportLab文档生成从零生成PDF页面控制精细纯代码画图中文处理要额外配置你可以简单理解成想从PDF里拿东西用pdfplumber想对PDF做手术合并拆分加密用pypdf追求速度和批量渲染就用PyMuPDF要造一个全新PDF就用ReportLab。实际项目中经常是混合用。环境准备也比较直接直接用pip装就行pip install pdfplumber pypdf pymupdf reportlab pdf2docx我建议把这几个都装上PDF处理几乎没有只用单一库就能完美解决的问题。比如我最近一个项目就是pdfplumber提取表格pypdf做拆分PyMuPDF把扫描页渲染成图片再OCR各干各的活互不干扰。2. 内容提取实战文本、表格与扫描件识别2.1 文本提取pdfplumber的常规操作与乱码应对提取文本是PDF处理里最基础也最常用的需求。用pdfplumber提取文本非常简单核心就几行import pdfplumber with pdfplumber.open(demo.pdf) as pdf: for page in pdf.pages: text page.extract_text() if text: print(text)extract_text()会把当前页面上能识别出的文字对象按阅读顺序拼成字符串。但这里有个很多人第一次用就会困惑的点返回的text可能是None。这不是BUG而是这个页面根本没有文本层比如是扫描件或者是某种特殊编码文字是用绘图指令画上去的曲线。遇到这种情况别死磕这一个库直接换思路。另一个高频坑是中文乱码。pdfplumber本身对中文PDF的提取能力其实不错但有些PDF尤其国产软件生成的或者从WPS、方正排版系统导出的字体编码不规范提取出来全是乱码或空白。我的经验是用PyMuPDF兜底import fitz # PyMuPDF doc fitz.open(demo.pdf) for page in doc: text page.get_text() print(text)这两个库提取文本走的是完全不同的技术路径pdfplumber会去解析PDF的字符流和字体信息再按坐标重组文字PyMuPDF是直接读取内容流里的文本对象。所以一个提取失败时另一个往往能救回来。实测中乱码PDF用PyMuPDF的成功率明显更高缺点是有时会把同一段文字拆得零零碎碎需要自己拼一下。2.2 表格提取从PDF里挖出结构化数据表格提取是PDF处理里真正有技术含量的事。PDF本身没有表格概念它只知道自己画了哪些线条、在哪放了哪些文字。pdfplumber之所以表格提取强是因为它会利用页面上的线条和文字位置重建网格结构再按行列把文字塞进去。import pdfplumber with pdfplumber.open(table.pdf) as pdf: page pdf.pages[0] tables page.extract_tables() for table in tables: for row in table: print(row)返回的结构是一个嵌套列表每个table代表一张表格每行又是一个列表单元格内容可能是字符串也可能是None空白单元格。拿到这个结构后要存成Excel、CSV或者写入数据库都很容易import csv import pdfplumber with pdfplumber.open(table.pdf) as pdf: page pdf.pages[0] table page.extract_tables()[0] with open(output.csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerows(table)实际处理表格时最磨人的不是提取本身而是表格样式不规整。比如没有完全封闭的框线、表头跨页、单元格里换行等。我的做法是先只提取一页做可视化验证——把提取出来的表格数量、行列数、是否有空行打出来看看再决定要不要调extract_table()的table_settings参数。比如对付那些线不完整的表格可以让它只依赖文字位置推断网格table page.extract_table({ vertical_strategy: text, horizontal_strategy: text, })意思是不用线定位而是根据文本的列对齐关系来切分。对付特殊的表格这个参数能救命。2.3 扫描件使用OCRPyMuPDF配合Tesseract扫描版PDF本质上不是PDF是一堆图片套了个PDF外壳。pdfplumber根本提取不出文字因为压根没有文本层。这时候只能上OCR我常用的组合是PyMuPDF渲染页面成图片再用Tesseract识别。import fitz import pytesseract from PIL import Image import io doc fitz.open(scan.pdf) for page in doc: pix page.get_pixmap(dpi200) img_data pix.tobytes(png) img Image.open(io.BytesIO(img_data)) text pytesseract.image_to_string(img, langchi_sim) print(text)有几个值得注意的点。DPI建议至少200太低了识别率明显下滑太高了文件大耗时久200-300是性价比区间。识别中文要装对应语言包Tesseract默认只有英文中文包需要单独下载装好后langchi_sim才能生效。另外字迹清晰的正规扫描件识别率很高但印章盖到文字上或者页面歪斜的最好先做预处理比如转灰度、去噪点这些用PIL就能实现。OCR这块我的建议是不要追求完美识别每一个字而是把目标定在能把关键字搜出来。很多场景下你不需要全文正确只要能把合同编号、姓名、金额这类信息捞出来就够了。3. 格式转换PDF转Word与HTML生成PDF3.1 PDF转Word版式还原的可行性边界PDF转Word可能是大家最常搜的功能。很多免费工具网站都能转但批量处理、格式敏感的时候还是本地脚本更靠谱。Python这边我用得最多的是pdf2docx库它内部是PyMuPDF解析页面布局再用python-docx重建Word文档。from pdf2docx import Converter cv Converter(demo.pdf) cv.convert(demo.docx) cv.close()几行就能完成转换但转换质量完全取决于源PDF的复杂程度。纯文字型、单栏排版的PDF转换效果最好能还原出段落和标题。让我吃过亏的是这些情况多栏排版的论文PDF转出来文本框移位频繁页眉页脚会被当作正文内容插入每段复杂表格转成Word后变成图片或乱排说到底是因为PDF压根不记录这是标题这是正文段落这类语义信息转Word本质上是从几何坐标逆推文档结构有天然上限。我的做法是能用就用批量转换前先抽查几页看看效果没问题再跑全量。遇到排版精密的文件转换完必须人工核对一遍别直接拿转换结果交付。3.2 HTML转PDF让网页和报表直接落地成文档反向的PDF生成需求也很常见比如把报表页面存成PDF存档或者把Markdown文档渲染成PDF发出去。这个方向我最喜欢用pdfkit它是wkhtmltopdf的Python封装可以直接把HTML字符串或URL转成PDFCSS支持度还不错页面样式基本能原样保留。# 需要先安装 wkhtmltopdf sudo apt install wkhtmltopdf # Ubuntu brew install wkhtmltopdf # macOSimport pdfkit # HTML字符串转PDF html h1这是标题/h1p这是正文内容/p pdfkit.from_string(html, output.pdf) # 网页URL转PDF pdfkit.from_url(https://example.com/report, web.pdf) # 本地HTML文件转PDF pdfkit.from_file(index.html, page.pdf)这个方案最大的优势是排版能力。你想生成带样式的漂亮PDF用HTMLCSS来写比用ReportLab一像素一像素调位置快得多学成本和维护成本都低很多。我现在做给非技术同事看的自动化报表基本都走这条链路。一个实际经验Python 3.10及以上版本有些环境直接pip装pdfkit会拉下来一个很老的版本运行正常但功能不全。遇到问题可以先检查wkhtmltopdf是否在系统PATH里很多时候PDF转出来空白的根源不是代码而是这个外部工具没装好。4. 文档编辑合并、拆分、加密与加水印4.1 合并与拆分用pypdf处理批量文档合并、拆分这种文档操作请求技术含量不高但架不住量大啊。几十份合同合并成一份提交几百页的资料按页码拆出去手工做能累死。pypdf写起来非常顺手。批量合并from pypdf import PdfWriter, PdfReader merger PdfWriter() for path in [doc1.pdf, doc2.pdf, doc3.pdf]: merger.append(path) merger.write(merged.pdf)append()可以直接传文件路径也可以传PdfReader对象后者可以做一些更细的控制比如只append某个文件的一部分页面reader PdfReader(big_file.pdf) merger.append(reader, pages(0, 5)) # 只要第0到4页按页拆分成独立文件from pypdf import PdfReader, PdfWriter reader PdfReader(large.pdf) for index, page in enumerate(reader.pages): writer PdfWriter() writer.add_page(page) with open(fpage_{index 1}.pdf, wb) as out_file: writer.write(out_file)值得注意的一个细节pypdf新版本3.x建议用PdfReader和PdfWriter老教程里的PdfFileReader已经废弃了新代码就别再用了。另外writer.write()之后要把writer关掉或者复用前重置不然多次写入同writer会累积页面。如果你处理的PDF里有大量高清图片合并后文件会膨胀得厉害。我处理过一批扫描合同合并前总共200MB合完变成1.2GB。后来先跑了一遍图片压缩再合并体积控制在300MB以内。这个问题很容易被忽视直到服务器磁盘告警才发现。4.2 加密与解密密码保护的常见处理方式给PDF加密码是常见需求尤其是发出去的资料不想让人随意改动。pypdf里加密很简单from pypdf import PdfReader, PdfWriter reader PdfReader(original.pdf) writer PdfWriter() writer.append_pages_from_reader(reader) writer.encrypt(my_password) with open(encrypted.pdf, wb) as out_file: writer.write(out_file)encrypt()支持两个密码参数owner password和user password。传一个参数的话默认是owner password控制能否修改文档权限给用户看的只读密码可以再传个user_password。实际使用中大多数场景一个密码就够不用分太细。解密同样简单from pypdf import PdfReader reader PdfReader(encrypted.pdf) if reader.is_encrypted: reader.decrypt(my_password) for page in reader.pages: print(page.extract_text())有个反直觉的坑decrypt()成功后会返回一个枚举值表示解密状态但reader.is_encrypted在解密后仍然可能显示True这是pypdf的设计不代表没解密成功。很多人在这一步卡半天以为是代码问题。4.3 批量加水印给页面盖个章给PDF批量加水印比如内部资料已审核原理其实是用另一个PDF当底图叠加到目标页上。pypdf的页面合并功能就能实现from pypdf import PdfReader, PdfWriter # 水印PDF通常是一张带透明度文字的页面 watermark PdfReader(watermark.pdf).pages[0] reader PdfReader(archive.pdf) writer PdfWriter() for page in reader.pages: page.merge_page(watermark) writer.add_page(page) with open(watermarked.pdf, wb) as out_file: writer.write(out_file)水印PDF通常用ReportLab生成就行了普通的文字打印出来就行。关键是水印文字要设成半透明效果不然会盖住正文影响阅读。这个可以预先在生成水印时控制透明度如上图文字对象用setAlpha来调整。用这个方案批量处理几百个文件也很快基本是秒级完成。5. 源码级踩坑记录中文、性能与输出质量5.1 中文PDF提取乱码问题不总在提取层前面提过中文PDF提取乱码的事这里展开说说我一个印象深刻的案例。有次处理某政务网站导出的公告PDFpdfplumber提取出来全是空白PyMuPDF提取出来是一堆乱七八糟的CJK字符。折腾半天发现这个PDF的文本对象根本不存在——所有汉字都是直接调用字体的字形轮廓画上去的本质上跟扫描件没有区别只是不是位图而是矢量图形。这种情况什么提取库都白搭唯一的路就是像扫描件那样OCR。先把页面渲染成图片再跑Tesseract。所以遇到提取乱码先别怀疑库不好老老实实检查这个PDF是不是真文本。最简单的方法用浏览器打开PDF选中文字拖一下看能不能选中的一条连续的文本。如果能说明有文本层可以尝试提取拖不动的就是伪文本直接走OCR路线。5.2 ReportLab中文输出字体注册这件小事用ReportLab生成PDF时新手第一坑就是中文。直接drawString写中文出来全是方框原因很简单PDF的字体机制里中文字体不是内置的必须把字体文件嵌入文档。ReportLab默认字体只支持Latin字符集。解决办法是注册一个中文字体from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import A4 # 注册中文字体SimSun是宋体 pdfmetrics.registerFont(TTFont(SimSun, C:/Windows/Fonts/simsun.ttc)) c canvas.Canvas(hello_cn.pdf, pagesizeA4) c.setFont(SimSun, 12) c.drawString(100, 700, 你好PDF中文输出测试) c.save()Linux上如果没有中文字体文件可以先安装文泉驿等开源中文字体。注册好字体后还有个小坑设置了字体之后每段文字都要显式setFont否则canvas会回退到默认字体导致中文再次变方块。制作带中文的表格或复杂图形直接用ReportLab会比较吃力。我的习惯是优先考虑HTML转PDF方案HTML的表格和CSS排版能力强太多了只有需要做精确到点的图形绘制比如生成标签、计价单时才动用ReportLab。5.3 性能问题几十万页PDF怎么扛最后聊聊性能。PDF处理平时小打小闹感觉不到差异但量一大就立刻见分晓。同样是提取文本pdfplumber比PyMuPDF慢好几倍。我在一台普通笔记本上测试过一个约500页、每页有一张图表的PDFpdfplumber提取全部文本需要40多秒PyMuPDF只要8秒左右。差一个数量级不止。总结一下我现在的性能策略批量提取、需要渲染图片的操作优先用PyMuPDF表格提取精度要求高、文件页数不多时才上pdfplumber合并拆分这种轻操作pypdf本身效率不高但对文件内容不敏感复杂PDF也不会卡死超大批量任务别一次性把整个文件都读进内存逐页流式处理或用for page in reader.pages这种懒加载方式另外一个经常被忽略的点是文件句柄释放。用with pdfplumber.open(...)没问题但如果手动PdfReader(...)打开了文件处理完一定要确认文件能重新覆盖写入不然Windows上经常遇到文件被占用删不掉的情况。我在写自动化脚本时通常会同时做异常捕获和finally关闭逻辑。PDF处理这条路入门容易做精深真的难。尤其是国内办公环境下的PDF产品五花八门导出方式千奇百怪你永远不知道下一份PDF会带什么编码、什么字体、什么奇葩结构。最关键的是别拿一把锤子敲所有的钉子——提取、转换、生成、编辑各用最合适的库搭配着来速度快又稳。上面这些代码我都跑通了直接复制就能用遇到问题欢迎回来交流。
返回列表