geekdoc-python-zh/docs/pythonlibrary/exporting-data-from-pdfs-wi...

24 KiB
Raw Permalink Blame History

使用 Python 从 pdf 导出数据

原文:https://www.blog.pythonlibrary.org/2018/05/03/exporting-data-from-pdfs-with-python/

很多时候,您会希望从 PDF 中提取数据,并使用 Python 将其导出为不同的格式。不幸的是,没有很多 Python 包能很好地完成提取部分。在这一章中,我们将会看到各种不同的可以用来提取文本的包。我们还将学习如何从 pdf 中提取一些图像。虽然 Python 中没有针对这些任务的完整解决方案,但是您应该能够使用这里的信息来开始。一旦我们提取了我们想要的数据,我们还将研究如何获取这些数据并以不同的格式导出。

让我们从学习如何提取文本开始吧!


使用 PDFMiner 提取文本

可能最广为人知的是一个叫做 PDFMiner 的包。PDFMiner 包从 Python 2.4 开始就存在了。它的主要目的是从 PDF 中提取文本。事实上PDFMiner 可以告诉您文本在页面上的确切位置以及关于字体的父信息。对于 Python 2.4 - 2.7,您可以参考以下网站以获得关于 PDFMiner 的更多信息:

PDFMiner 与 Python 3 不兼容。幸运的是,有一个名为 PDFMiner.six 的 PDFMiner 分支,其工作原理完全相同。你可以在这里找到:【https://github.com/pdfminer/pdfminer.sixT2

| | 想了解更多关于使用 Python 处理 pdf 的信息吗?然后看看我的书:

ReportLab:使用 Python 处理 PDF

在 Leanpub 上立即购买 |

安装 PDFMiner 的说明已经过时了。您实际上可以使用 pip 来安装它:


python -m pip install pdfminer

如果您想为 Python 3 安装 PDFMiner(这可能是您应该做的),那么您必须像这样进行安装:


python -m pip install pdfminer.six

关于 PDFMiner 的文档充其量只能算是相当差的。你很可能需要使用 Google 和 StackOverflow 来弄清楚如何有效地使用本章没有涉及的 PDFMiner。

提取所有文本

有时您会想要提取 PDF 中的所有文本。PDFMiner 包提供了几种不同的方法来实现这一点。我们将首先看一些编程方法。让我们试着读出国内税收服务 W9 表格中的所有文本。你可以从这里得到一份:https://www.irs.gov/pub/irs-pdf/fw9.pdf

一旦您正确保存了 PDF我们就可以查看代码:


import io

from pdfminer.converter import TextConverter
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfpage import PDFPage

def extract_text_from_pdf(pdf_path):
    resource_manager = PDFResourceManager()
    fake_file_handle = io.StringIO()
    converter = TextConverter(resource_manager, fake_file_handle)
    page_interpreter = PDFPageInterpreter(resource_manager, converter)

    with open(pdf_path, 'rb') as fh:
        for page in PDFPage.get_pages(fh, 
                                      caching=True,
                                      check_extractable=True):
            page_interpreter.process_page(page)

        text = fake_file_handle.getvalue()

    # close open handles
    converter.close()
    fake_file_handle.close()

    if text:
        return text

if __name__ == '__main__':
    print(extract_text_from_pdf('w9.pdf'))

当您直接使用 PDFMiner 包时,它可能有点冗长。在这里,我们从 PDFMiner 的各个部分导入不同的片段。因为没有这些类的文档,也没有 docstrings所以我不会深入解释它们是做什么的。如果你真的很好奇的话可以自己钻研源代码。然而我认为我们可以跟着代码走。

我们做的第一件事是创建一个资源管理器实例。然后我们通过 Python 的 io 模块创建一个类似文件的对象。如果你使用的是 Python 2那么你会希望使用 StringIO 模块。我们的下一步是创建一个转换器。在本例中,我们选择了 TextConverter ,但是如果你愿意,你也可以使用 HTMLConverter 或者 XMLConverter 。最后,我们创建一个 PDF 解释器对象,它将接受我们的资源管理器和转换器对象并提取文本。

