小爬上篇文章分析了,SAP凭证批量打印场景中为啥要用到PDF文件解析&拆分。这篇文章,紧接着上一篇,重点谈谈如何用python来做到高效的PDF文件解析&拆分。
小爬使用了python第三方库PyPDF2,它可以轻松的处理pdf文件,它提供了读、写、分割、合并、文件转换等多种操作。小爬试了下,PyPDF2分割和合并的工作能轻松搞定,但是提取文本这块,它只擅长英文。如果PDF内容涉及大量中文,则PYPDF2提取到的文本是大量的乱码。
StackOverflow上热心的程序员推荐了pdfminer,或者tika-python,可惜tika-python底层是用java实现的,它要求电脑上至少安装有Java7的开发环境,所以它不在我的考虑范围。小爬试了下pdfminer以及很多人推荐的pdfplumber库,下面这段代码,讲述了如何通过PYPDF2+pdfplumber库,以及RE正则表达式完成pdf文本的解析,得到PDF文本中的 “SAP凭证编号” 以及“页码”,直至生成新的pdf文件:
import pdfplumber,re
亲测,每解析一页PDF内容,需要0.8秒~1秒。轻度使用自然是问题不大,小爬也乐于推荐这种方法。不过当我们的PDF有几百上千页,且我们有多个这样的PDF文件时,我们难免会担心它的解析效率。
为了进一步提升PDF文本解析的效率,小爬尝试了各类python-pdf解析库,最终功夫不负有心人,找到了心仪的解决方案——XpdfReader,官网:https://www.xpdfreader.com/。
亲测,它的核心产品 XpdfReader 提供了各大系统版本下的安装包,读取PDF文件效率极高,要好过市面上的福昕PDF阅读器和adobe reader,不过功能相对简单。小爬这里要用到的是它提供的命令行工具:
pdftotext.exe。为了能够读取多种语言,我们还需要对应的语言包,比如小爬的xpdf文件夹结构如下:
感兴趣的童鞋可以上官网下载对应文件。准备好这些后,我们就可以开始提取文本了,具体见下面的代码示例:
import os,subprocess,time,re,glob
import warnings
from os.path import isfile,join
from PyPDF2 import PdfFileReader, PdfFileWriter,PdfFileMerger
warnings.filterwarnings('ignore') # 关掉控制台的大量pdfFileReader的warning,没有这句也不影响程序执行
start=time.perf_counter()
base_dir=os.path.dirname(os.path.abspath(__file__))
ef=join(base_dir,"xpdf/pdftotext.exe")
cfg=join(base_dir,"xpdf/xpdfrc")
files=[]
voucher_codes=[]
pdf = PdfFileReader("test.pdf", 'rb')
total=pdf.getNumPages()
for i in range(pdf.getNumPages()):
pdf_writer = PdfFileWriter()
pdf_writer.addPage(pdf.getPage(i))
output = f'result_{i+1}.pdf'
print(i,output)
with open(output, 'wb') as output_pdf:
pdf_writer.write(output_pdf)
files.append(join(base_dir,output))
def convert(file):
bo = subprocess.check_output([ef,'-f','1','-l','1','-cfg',cfg,'-raw',file,'-']) #这个命令中的所有调用文件参数必须使用full path.否则调用出错。
return bo.decode('utf-8')
for index,file in enumerate(files):
print(index+1)
bo=convert(file)
if len(bo)!=0:
contents=bo.split('\r\n')
for content in contents:
if "SAP凭证编号" in content:
voucher_code=re.search(".*?SAP凭证编号:([0-9]{10}).*?",content).group(1)
if voucher_code not in voucher_codes:
voucher_codes.append(voucher_code)
if "页码:" in content:
pageCode=re.search(".*?页码:(.*?)/.*?",content).group(1).strip().rjust(3,"0")
os.rename(file,join(base_dir,"results",f"{voucher_code}_{pageCode}.pdf"))
print(voucher_code,pageCode)
openFiles=[]
for index,voucher_code in enumerate(voucher_codes):
files=sorted(glob.glob(join(base_dir,"results",f"{voucher_code}*.pdf")))
pdf_merger = PdfFileMerger()
for file in files:
openFile=open(file, 'rb')
pdf_merger.append(openFile)
openFiles.append(openFile)
with open(join(base_dir,"results",f"final_{voucher_code}.pdf"), 'wb') as fout:
pdf_merger.write(fout)
for openfile in openFiles:
openfile.close() # 对打开的文件,逐一关闭,后续进行移除。如果不关闭,后续无法使用remove方法删除文件
files=sorted(glob.glob(join(base_dir,"results",f"*.pdf")))
for file in files:
if "final" not in file:
os.remove(file)
end=time.perf_counter()
totalTime=round(end-start,2)
print(f"total time:{totalTime} seconds.")
这段代码的核心就是自定义方法 convert,该方法很简单,利用subprocess库发送命令行:按照 pdftotext.exe的要求,传递相关参数即可。亲测,该方法提取pdf文本效率极高,大概0.1秒就可以提取一页PDF内容。
这段代码中还有一点需要强调,当我们用PdfFileMerger()方法时,需要打开大量的PDF对象,我们这个合并完成后,这些打开的PDF对象不会自行关掉,这会导致我们没法用remove方法删除这些PDF文件(假设merge完pdf后,我们不再需要一开始的这些pdf了),这里小爬把这些打开的openFile放到Openfiles池子里(list对象),最后统一调用close()方法后,再进行remove。
如果你遇到过类似的PDF文本解析效率不高的问题,赶紧用文中的方法试下,相信你会惊讶于它的简单、直接、高效。