Python模块用于将PDF转换为文本。

417

有没有Python模块可以将PDF文件转换成文本?我尝试了在Activestate中找到的一段代码,它使用了pypdf,但生成的文本没有空格,也没有用处。


3
我正在寻找类似的解决方案。我只需要读取 PDF 文件中的文本,不需要图片。pdfminer 是一个不错的选择,但是我没有找到一个简单的例子来提取文本。最终我找到了这个 Stack Overflow 回答(https://dev59.com/fW025IYBdhLWcg3w6aUX#8325135),现在我正在使用它。 - Nayan
8
由于这个问题被关闭了,我将其重新发布到专门用于软件推荐的 Stack Exchange 上,以防有人想要撰写新答案:用于将 PDF 转换为文本的 Python 模块 - Franck Dernoncourt
1
对于UTF-8内容,我找到的唯一解决方案是Apache Tika。 - Shoham
我想要更新Python中PDF转文本转换的可用选项列表,GroupDocs.Conversion Cloud SDK for Python可以准确地将PDF转换为文本。 - Tilal Ahmad
PyPDF2 在文本提取方面有了很大的改进!再试一次吧 :-) - Martin Thoma
显示剩余2条评论
13个回答

159

尝试使用PDFMiner。它可以将PDF文件中的文本提取为HTML、SGML或“标记式PDF”格式。

标记式PDF格式似乎是最干净的格式,去除XML标签后只剩下纯文本。

Python 3版本可在以下链接中获取:


2
我刚刚添加了一个答案,描述如何将pdfminer用作库。 - codeape
1
我在这个帖子中提供的答案可能对那些想知道如何使用PDFMiner库从PDF中提取文本的人有用。我给出了一个使用PDFMiner库提取PDF文本的示例。由于文档有点稀疏,我认为这可能会帮助一些人。 - RattleyCooper
你能帮我想办法将PDF转换成标记PDF格式吗? - user199354
1
请查看以下网址中的示例代码:https://dev59.com/UF8d5IYBdhLWcg3wvkSN#26495057 - Renaud
2
不幸的是,pdfminer并不快速,特别是当您要用它处理超过100页的长PDF文档时。 - Pedram
显示剩余2条评论

144

PDFMiner软件包自codeape发布以来已经发生了变化。

编辑(再次更新):

PDFMiner在20100213版本中进行了更新。

您可以使用以下命令检查您所安装的版本:

>>> import pdfminer
>>> pdfminer.__version__
'20100213'

以下是更新版本(包含我所更改/添加的注释):

这是更新版本(附有我所更改/添加的注释):

def pdf_to_csv(filename):
    from cStringIO import StringIO  #<-- added so you can copy/paste this to try it
    from pdfminer.converter import LTTextItem, TextConverter
    from pdfminer.pdfparser import PDFDocument, PDFParser
    from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter

    class CsvConverter(TextConverter):
        def __init__(self, *args, **kwargs):
            TextConverter.__init__(self, *args, **kwargs)

        def end_page(self, i):
            from collections import defaultdict
            lines = defaultdict(lambda : {})
            for child in self.cur_item.objs:
                if isinstance(child, LTTextItem):
                    (_,_,x,y) = child.bbox                   #<-- changed
                    line = lines[int(-y)]
                    line[x] = child.text.encode(self.codec)  #<-- changed

            for y in sorted(lines.keys()):
                line = lines[y]
                self.outfp.write(";".join(line[x] for x in sorted(line.keys())))
                self.outfp.write("\n")

    # ... the following part of the code is a remix of the 
    # convert() function in the pdfminer/tools/pdf2text module
    rsrc = PDFResourceManager()
    outfp = StringIO()
    device = CsvConverter(rsrc, outfp, codec="utf-8")  #<-- changed 
        # becuase my test documents are utf-8 (note: utf-8 is the default codec)

    doc = PDFDocument()
    fp = open(filename, 'rb')
    parser = PDFParser(fp)       #<-- changed
    parser.set_document(doc)     #<-- added
    doc.set_parser(parser)       #<-- added
    doc.initialize('')

    interpreter = PDFPageInterpreter(rsrc, device)

    for i, page in enumerate(doc.get_pages()):
        outfp.write("START PAGE %d\n" % i)
        interpreter.process_page(page)
        outfp.write("END PAGE %d\n" % i)

    device.close()
    fp.close()

    return outfp.getvalue()