最后一步是打开 PDF 并循环浏览每一页。最后,我们获取所有文本,关闭各种处理程序,并将文本打印到 stdout。

逐页提取文本

坦白地说,从一个多页文档中抓取所有文本并不那么有用。通常,您会希望在文档的较小子集上工作。因此,让我们重写代码,以便它能够逐页提取文本。这将允许我们一页一页地检查文本:


# miner_text_generator.py

import io

from pdfminer.converter import TextConverter
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfpage import PDFPage

def extract_text_by_page(pdf_path):
    with open(pdf_path, 'rb') as fh:
        for page in PDFPage.get_pages(fh, 
                                      caching=True,
                                      check_extractable=True):
            resource_manager = PDFResourceManager()
            fake_file_handle = io.StringIO()
            converter = TextConverter(resource_manager, fake_file_handle)
            page_interpreter = PDFPageInterpreter(resource_manager, converter)
            page_interpreter.process_page(page)

            text = fake_file_handle.getvalue()
            yield text

            # close open handles
            converter.close()
            fake_file_handle.close()

def extract_text(pdf_path):
    for page in extract_text_by_page(pdf_path):
        print(page)
        print()

if __name__ == '__main__':
    print(extract_text('w9.pdf'))

在这个例子中,我们创建了一个生成器函数,生成每页的文本。 extract_text 函数打印出每一页的文本。在这里我们可以添加一些解析逻辑来解析出我们想要的东西。或者我们可以将文本(或 HTML 或 XML)保存为单独的文件,以便将来解析。

您会注意到文本可能没有按照您期望的顺序排列。所以你肯定需要找出解析出你感兴趣的文本的最佳方法。

PDFMiner 的好处在于,您已经可以将 PDF 导出为文本、HTML 或 XML。

如果您不想自己尝试找出 PDFMiner也可以使用 PDFMiner 的命令行工具, pdf2txt.pydumppdf.py 来为您完成导出。根据 pdf2txt.py 的源代码,可以用来将一个 pdf 导出为纯文本、html、xml 或者“标签”。


通过 pdf2txt.py 导出文本

默认情况下pdfMiner 附带的 pdf2txt.py 命令行工具将从 PDF 文件中提取文本并将其打印到 stdout。它不能识别图像文本因为 PDFMiner 不支持光学字符识别(OCR)。让我们试试使用它的最简单的方法,就是把路径传递给 PDF 文件。我们将使用 w9.pdf 的**。打开终端并导航到您保存该 PDF 的位置,或者修改下面的命令以指向该文件:**


pdf2txt.py w9.pdf

如果运行这个命令,它会将所有文本打印到 stdout。您也可以让 pdf2txt.py 将文本以文本、HTML、XML 或“标记 pdf”的形式写入文件。XML 格式将给出关于 PDF 的大部分信息,因为它包含文档中每个字母的位置以及字体信息。不推荐使用 HTML因为 pdf2txt 生成的标记往往很难看。以下是获得不同格式输出的方法:


pdf2txt.py -o w9.html w9.pdf 
pdf2txt.py -o w9.xml w9.pdf 

第一个命令将创建一个 HTML 文档,而第二个命令将创建一个 XML 文档。以下是我在进行 HTML 转换时得到的一张照片:

正如你所看到的,最终的结果看起来有点差,但还不算太差。它输出的 XML 非常冗长,所以我不能在这里全部复制。不过,这里有一个片段可以让你了解它的样子:

 <pages><page id="1" bbox="0.000,0.000,611.976,791.968" rotate="0"><textbox id="0" bbox="36.000,732.312,100.106,761.160"><textline bbox="36.000,732.312,100.106,761.160"><text font="JYMPLA+HelveticaNeueLTStd-Roman" bbox="36.000,736.334,40.018,744.496" size="8.162">F</text>
