PDF文件解析&拆分在SAP凭证打印场景中的运用(二)
小爬上篇文章分析了,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 from PyPDF2 import PdfFileReader, PdfFileWriter pdf_dict={} with pdfplumber.open("test.pdf") as pdf: total_page_num=len(pdf.pages) for i in range(total_page_num): print(i) p0 = pdf.pages[i] contents=p0.extract_text() voucherCode=re.search(".*?SAP凭证编号:([0-9]{10}).*?",contents,re.S).group(1) pageCode=re.search(".*?页码:(.*?)/.*?",contents,re.S).group(1).strip().rjust(3,"0") #部分凭证不止一页,如果仅仅基于凭证号命名,会重名 # print(voucherCode,pageCode) pdf_dict[i]=[voucherCode,pageCode] pdf = PdfFileReader("test.pdf") total=pdf.getNumPages() for i in range(pdf.getNumPages()): pdf_writer = PdfFileWriter() pdf_writer.addPage(pdf.getPage(i)) output = f'{pdf_dict[i][0]}_{pdf_dict[i][1]}.pdf' print(i,output) with open(output, 'wb') as output_pdf: pdf_writer.write(output_pdf)
亲测,每解析一页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文本解析效率不高的问题,赶紧用文中的方法试下,相信你会惊讶于它的简单、直接、高效。