再次编辑:

这是最新版本(pypi中的20100619p1)的更新。简而言之,我用LTChar替换了LTTextItem,并向CsvConverter构造函数传递了LAParams的实例。

def pdf_to_csv(filename):
    from cStringIO import StringIO  
    from pdfminer.converter import LTChar, TextConverter    #<-- changed
    from pdfminer.layout import LAParams
    from pdfminer.pdfparser import PDFDocument, PDFParser
    from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter

    class CsvConverter(TextConverter):
        def __init__(self, *args, **kwargs):
            TextConverter.__init__(self, *args, **kwargs)

        def end_page(self, i):
            from collections import defaultdict
            lines = defaultdict(lambda : {})
            for child in self.cur_item.objs:
                if isinstance(child, LTChar):               #<-- changed
                    (_,_,x,y) = child.bbox                   
                    line = lines[int(-y)]
                    line[x] = child.text.encode(self.codec)

            for y in sorted(lines.keys()):
                line = lines[y]
                self.outfp.write(";".join(line[x] for x in sorted(line.keys())))
                self.outfp.write("\n")

    # ... the following part of the code is a remix of the 
    # convert() function in the pdfminer/tools/pdf2text module
    rsrc = PDFResourceManager()
    outfp = StringIO()
    device = CsvConverter(rsrc, outfp, codec="utf-8", laparams=LAParams())  #<-- changed
        # becuase my test documents are utf-8 (note: utf-8 is the default codec)

    doc = PDFDocument()
    fp = open(filename, 'rb')
    parser = PDFParser(fp)       
    parser.set_document(doc)     
    doc.set_parser(parser)       
    doc.initialize('')

    interpreter = PDFPageInterpreter(rsrc, device)

    for i, page in enumerate(doc.get_pages()):
        outfp.write("START PAGE %d\n" % i)
        if page is not None:
            interpreter.process_page(page)
        outfp.write("END PAGE %d\n" % i)

    device.close()
    fp.close()

    return outfp.getvalue()

编辑(再次更新):

针对版本20110515进行了更新(感谢Oeufcoque Penteano!):

def pdf_to_csv(filename):
    from cStringIO import StringIO  
    from pdfminer.converter import LTChar, TextConverter
    from pdfminer.layout import LAParams
    from pdfminer.pdfparser import PDFDocument, PDFParser
    from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter

    class CsvConverter(TextConverter):
        def __init__(self, *args, **kwargs):
            TextConverter.__init__(self, *args, **kwargs)

        def end_page(self, i):
            from collections import defaultdict
            lines = defaultdict(lambda : {})
            for child in self.cur_item._objs:                #<-- changed
                if isinstance(child, LTChar):
                    (_,_,x,y) = child.bbox                   
                    line = lines[int(-y)]
                    line[x] = child._text.encode(self.codec) #<-- changed

            for y in sorted(lines.keys()):
                line = lines[y]
                self.outfp.write(";".join(line[x] for x in sorted(line.keys())))
                self.outfp.write("\n")

    # ... the following part of the code is a remix of the 
    # convert() function in the pdfminer/tools/pdf2text module
    rsrc = PDFResourceManager()
    outfp = StringIO()
    device = CsvConverter(rsrc, outfp, codec="utf-8", laparams=LAParams())
        # becuase my test documents are utf-8 (note: utf-8 is the default codec)

    doc = PDFDocument()
    fp = open(filename, 'rb')
    parser = PDFParser(fp)       
    parser.set_document(doc)     
    doc.set_parser(parser)       
    doc.initialize('')

    interpreter = PDFPageInterpreter(rsrc, device)

    for i, page in enumerate(doc.get_pages()):
        outfp.write("START PAGE %d\n" % i)
        if page is not None:
            interpreter.process_page(page)
        outfp.write("END PAGE %d\n" % i)

    device.close()
    fp.close()

    return outfp.getvalue()