<text font="JYMPLA+HelveticaNeueLTStd-Roman" bbox="40.018,736.334,44.036,744.496" size="8.162">o</text>
<text font="JYMPLA+HelveticaNeueLTStd-Roman" bbox="44.036,736.334,46.367,744.496" size="8.162">r</text>
<text font="JYMPLA+HelveticaNeueLTStd-Roman" bbox="46.367,736.334,52.338,744.496" size="8.162">m</text>

<text font="ZWOHBU+HelveticaNeueLTStd-BlkCn" bbox="60.122,732.312,78.794,761.160" size="28.848">W</text>
<text font="ZWOHBU+HelveticaNeueLTStd-BlkCn" bbox="78.794,732.312,87.626,761.160" size="28.848">-</text>
<text font="ZWOHBU+HelveticaNeueLTStd-BlkCn" bbox="87.626,732.312,100.106,761.160" size="28.848">9</text></textline></textbox></page></pages>

使用 Slate 提取文本

Tim McNamara 不喜欢 PDFMiner 使用起来如此迟钝和困难,所以他写了一个名为 slate 的包装器,使得从 pdf 中提取文本更加容易。不幸的是,它似乎与 Python 3 不兼容。如果你想试一试,你可能需要有 easy_install 来安装 distribute 包,就像这样:


easy_install distribute

我无法让 pip 正确安装软件包。一旦安装完毕,您将能够使用 pip 来安装 slate:


python -m pip install slate

请注意,最新版本是 0.5.2pip 可能会也可能不会获取该版本。如果没有,那么您可以直接从 Github 安装 slate:


python -m pip install git+https://github.com/timClicks/slate

现在我们准备编写一些代码来从 PDF 中提取文本:


# slate_text_extraction.py

import slate

def extract_text_from_pdf(pdf_path):
    with open(pdf_path) as fh:
        document = slate.PDF(fh, password='', just_text=1)

    for page in document:
        print(page)

if __name__ == '__main__':
    extract_text_from_pdf('w9.pdf')

如您所见,要让 slate 解析 PDF您只需导入 slate然后创建其 PDF 类的一个实例。PDF 类实际上是 Python 的列表内置的一个子类,所以它只返回文本页面的列表/ iterable。您还会注意到如果 PDF 设置了密码,我们可以传入一个密码参数。无论如何,一旦文档被解析,我们只需打印出每页上的文本。

我真的很喜欢 slate 的易用性。不幸的是,几乎没有与这个包相关的文档。查看源代码后,似乎这个包只支持文本提取。


导出您的数据

现在我们有了一些要处理的文本,我们将花一些时间学习如何以各种不同的格式导出数据。具体来说,我们将学习如何通过以下方式导出文本:

  • 可扩展标记语言
  • JSON
  • 战斗支援车

我们开始吧!


导出到 XML

可扩展标记语言(XML)格式是最广为人知的输出和输入格式之一。它在互联网上被广泛用于许多不同的事情。正如我们在本章中已经看到的PDFMiner 也支持 XML 作为其输出之一。

不过,让我们创建自己的 XML 创建工具。这里有一个简单的例子:


# xml_exporter.py

import os
import xml.etree.ElementTree as xml

from miner_text_generator import extract_text_by_page
from xml.dom import minidom

def export_as_xml(pdf_path, xml_path):
    filename = os.path.splitext(os.path.basename(pdf_path))[0]
    root = xml.Element('{filename}'.format(filename=filename))
    pages = xml.Element('Pages')
    root.append(pages)

    counter = 1
    for page in extract_text_by_page(pdf_path):
        text = xml.SubElement(pages, 'Page_{}'.format(counter))
        text.text = page[0:100]
        counter += 1

    tree = xml.ElementTree(root)
    xml_string = xml.tostring(root, 'utf-8')
    parsed_string = minidom.parseString(xml_string)
    pretty_string = parsed_string.toprettyxml(indent='  ')

    with open(xml_path, 'w') as fh:
        fh.write(pretty_string)
    #tree.write(xml_path)

