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('
')
        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文本解析效率不高的问题,赶紧用文中的方法试下,相信你会惊讶于它的简单、直接、高效。

原文地址:https://www.cnblogs.com/new-june/p/13621268.html