1
在[6]中:导入pdfminer在[7]中:pdfminer.version Out[7]: '20100424'在[8]中:从pdfminer.converter导入LTTextItemImportError: 无法导入名称LTTextItem ....LITERALS_DCT_DECODE LTChar LTImage LTPolygon LTTextBox LITERAL_DEVICE_GRAY LTContainer LTLine LTRect LTTextGroup LITERAL_DEVICE_RGB LTFigure LTPage LTText LTTextLine - Skylar Saveland
请参见http://www.unixuser.org/~euske/python/pdfminer/programming.html。 - Oeufcoque Penteano
2
@Oeufcoque Penteano,谢谢!我根据你的评论为版本“20110515”添加了另一个部分到答案中。 - tgray
1
@user3272884所给出的答案在2014年5月1日仍然有效。 - jmunsch
1
今天我也遇到了同样的问题,稍微修改了tgray的代码以提取有关空格的信息,并在这里发布了它。 - tarikki
显示剩余7条评论

77

由于这些解决方案都不支持最新版本的PDFMiner,我编写了一个简单的解决方案,使用PDFMiner返回PDF文本。对于那些使用process_pdf时遇到导入错误的人来说,这将起作用。

import sys
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.pdfpage import PDFPage
from pdfminer.converter import XMLConverter, HTMLConverter, TextConverter
from pdfminer.layout import LAParams
from cStringIO import StringIO

def pdfparser(data):

    fp = file(data, 'rb')
    rsrcmgr = PDFResourceManager()
    retstr = StringIO()
    codec = 'utf-8'
    laparams = LAParams()
    device = TextConverter(rsrcmgr, retstr, codec=codec, laparams=laparams)
    # Create a PDF interpreter object.
    interpreter = PDFPageInterpreter(rsrcmgr, device)
    # Process each page contained in the document.

    for page in PDFPage.get_pages(fp):
        interpreter.process_page(page)
        data =  retstr.getvalue()

    print data

if __name__ == '__main__':
    pdfparser(sys.argv[1])  

请见下面适用于Python 3的代码:

import sys
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.pdfpage import PDFPage
from pdfminer.converter import XMLConverter, HTMLConverter, TextConverter
from pdfminer.layout import LAParams
import io

def pdfparser(data):

    fp = open(data, 'rb')
    rsrcmgr = PDFResourceManager()
    retstr = io.StringIO()
    codec = 'utf-8'
    laparams = LAParams()
    device = TextConverter(rsrcmgr, retstr, codec=codec, laparams=laparams)
    # Create a PDF interpreter object.
    interpreter = PDFPageInterpreter(rsrcmgr, device)
    # Process each page contained in the document.

    for page in PDFPage.get_pages(fp):
        interpreter.process_page(page)
        data =  retstr.getvalue()

    print(data)

if __name__ == '__main__':
    pdfparser(sys.argv[1])  

3
这是我找到的第一个可以处理奇怪的PDF文件的片段(特别是从packtpub获得的免费电子书)。其他所有代码只返回奇怪编码的原始内容,但你的代码实际上返回了文本。谢谢! - somada141
1
在获取数据后,您可能希望执行retstr.seek(0),否则您将累积所有页面的文本。 - Tshirtman
3
要在Python3中使用,除了在print命令后加上明显的括号外,还需将file命令替换为open命令,并从io包中导入StringIO - McLawrence
2
哇,第一次复制这个代码块就完美运行了。太神奇了!现在可以开始解析和修复数据,而不必为输入数据而感到紧张了。 - SecsAndCyber
1
pdfminer不支持Python3。这段代码不适用于pdfminer3k。 - thang
显示剩余4条评论

