zoukankan      html  css  js  c++  java
  • 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文本解析效率不高的问题,赶紧用文中的方法试下,相信你会惊讶于它的简单、直接、高效。

  • 相关阅读:
    程序员的私人外包专家
    目录
    Autumoon Code Library 2008 Beta版 重新发布
    为您的开发团队找个好管家
    .NET编程利器:Reflector for .NET
    3. Extension Methods(扩展方法)
    1. C# 3.0简介
    4. Lambda Expressions (Lambda表达式)与Expressions Tree(表达式树)
    7. Query Expressions(查询表达式)
    6. Anonymous Types(匿名类型)
  • 原文地址:https://www.cnblogs.com/new-june/p/13621268.html
Copyright © 2011-2022 走看看