if __name__ == '__main__':
    pdf_path = 'w9.pdf'
    xml_path = 'w9.xml'
    export_as_xml(pdf_path, xml_path)

这个脚本将使用 Python 的内置 XML 库, minidomElementTree 。我们还导入了 PDFMiner 生成器脚本,用于一次抓取一页文本。在这个例子中,我们创建了顶层元素,它是 PDF 的文件名。然后我们在它下面添加一个页面元素。下一步是我们的 for 循环,我们从 PDF 中提取每一页并保存我们想要的信息。您可以在这里添加一个特殊的解析器,将页面分成句子或单词,解析出更多有趣的信息。例如,您可能只想要带有特定名称或日期/时间戳的句子。您可以使用 Python 的正则表达式来查找这些内容,或者只是检查句子中是否存在子字符串。

对于这个例子,我们只从每个页面中提取前 100 个字符,并将它们保存到一个 XML 子元素中。从技术上讲,下一段代码可以简化为只写出 XML。然而ElementTree 并没有对 XML 做任何处理以使其易于阅读。它看起来有点像缩小的 javascript因为它只是一个巨大的文本块。因此我们没有将文本块写入磁盘而是使用 minidom 在写出之前用空格“美化”XML。结果看起来像这样:



 <w9><pages><page_1>Form W-9(Rev. November 2017)Department of the Treasury Internal Revenue Service Request for Taxp</page_1>
<page_2>Form W-9 (Rev. 11-2017)Page 2 By signing the filled-out form, you: 1\. Certify that the TIN you are g</page_2>
<page_3>Form W-9 (Rev. 11-2017)Page 3 Criminal penalty for falsifying information. Willfully falsifying cert</page_3>
<page_4>Form W-9 (Rev. 11-2017)Page 4 The following chart shows types of payments that may be exempt from ba</page_4>
<page_5>Form W-9 (Rev. 11-2017)Page 5 1\. Interest, dividend, and barter exchange accounts opened before 1984</page_5>
<page_6>Form W-9 (Rev. 11-2017)Page 6 The IRS does not initiate contacts with taxpayers via emails. Also, th</page_6></pages></w9>

这是非常干净的 XML也很容易阅读。作为奖励您可以利用在 PyPDF2 章节中学到的知识,从 PDF 中提取元数据并添加到 XML 中。


导出到 JSON

JavaScript Object Notation 或 JSON 是一种易于读写的轻量级数据交换格式。Python 在其标准库中包含了一个 json 模块,允许您以编程方式读写 json。让我们利用上一节学到的知识创建一个输出 JSON 而不是 XML 的导出器脚本:


# json_exporter.py

import json
import os

from miner_text_generator import extract_text_by_page

def export_as_json(pdf_path, json_path):
    filename = os.path.splitext(os.path.basename(pdf_path))[0]
    data = {'Filename': filename}
    data['Pages'] = []

    counter = 1
    for page in extract_text_by_page(pdf_path):
        text = page[0:100]
        page = {'Page_{}'.format(counter): text}
        data['Pages'].append(page)
        counter += 1

    with open(json_path, 'w') as fh:
        json.dump(data, fh)

if __name__ == '__main__':
    pdf_path = 'w9.pdf'
    json_path = 'w9.json'
    export_as_json(pdf_path, json_path)

在这里,我们导入我们需要的各种库,包括我们的 PDFMiner 模块。然后我们创建一个接受 PDF 输入路径和 JSON 输出路径的函数。JSON 基本上是 Python 中的一个字典,所以我们创建了几个简单的顶级键:文件名页面。第页的键映射到一个空列表。接下来,我们遍历 PDF 的每一页,提取每页的前 100 个字符。然后,我们创建一个以页码为键、以 100 个字符为值的字典,并将其添加到顶级页面的列表中。最后,我们使用 json 模块的 dump 命令来编写文件。