53

Pdftotext 是一款开源程序(Xpdf 的一部分),可以从 Python 中调用(这不是您要求的,但可能很有用)。我使用它没有遇到问题。我认为谷歌在 Google 桌面中也使用了它。


8
这里列出的工具中,这似乎是最有用的一个,它有“-layout”选项,可以保持文本在PDF中的位置不变。现在如果我能弄清楚如何将PDF内容传递给它就好了。 - Matthew Schinckel
经过测试多种解决方案后,这个似乎是最简单和最强大的选择。可以使用Python轻松包装它,使用tempfile来指定输出写入的位置。 - Cerin
1
Cerin,使用“-”作为文件名将输出重定向到标准输出。这样你就可以使用简单的subprocess.check_output,并且这个调用会感觉像一个内部函数。 - Ctrl-C
只是为了强调使用它的人... pdftotext似乎工作得非常好,但如果希望在标准输出上看到结果,则需要第二个参数是连字符。 - Gordon Linoff
3
这将递归地转换所有以当前文件夹为起点的PDF文件:find . -iname "*.pdf" -exec pdftotext -enc UTF-8 -eol unix -raw {} \; 默认情况下,生成的文件将采用原始名称,并带有.txt扩展名。 - ccpizza
显示剩余2条评论

45

pyPDF在使用格式正确的PDF文档时表现良好。如果你只要获取文本(包括空格),你可以这样做:

import pyPdf
pdf = pyPdf.PdfFileReader(open(filename, "rb"))
for page in pdf.pages:
    print page.extractText()

您还可以轻松地访问元数据、图像数据等。

在extractText代码的注释中:

定位所有文本绘制命令,并按照内容流中提供的顺序提取文本。这对于某些PDF文件效果很好,但对其他文件效果很差,具体取决于所使用的生成器。这将在未来得到改进。请不要依赖此函数输出的文本顺序,因为如果该函数变得更加复杂,其输出顺序也会发生变化。

这是否是一个问题,取决于您对文本的处理方式(例如,如果文本顺序无关紧要,则没有问题,或者如果生成器按照将要显示的顺序向流中添加文本,则没有问题)。我每天都在使用pyPdf提取代码,没有任何问题。


