你不懂得安排自己的人生,会有很多人帮你安排,他们需要你做的事。
PDF文件我们经常用,尤其是这两个场景:
- 下载参考资料,如各类报告、文档
- 分享只读资料,方便传播同时保留源文件
场景和模块
所以,对于PDF文件,常见的需求也就是两类:
- 处理文件本身,属于文件页面级操作,如合并/分拆PDF页面、加/解密、加/去水印;
- 处理文件内容,属于内容级操作,如提取文字、表格数据、图表等。
目前Python用于处理PDF的模块,主要有3个:
PyPDF2:模块成熟,最后一次更新在2年前,适合页面级操作,文字提取效果较差。PDFMiner:擅长文字抽取,目前主分支已停止维护,取而代之的是pdfminer.sixpdfplumber:基于pdfminer.six的文本内容抽取工具,使用门槛更低,如支持表格提取。
实战中,可以根据需求的类型选择模块。如果是页面级的操作,就用PyPDF2,如果需要内容抽取,优先使用pdfplumber。
对应的模块安装:
pip install pypdf2pip install pdfminer.sixpip install pdfplumber
下面按使用场景演示3个模块的使用。
PyPDF2
PyPDF2的主要能力在页面级操作,比如:
- 获取PDF文档基本信息
- PDF分割及合并
- PDF的旋转及排序
- PDF加水印及去水印
- PDF加密及解密
PyPDF2的核心两个类是PdfFileReader和PdfFileWriter,完成PDF文件的读写操作。
获取PDF文档基本信息
import pathlibfrom PyPDF2 import PdfFileReaderpath = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')f_path = path.joinpath('2020-新冠肺炎疫情对中国连锁餐饮行业的影响调研报告-中国连锁经营协会.pdf')with open(f_path, 'rb') as f:pdf = PdfFileReader(f)info = pdf.getDocumentInfo()cnt_page = pdf.getNumPages()is_encrypt = pdf.getIsEncrypted()print(f'''作者: {info.author}创建者: {info.creator}制作者: {info.producer}主题: {info.subject}标题: {info.title}总页数: {cnt_page}是否加密: {is_encrypt}''')
PDF分割及合并
import pathlibfrom PyPDF2 import PdfFileReader, PdfFileWriterpath = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')f_path = path.joinpath('2020-新冠肺炎疫情对中国连锁餐饮行业的影响调研报告-中国连锁经营协会.pdf')out_path = path.joinpath('002pdf_split_merge.pdf')out_path_1 = path.joinpath('002pdf_split_half_front.pdf')out_path_2 = path.joinpath('002pdf_split_half_back.pdf')# 把文件分为两半with open(f_path, 'rb') as f, open(out_path_1, 'wb') as f_out1, open(out_path_2, 'wb') as f_out2:pdf = PdfFileReader(f)pdf_out1 = PdfFileWriter()pdf_out2 = PdfFileWriter()cnt_pages = pdf.getNumPages()print(f'共 {cnt_pages} 页')for i in range(cnt_pages):if i <= cnt_pages //2:pdf_out1.addPage(pdf.getPage(i))else:pdf_out2.addPage(pdf.getPage(i))pdf_out1.write(f_out1)pdf_out2.write(f_out2)# 再把后半个文件与前半个文件合并,后半个文件在前with open(out_path, 'wb') as f_out:cnt_f, cnt_b = pdf_out1.getNumPages(), pdf_out2.getNumPages()pdf_out = PdfFileWriter()for i in range(cnt_b):pdf_out.addPage(pdf_out2.getPage(i))for i in range(cnt_f):pdf_out.addPage(pdf_out1.getPage(i))pdf_out.write(f_out)
PDF的旋转及排序
import pathlibfrom PyPDF2 import PdfFileReader, PdfFileWriterpath = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')f_path = path.joinpath('2020-新冠肺炎疫情对中国连锁餐饮行业的影响调研报告-中国连锁经营协会.pdf')out_path = path.joinpath('002pdf_rotate.pdf')with open(f_path, 'rb') as f, open(out_path, 'wb') as f_out:pdf = PdfFileReader(f)pdf_out = PdfFileWriter()page = pdf.getPage(0).rotateClockwise(90)pdf_out.addPage(page)# 把第二页放到前面pdf_out.addPage(pdf.getPage(2))page = pdf.getPage(1).rotateCounterClockwise(90)pdf_out.addPage(page)pdf_out.write(f_out)
PDF加水印及去水印
加图片水印,其实就是在页面中增加一个透明背景的图片,通过页面的mergePage方法即可完成。
import pathlibfrom PyPDF2 import PdfFileReader, PdfFileWriterpath = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')f_path = path.joinpath('2020-新冠肺炎疫情对中国连锁餐饮行业的影响调研报告-中国连锁经营协会.pdf')wm_path = path.joinpath('watermark.pdf')en_path = path.joinpath('002pdf_with_watermark_en.pdf')out_path = path.joinpath('002pdf_with_watermark.pdf')with open(f_path, 'rb') as f, open(wm_path, 'rb') as f_wm, open(out_path, 'wb') as f_out:pdf = PdfFileReader(f)pdf_wm = PdfFileReader(f_wm)pdf_out = PdfFileWriter()wm_cn_page = pdf_wm.getPage(0)wm_en_page = pdf_wm.getPage(1)cnt_pages = pdf.getNumPages()for i in range(cnt_pages):page = pdf.getPage(i)page.mergePage(wm_cn_page)pdf_out.addPage(page)pdf_out.write(f_out)
去水印,就比较复杂,需要根据不同情况具体分析。因为水印可能是文字、图片或者各种组合,关键是识别出特征。
去水印的3个常见思路参考:
- 找到特征词后替换,适合英文文档,但不适用于中文等CJK字符。
- 把PDF页转成图片后,用图像算法去水印,但这样会破坏文件原信息结构。
- 根据水印大小位置特征,找到所有元素后删除。这是更推荐的方式。
第3种方式效果最好,但如果碰到一些复杂的文档水印,就非常考验耐心。
你得一个个识别操作命令,一边替换一边检查效果,直到水印成功去除。
但,未必剩下的所有页都可以用同样特征模式来消除,因为这份PDF可能经过多人加水印,已经包含多种加水印方式。
所以,去水印并没有一种100%安全有效(不错删信息)且通用的方法。
加水印、去水印本质上是一种攻防策略。
比如一些工具推出去水印功能,一旦公开,加水印方就能识别并避开它的去除方法。
最后,尊重版权,是每个人应有的态度。
除了学习外,正式使用时,应该遵守内容创作方的规则。
PDF加密解密
PDF里的密码,分为用户密码和所有者密码。
PyPDF2里提供了基本的加密功能,“防君子不防小人”。
如果打开PDF文件后,复制了新文件,那新文件就不受所有者密码的约束,可被修改。
import pathlibfrom PyPDF2 import PdfFileReader, PdfFileWriterpath = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')f_path = path.joinpath('2020-新冠肺炎疫情对中国连锁餐饮行业的影响调研报告-中国连锁经营协会.pdf')out_path_encrypt = path.joinpath('002pdf_encrypt.pdf')out_path_decrypt = path.joinpath('002pdf_decrypt.pdf')with open(f_path, 'rb') as f, open(out_path_encrypt, 'wb') as f_out:pdf = PdfFileReader(f)pdf_out = PdfFileWriter()cnt_pages = pdf.getNumPages()for i in range(cnt_pages):page = pdf.getPage(i)pdf_out.addPage(page)pdf_out.encrypt('123456', owner_pwd='654321')pdf_out.write(f_out)# 重新读取加密文件并生成解密文件with open(out_path_encrypt, 'rb') as f, open(out_path_decrypt, 'wb') as f_out:pdf = PdfFileReader(f)if not pdf.isEncrypted:print('文件未被加密')else:success = pdf.decrypt('123456')# if not success:pdf_out = PdfFileWriter()pdf_out.appendPagesFromReader(pdf)pdf_out.write(f_out)
pdfminer.six
PDFMiner的操作门槛比较高,需要部分了解PDF的文档结构模型,适合定制开发复杂的内容处理工具。
平时直接用PDFMiner比较少,这里只演示基本的文档内容操作:
import pathlibfrom pdfminer.pdfparser import PDFParserfrom pdfminer.pdfdocument import PDFDocumentfrom pdfminer.pdfpage import PDFPagefrom pdfminer.pdfinterp import PDFResourceManagerfrom pdfminer.pdfinterp import PDFPageInterpreterfrom pdfminer.pdfdevice import PDFDevicefrom pdfminer.layout import LAParams, LTTextBox, LTFigure, LTImagefrom pdfminer.converter import PDFPageAggregatorpath = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')f_path = path.joinpath('2020-新冠肺炎疫情对中国连锁餐饮行业的影响调研报告-中国连锁经营协会.pdf')with open(f_path, 'rb') as f:parser = PDFParser(f)doc = PDFDocument(parser)rsrcmgr = PDFResourceManager()laparams = LAParams()device = PDFPageAggregator(rsrcmgr, laparams=laparams)interpreter = PDFPageInterpreter(rsrcmgr, device)for page in PDFPage.create_pages(doc):interpreter.process_page(page)layout = device.get_result()for x in layout:# 获取文本对象if isinstance(x, LTTextBox):print(x.get_text().strip())# 获取图片对象if isinstance(x,LTImage):print('这里获取到一张图片')# 获取 figure 对象if isinstance(x,LTFigure):print('这里获取到一个 figure 对象')
虽然pdfminer使用门槛较高,但遇到复杂情况,最后还得用它。目前开源模块中,它对PDF的支持应该是最全的了。
下面这个pdfplumber就是基于pdfminer.six开发的模块,降低了使用门槛。
pdfplumber
相比pdfminer.six,pdfplumber提供了更便捷的PDF内容抽取接口。
日常工作中常用的操作,比如:
- 提取PDF内容,保存到txt文件
- 提取PDF中的表格到Excel
- 提取PDF中的图片
- 提取PDF中的图表
提取PDF内容,保存到txt文件
import pathlibimport pdfplumberpath = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')f_path = path.joinpath('2020-新冠肺炎疫情对中国连锁餐饮行业的影响调研报告-中国连锁经营协会.pdf')out_path = path.joinpath('002pdf_out.txt')with pdfplumber.open(f_path) as pdf, open(out_path ,'a') as txt:for page in pdf.pages:textdata = page.extract_text()txt.write(textdata)
提取PDF中的表格到Excel
import pathlibimport pdfplumberfrom openpyxl import Workbookpath = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')f_path = path.joinpath('2020-新冠肺炎疫情对中国连锁餐饮行业的影响调研报告-中国连锁经营协会.pdf')out_path = path.joinpath('002pdf_excel.xlsx')wb = Workbook()sheet = wb.activewith pdfplumber.open(f_path) as pdf:for i in range(19, 22):page = pdf.pages[i]table = page.extract_table()for row in table:sheet.append(row)wb.save(out_path)
上面用到了openpyxl的功能创建了一个Excel文件,后面会有单独文章介绍它。
提取PDF中的图片
import pathlibimport pdfplumberfrom PIL import Imagepath = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')f_path = path.joinpath('2020-疫情影响下的中国社区趋势研究-艾瑞.pdf')out_path = path.joinpath('002pdf_images.png')with pdfplumber.open(f_path) as pdf, open(out_path, 'wb') as fout:page = pdf.pages[10]# for img in page.images:im = page.to_image()im.save(out_path, format='PNG')imgs = page.imagesfor i, img in enumerate(imgs):size = img['width'], img['height']data = img['stream'].get_data()out_path = path.joinpath(f'002pdf_images_{i}.png')with open(out_path, 'wb') as fimg_out:fimg_out.write(data)
上面用到了PIL(Pillow)的功能处理图片。
提取PDF中的图表
图表与图像不同,指的是类似直方图、饼图之类的数据生成图。
import pathlibimport pdfplumberfrom PIL import Imagepath = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')f_path = path.joinpath('2020-新冠肺炎疫情对中国连锁餐饮行业的影响调研报告-中国连锁经营协会.pdf')out_path = path.joinpath('002pdf_figures.png')with pdfplumber.open(f_path) as pdf, open(out_path, 'wb') as fout:page = pdf.pages[7]im = page.to_image()im.save(out_path, format='PNG')figures = page.figuresfor i, fig in enumerate(figures):size = fig['width'], fig['height']crop = page.crop((fig['x0'], fig['top'], fig['x1'], fig['bottom']))img_crop = crop.to_image()out_path = path.joinpath(f'002pdf_figures_{i}.png')img_crop.save(out_path, format='png')im.draw_rects(page.extract_words(), stroke='yellow')im.draw_rects(page.images, stroke='blue')im.draw_rects(page.figures)im # show in notebook
总结
本文介绍了PDF的常见使用场景,以及Python处理PDF的3个主要模块。
补充一点,PDF标准规范由Adobe公司主导。
平时我们不需要参考规范,但如果遇到一些较复杂的场景,尤其是模块没有直接支持,就只能硬着头皮翻阅文档了。文档是公开的,可以去搜索引擎搜索关键词:pdf_reference_1-7.pdf。
加入学习群