文件的内容最终看起来像这样:


{'Filename': 'w9',
 'Pages': [{'Page_1': 'Form    W-9(Rev. November 2017)Department of the Treasury  Internal Revenue Service Request for Taxp'},
           {'Page_2': 'Form W-9 (Rev. 11-2017)Page 2 By signing the filled-out form, you: 1\. Certify that the TIN you are g'},
           {'Page_3': 'Form W-9 (Rev. 11-2017)Page 3 Criminal penalty for falsifying information. Willfully falsifying cert'},
           {'Page_4': 'Form W-9 (Rev. 11-2017)Page 4 The following chart shows types of payments that may be exempt from ba'},
           {'Page_5': 'Form W-9 (Rev. 11-2017)Page 5 1\. Interest, dividend, and barter exchange accounts opened before 1984'},
           {'Page_6': 'Form W-9 (Rev. 11-2017)Page 6 The IRS does not initiate contacts with taxpayers via emails. Also, th'}]}

同样,我们有一些易读的输出。如果您愿意,也可以用 PDF 的元数据来增强这个例子。请注意,输出会根据您希望从每个页面或文档中解析出的内容而变化。

现在,让我们快速了解一下如何导出到 CSV。


导出到 CSV

CSV 代表**逗号分隔值* *。这是一种非常标准的格式已经存在了很长时间。CSV 的好处在于 Microsoft Excel 和 LibreOffice 会自动在一个漂亮的电子表格中打开它们。如果您想查看原始值,也可以在文本编辑器中打开 CSV 文件。

Python 有一个内置的 csv 模块,可以用来读写 csv 文件。我们将在这里使用它从 PDF 中提取的文本创建一个 CSV。让我们来看看一些代码:


# csv_exporter.py

import csv
import os

from miner_text_generator import extract_text_by_page

def export_as_csv(pdf_path, csv_path):
    filename = os.path.splitext(os.path.basename(pdf_path))[0]

    counter = 1
    with open(csv_path, 'w') as csv_file:
        writer = csv.writer(csv_file)
        for page in extract_text_by_page(pdf_path):
            text = page[0:100]
            words = text.split()
            writer.writerow(words)

if __name__ == '__main__':
    pdf_path = 'w9.pdf'
    csv_path = 'w9.csv'
    export_as_csv(pdf_path, csv_path)

对于这个例子,我们导入 Python 的 csv 库。否则,导入与前面的示例相同。在我们的函数中,我们使用 CSV 文件路径创建了一个 CSV 文件处理程序。然后,我们初始化一个 CSV writer 对象,将该文件处理程序作为唯一的参数。接下来,我们像以前一样遍历 PDF 的页面。这里唯一的不同是,我们将前 100 个字符拆分成单独的单词。这允许我们将一些实际数据添加到 CSV 中。如果我们不这样做,那么每一行中只有一个元素,这就不是一个真正的 CSV 文件。最后,我们将单词列表写入 CSV 文件。

这是我得到的结果:


Form,W-9(Rev.,November,2017)Department,of,the,Treasury,Internal,Revenue,Service,Request,for,Taxp
Form,W-9,(Rev.,11-2017)Page,2,By,signing,the,filled-out,"form,",you:,1.,Certify,that,the,TIN,you,are,g
Form,W-9,(Rev.,11-2017)Page,3,Criminal,penalty,for,falsifying,information.,Willfully,falsifying,cert
Form,W-9,(Rev.,11-2017)Page,4,The,following,chart,shows,types,of,payments,that,may,be,exempt,from,ba
Form,W-9,(Rev.,11-2017)Page,5,1.,"Interest,","dividend,",and,barter,exchange,accounts,opened,before,1984
Form,W-9,(Rev.,11-2017)Page,6,The,IRS,does,not,initiate,contacts,with,taxpayers,via,emails.,"Also,",th