9
不支持Unicode :( - PanosJee
8
pyPdf现在支持UTF了。 - lbolla
2
这个库看起来像垃圾。在随机的PDF上测试时,出现了错误“pyPdf.utils.PdfReadError:未找到EOF标记”。 - Cerin
4
从问题中得知:生成的文本没有空格,因此毫无用处。我使用了pyPDF并得到了相同的结果——提取的文本单词之间没有空格。 - Jordan Reiter
1
建议根据SO帖子[https://dev59.com/gFgQ5IYBdhLWcg3w1njm]使用pyPDF2。 - shyam
显示剩余7条评论

21
你也可以很容易地将pdfminer用作库。您可以访问pdf的内容模型,并可以创建自己的文本提取。我使用下面的代码将pdf内容转换为分号分隔的文本。
该函数根据其y和x坐标简单地对TextItem内容对象进行排序,并输出具有相同y坐标的项作为一行文本,使用';'字符分隔同一行上的对象。
使用这种方法,我能够从其他工具无法提取适合进一步解析的内容的pdf中提取文本。 我尝试过的其他工具包括pdftotext,ps2ascii和在线工具pdftextonline.com。 pdfminer是进行pdf-scraping的宝贵工具。

def pdf_to_csv(filename):
    from pdflib.page import TextItem, TextConverter
    from pdflib.pdfparser import PDFDocument, PDFParser
    from pdflib.pdfinterp import PDFResourceManager, PDFPageInterpreter

    class CsvConverter(TextConverter):
        def __init__(self, *args, **kwargs):
            TextConverter.__init__(self, *args, **kwargs)

        def end_page(self, i):
            from collections import defaultdict
            lines = defaultdict(lambda : {})
            for child in self.cur_item.objs:
                if isinstance(child, TextItem):
                    (_,_,x,y) = child.bbox
                    line = lines[int(-y)]
                    line[x] = child.text

            for y in sorted(lines.keys()):
                line = lines[y]
                self.outfp.write(";".join(line[x] for x in sorted(line.keys())))
                self.outfp.write("\n")

    # ... the following part of the code is a remix of the 
    # convert() function in the pdfminer/tools/pdf2text module
    rsrc = PDFResourceManager()
    outfp = StringIO()
    device = CsvConverter(rsrc, outfp, "ascii")

    doc = PDFDocument()
    fp = open(filename, 'rb')
    parser = PDFParser(doc, fp)
    doc.initialize('')

    interpreter = PDFPageInterpreter(rsrc, device)

    for i, page in enumerate(doc.get_pages()):
        outfp.write("START PAGE %d\n" % i)
        interpreter.process_page(page)
        outfp.write("END PAGE %d\n" % i)

    device.close()
    fp.close()

    return outfp.getvalue()

更新:

以上代码是针对旧版本API编写的,请见下方我的评论。


你需要哪些插件才能使它工作?我下载并安装了pdfminer,但这还不够... - kxk
1
以上代码是针对旧版本的PDFminer编写的。API在更近期的版本中已经发生了变化(例如,包现在是“pdfminer”,而不是“pdflib”)。我建议您查看PDFminer源代码中的“pdf2txt.py”的源代码,上述代码是受该文件旧版本的启发编写的。 - codeape

17

slate 是一个项目,使得通过库很容易使用 PDFMiner:

>>> with open('example.pdf') as f:
...    doc = slate.PDF(f)
...
>>> doc
[..., ..., ...]
>>> doc[1]
'Text from page 2...'   

1
当执行"import slate"时,我遇到了导入错误: {File "C:\Python33\lib\site-packages\slate-0.3-py3.3.egg\slate_init_.py", line 48, in <module> ImportError: cannot import name PDF} 但是PDF类确实存在!你知道如何解决吗? - juankysmith
不,这听起来非常奇怪。你有依赖关系吗? - Tim McNamara
通常我会收到有关缺失依赖项的消息,但在这种情况下,我收到了经典的消息“import slate File"C:\ Python33 \ lib \ site-packages \ slate-0.3-py3.3.egg \ slate \ __ init__ .py ",第48行,在<module>中: ImportError:无法导入名称PDF” - juankysmith
Slate 0.3需要pdfminer 20110515,根据这个GitHub问题 - jabbett
7
此软件包已不再维护,请勿使用。甚至在Python 3.5中也无法使用。 - Siva Arunachalam

9

我需要在Python模块中将特定的PDF转换为纯文本。我使用了PDFMiner 20110515版本,在阅读了他们的pdf2txt.py工具后,我编写了这个简单的代码片段:

from cStringIO import StringIO
from pdfminer.pdfinterp import PDFResourceManager, process_pdf
from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams

def to_txt(pdf_path):
    input_ = file(pdf_path, 'rb')
    output = StringIO()

    manager = PDFResourceManager()
    converter = TextConverter(manager, output, laparams=LAParams())
    process_pdf(manager, converter, input_)

    return output.getvalue() 

1
def to_txt(pdf_path): - Cătălin George Feștilă
如果我只想转换特定数量的页面,我该如何使用这段代码实现? - psychok7
@psychok7 你尝试过使用pdf2txt工具吗?它似乎在当前版本中支持-p标志的功能,实现起来很容易跟随,并且也应该很容易自定义:https://github.com/euske/pdfminer/blob/master/tools/pdf2txt.py 希望能帮到你! :) - gonz
1
谢谢 @gonz,我尝试了以上所有方法,但你的解决方案对我来说非常完美,输出带有空格 :) - lazarus
pdf2txt.py已经在我的这个位置安装好了:C:\Python27\Scripts\pdfminer\tools\pdf2txt.py - Nate Anderson

6

利用pdfminer自带的pdf2txt.py代码,您可以创建一个函数,该函数将接受pdf路径、可选的输出类型(txt|html|xml|tag)和选项,就像命令行pdf2txt一样{'-o': '/path/to/outfile.txt' ...}。默认情况下,您可以调用:

convert_pdf(path)

将创建一个文本文件,该文件将作为原始pdf文件在文件系统中的同级。
def convert_pdf(path, outtype='txt', opts={}):
    import sys
    from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter, process_pdf
    from pdfminer.converter import XMLConverter, HTMLConverter, TextConverter, TagExtractor
    from pdfminer.layout import LAParams
    from pdfminer.pdfparser import PDFDocument, PDFParser
    from pdfminer.pdfdevice import PDFDevice
    from pdfminer.cmapdb import CMapDB

    outfile = path[:-3] + outtype
    outdir = '/'.join(path.split('/')[:-1])

    debug = 0
    # input option
    password = ''
    pagenos = set()
    maxpages = 0
    # output option
    codec = 'utf-8'
    pageno = 1
    scale = 1
    showpageno = True
    laparams = LAParams()
    for (k, v) in opts:
        if k == '-d': debug += 1
        elif k == '-p': pagenos.update( int(x)-1 for x in v.split(',') )
        elif k == '-m': maxpages = int(v)
        elif k == '-P': password = v
        elif k == '-o': outfile = v
        elif k == '-n': laparams = None
        elif k == '-A': laparams.all_texts = True
        elif k == '-D': laparams.writing_mode = v
        elif k == '-M': laparams.char_margin = float(v)
        elif k == '-L': laparams.line_margin = float(v)
        elif k == '-W': laparams.word_margin = float(v)
        elif k == '-O': outdir = v
        elif k == '-t': outtype = v
        elif k == '-c': codec = v
        elif k == '-s': scale = float(v)
    #
    CMapDB.debug = debug
    PDFResourceManager.debug = debug
    PDFDocument.debug = debug
    PDFParser.debug = debug
    PDFPageInterpreter.debug = debug
    PDFDevice.debug = debug
    #
    rsrcmgr = PDFResourceManager()
    if not outtype:
        outtype = 'txt'
        if outfile:
            if outfile.endswith('.htm') or outfile.endswith('.html'):
                outtype = 'html'
            elif outfile.endswith('.xml'):
                outtype = 'xml'
            elif outfile.endswith('.tag'):
                outtype = 'tag'
    if outfile:
        outfp = file(outfile, 'w')
    else:
        outfp = sys.stdout
    if outtype == 'txt':
        device = TextConverter(rsrcmgr, outfp, codec=codec, laparams=laparams)
    elif outtype == 'xml':
        device = XMLConverter(rsrcmgr, outfp, codec=codec, laparams=laparams, outdir=outdir)
    elif outtype == 'html':
        device = HTMLConverter(rsrcmgr, outfp, codec=codec, scale=scale, laparams=laparams, outdir=outdir)
    elif outtype == 'tag':
        device = TagExtractor(rsrcmgr, outfp, codec=codec)
    else:
        return usage()

    fp = file(path, 'rb')
    process_pdf(rsrcmgr, device, fp, pagenos, maxpages=maxpages, password=password)
    fp.close()
    device.close()

    outfp.close()
    return

1

我使用了pdftohtml命令并带有-xml参数,通过subprocess.Popen()读取结果,这将给出pdf中每个文本片段的x坐标、y坐标、宽度、高度和字体。我认为这也是'evince'可能使用的方法,因为相同的错误消息会输出。

如果您需要处理列数据,则情况会略微复杂,因为您必须发明适合pdf文件的算法。问题在于生成PDF文件的程序通常不以任何逻辑格式布置文本。您可以尝试简单的排序算法,并且有时可以奏效,但可能会有一些文本部分没有按照您想象的那样排序。因此,您需要变得更有创造力。

对于我正在处理的pdf,花费了我大约5个小时才找到一个适合的算法。但现在它运行得非常好。祝好运。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接