我认为这个例子比 JSON 或 XML 例子更难理解,但也不算太差。现在让我们继续,看看我们如何从 PDF 中提取图像。


从 pdf 中提取图像

不幸的是,没有 Python 包真正从 pdf 中提取图像。我找到的最接近的是一个名为 minecart 的项目,它声称可以做到,但只在 Python 2.7 上工作。我无法让它与我拥有的样本 pdf 一起工作。内德·巴奇尔德的博客上有一篇文章讲述了他是如何从 pdf 中提取 jpg 的。他的代码如下:


# Extract jpg's from pdf's. Quick and dirty.
import sys

pdf = file(sys.argv[1], "rb").read()

startmark = "\xff\xd8"
startfix = 0
endmark = "\xff\xd9"
endfix = 2
i = 0

njpg = 0
while True:
    istream = pdf.find("stream", i)
    if istream < 0:
        break
    istart = pdf.find(startmark, istream, istream+20)
    if istart < 0:
        i = istream+20
        continue
    iend = pdf.find("endstream", istart)
    if iend < 0:
        raise Exception("Didn't find end of stream!")
    iend = pdf.find(endmark, iend-20)
    if iend < 0:
        raise Exception("Didn't find end of JPG!")

    istart += startfix
    iend += endfix
    print("JPG %d from %d to %d" % (njpg, istart, iend))
    jpg = pdf[istart:iend]
    jpgfile = file("jpg%d.jpg" % njpg, "wb")
    jpgfile.write(jpg)
    jpgfile.close()

    njpg += 1
    i = iend

这也不适用于我正在使用的 pdf。有些人在评论中声称它对他们的一些 pdf 文件有效评论中也有一些更新代码的例子。StackOverflow 上有这段代码的变体,其中一些以某种方式使用了 PyPDF2。这些对我来说都不起作用。

我的建议是使用像 Poppler 这样的工具来提取图像。Poppler 有一个叫做 pdfimages 的工具,你可以和 Python 的子流程模块一起使用。以下是在没有 Python 的情况下如何使用它:


pdfimages -all reportlab-sample.pdf images/prefix-jpg

确保已经创建了图像文件夹(或您想要创建的任何输出文件夹),因为 pdfimages 不会为您创建它。

让我们编写一个 Python 脚本来执行这个命令,并确保输出文件夹也为您存在:


# image_exporter.py

import os
import subprocess

def image_exporter(pdf_path, output_dir):
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)

    cmd = ['pdfimages', '-all', pdf_path, 
           '{}/prefix'.format(output_dir)]
    subprocess.call(cmd)
    print('Images extracted:')
    print(os.listdir(output_dir))

if __name__ == '__main__':
    pdf_path = 'reportlab-sample.pdf'
    image_exporter(pdf_path, output_dir='images')

在这个例子中,我们导入了子进程操作系统模块。如果输出目录不存在,我们尝试创建它。然后我们使用子进程的调用方法来执行 pdfimages。我们使用调用,因为它将等待 pdfimages 完成运行。你可以使用 Popen 来代替,但是那将基本上在后台运行这个过程。最后,我们打印出输出目录的列表,以确认图像被提取到其中。

互联网上还有其他一些文章引用了一个名为 Wand 的库,您可能也想试试。它是一个 ImageMagick 包装器。同样值得注意的是,有一个名为 pypoppler 的 Python 绑定到 Poppler尽管我找不到该包进行图像提取的任何示例。


包扎

我们在这一章中讲述了许多不同的信息。您了解了几种不同的包,我们可以使用它们从 pdf(如 PDFMiner 或 Slate)中提取文本。我们还学习了如何使用 Python 的内置库将文本导出为 XML、JSON 和 CSV。最后我们看了从 pdf 导出图像的难题。虽然 Python 目前没有任何好的库来完成这项任务,但是您可以通过使用其他工具来解决这个问题,比如 Poppler 的 pdfimage 实用程序。


相关阅读