三大神器助力Python提取pdf文档信息

今年最后一篇技术文章来袭了。。。

今天这篇文章是今年最后一篇文章了,因此也是一篇非常有用的技术文章,你可以现在只了解一下并进行收藏,等你需要用到的时候再拿出来看一看,这样就好了。

这个是我上个月接的一个私活,帮一个人读取PDF里面的信息,特别是含有很多表格的PDF。以前我进行文章识别的时候都是使用OCR。现在这个用不了,因为里面的表格数据太多了,而且每个表格的样式又是不一样,所以真正做到完全识别是需要花费很多时间,而且光一篇文章是讲不完的,因此我这里也只是挑重要的介绍,能识别大部分的表格,并以JSON格式将识别结果进行返回。

在识别过程中,我使用了很多第三库,但是由于本文篇幅限制,我就简单介绍pdfminer和pdfplumber,着重介绍camelot。通过介绍你可以有目的性的选择自己需要的库。注意我使用的Python版本为3.6。

首先介绍pdfminer。pdminer是一个从PDF文档中提取信息的工具。与其他pdf相关的工具不同,它完全专注于获取和分析文本数据。PDFMiner允许获取页面中文本的确切位置,以及其他信息,比如字体或行。它包括一 个PDF转换器,可以将PDF文件转换成其他文本格式(如HTML)。 它有一个可扩展的PDF解析器,可以用于其他目的而不是文本分析。所以说它的功能还是非常强大的。官方文档:

http://www.unixuser.org/~euske/python/pdfminer/index.html

由于pdfminer存在python2和python3的版本,而我们需要的是python3的版本,因此相应的安装命令为:

pip install pdfminer3k

在使用过程中,可能需要安装其他的依赖包,这个可以使用Alt+Enter组合键进行导入安装。下面将演示如何使用它。首先我们需要识别这张图片上的所有文字,并以原来所在的行进行返回:

相关的代码如下:

 1import sys
 2import importlib
 3importlib.reload(sys)
 4
 5from pdfminer.pdfparser import PDFParser
 6from pdfminer.pdfdocument import PDFDocument
 7from pdfminer.pdfpage import PDFPage
 8from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
 9from pdfminer.converter import PDFPageAggregator
10from pdfminer.layout import LTTextBoxHorizontal,LAParams
11from pdfminer.pdfpage import PDFTextExtractionNotAllowed
12
13
14# 对本地保存的pdf文件进行读取和写入到txt文件当中
15
16
17# 定义解析函数
18def pdftotxt(path,new_name):
19    # 创建一个文档分析器
20    parser = PDFParser(path)
21    # 创建一个PDF文档对象存储文档结构
22    document =PDFDocument(parser)
23    # 判断文件是否允许文本提取
24    if not document.is_extractable:
25        raise PDFTextExtractionNotAllowed
26    else:
27        # 创建一个PDF资源管理器对象来存储资源
28        resmag =PDFResourceManager()
29        # 设定参数进行分析
30        laparams =LAParams()
31        # 创建一个PDF设备对象
32        # device=PDFDevice(resmag)
33        device =PDFPageAggregator(resmag,laparams=laparams)
34        # 创建一个PDF解释器对象
35        interpreter = PDFPageInterpreter(resmag, device)
36        # 处理每一页
37        for page in PDFPage.create_pages(document):
38            interpreter.process_page(page)
39            # 接受该页面的LTPage对象
40            layout =device.get_result()
41            for y in layout:
42                if(isinstance(y,LTTextBoxHorizontal)):
43                    with open("%s"%(new_name),'a',encoding="utf-8") as f:
44                        f.write(y.get_text()+"\n")
45
46# 获取文件的路径
47path =open( "I:\Python3.6\patest\PdfTest\数据挖掘在医学大数据研究中的应用_孙雪松.pdf",'rb')
48pdftotxt(path,"pdfminer.txt")

运行结果如下:

总的来说识别程度还是不错的,排版也可以接受,但是对于下面这张图就无法进行合适的排版并进行输出了:

我们仅仅修改文件名称,那么输出的结果将会是这样:

我重写了代码,发现对英文格式可以进行精确输出,但是中文依旧还是和上面的结果一样,所以就没有贴代码了。因为中文的格式和英文的差很多,很难做到百分百的精确输出。所以你们如果有好的方法点击阅读原文,留言和我交流啊。

前面是针对本地的pdf,那么有小伙伴们就要问了,如果是线上的pdf呢?我们应该怎么办?别急这里就教你如何解决。

首先我们将本地的pdf使用浏览器进行打开,这样就模拟了线上的文件。然后就是书写代码了,其实这个代码和之前的几乎完全一样,就是path发生了变化,因此我们需要传入URL。(注意一下URL里面最好不要含有中文,因为URL是byte类型不是String类型)

相应的代码如下:

 1import urllib
 2from io import BytesIO
 3
 4from pdfminer.pdfparser import PDFParser
 5from pdfminer.pdfdocument import PDFDocument
 6from pdfminer.pdfpage import PDFPage
 7from pdfminer.pdfpage import PDFTextExtractionNotAllowed
 8from pdfminer.pdfinterp import PDFResourceManager
 9from pdfminer.pdfinterp import PDFPageInterpreter
10from pdfminer.pdfdevice import PDFDevice
11from pdfminer.layout import *
12from pdfminer.converter import PDFPageAggregator
13from urllib.request import Request
14from urllib.request import urlopen
15
16# 对线上pdf文件进行读取和写入到txt文件当中
17
18
19# 定义解析函数
20def OnlinePdfToTxt(dataIo,new_path):
21    # 创建一个文档分析器
22    parser = PDFParser(dataIo)
23    # 创建一个PDF文档对象存储文档结构
24    document = PDFDocument(parser)
25    # 判断文件是否允许文本提取
26    if not document.is_extractable:
27        raise PDFTextExtractionNotAllowed
28    else:
29        # 创建一个PDF资源管理器对象来存储资源
30        resmag =PDFResourceManager()
31        # 设定参数进行分析
32        laparams=LAParams()
33        # 创建一个PDF设备对象
34        # device=PDFDevice(resmag )
35        device=PDFPageAggregator(resmag ,laparams=laparams)
36        # 创建一个PDF解释器对象
37        interpreter=PDFPageInterpreter(resmag ,device)
38        # 处理每一页
39        for page in PDFPage.create_pages(document):
40            interpreter.process_page(page)
41            # 接受该页面的LTPage对象
42            layout=device.get_result()
43            for y in layout:
44                try:
45                    if(isinstance(y,LTTextBoxHorizontal)):
46                        with open('%s'%(new_path),'a',encoding="utf-8") as f:
47                            f.write(y.get_text()+'\n')
48                            print("读入成功!")
49                except:
50                    print("读入失败!")
51
52# 获取文件的路径
53url = "file:///I:/Python3.6/patest/PdfTest/pdftestto.pdf"
54html = urllib.request.urlopen(urllib.request.Request(url)).read()
55dataIo = BytesIO(html)
56OnlinePdfToTxt(dataIo,'d.txt')

怎么样,是不是代码几乎一样,运行结果和前面的也是完全一样,因此就不贴代码了。

现在我们试试这个文档,这个文档是我做的,里面非常复杂,数字,字母,中文,符号,空格,就连单元格也有合并的。

使用之前的代码能读出来,结果就是这样:

依旧还是以空格和行数表示实际的行,但是能做到这样已经不错了。这个同样是支持多页扫描的,这里我就不介绍了,你们有需要的可以参看官方文档。

pdfplumber介绍

前面介绍了pdfminer,下面我们来介绍另外一个神器pdfplumber。看到名字你就知道它支持多页扫描的,实际上我们今天介绍的三个神器都支持多页扫描,但是就是精度上有些差别而已。这个神器的官方地址在这里:

https://pypi.org/project/pdfplumber/

它相应的安装命令为:

pip install pdfplumber

这个库非常适合含有表格的pdf文档的提取,为了更好的进行测试,你可以点击这里下载该文件,或者该网站的其他pdf文档。

http://www.csrc.gov.cn/pub/newsite/scb/ssgshyfljg/201811/W020181102350857036194.pdf

由于这个pdf很长,大概有85页,因此这里就只提取出一页进行测试:

 1import pdfplumber
 2import re
 3import json
 4
 5path = 'I:\Python3.6\patest\PdfTest\\numberTest 1.pdf'  # 待读取的PDF文件的路径
 6pdf = pdfplumber.open(path)
 7
 8for page in pdf.pages:
 9    # print(page.extract_text())
10    for pdf_table in page.extract_tables():
11        table = []
12        cells = []
13        for row in pdf_table:
14            if not any(row):
15                # 如果一行全为空,则视为一条记录结束
16                if any(cells):
17                    table.append(cells)
18                    cells = []
19            elif all(row):
20                # 如果一行全不为空,则本条为新行,上一条结束
21                if any(cells):
22                    table.append(cells)
23                    cells = []
24                table.append(row)
25            else:
26                if len(cells) == 0:
27                    cells = row
28                else:
29                    for i in range(len(row)):
30                        if row[i] is not None:
31                            cells[i] = row[i] if cells[i] is None else cells[i] + row[i]
32        for row in table:
33            data =[re.sub('\s+', '', cell) if cell is not None else None for cell in row]
34            data_list =list(enumerate(data))
35            # print(json.dumps(data_list, indent=2, ensure_ascii=False))
36            with open('I:\Python3.6\patest\PdfTest\\numberTest1.json','a',encoding="utf-8") as file:   # json文件的存放位置
37                file.write(json.dumps(data_list, ensure_ascii=False))
38pdf.close()

运行结果:

[[0, "门类名称及代码"], [1, "行业大类代码"], [2, "行业大类名称"], [3, "上市公司代码"], [4, "上市公司简称"]]
[[0, "农、林、牧、渔业(A)"], [1, "01"], [2, "农业"], [3, "000998"], [4, "隆平高科"]]

看到没,因为这里我是用json格式进行输出,而且是每一个单元格都有一个id,这样使得每行不区分单元格的大小,也就是说原本多个单元格合并而成的单元格,就是一个大的单元格,其余就被删除了,无法进行下面的扫描。提示一下,我们可以根据坐标来进行扫描。由于这里只是单纯的介绍一下,所以就不详细展开了。

camelot介绍

为什么我们这里着重介绍这个呢?因为这个的官方文档介绍的很详细,而且对新手非常友好,因此强烈建议大家使用这个。

camelot的官方文档在这里:

https://camelot-py.readthedocs.io/en/master/

同样,相应的安装命令如下:

pip install camelot-py

我们现在来测试之前的那个special.pdf文档,之前说了这个很不规则,非常具有代表性:

相应的测试代码如下:

 1import camelot
 2
 3# 从本地的PDF文件中提取表格数据,pages为pdf的页数,默认为第一页
 4tables = camelot.read_pdf('I:\Python3.6\patest\PdfTest\special.pdf', pages='1', flavor='stream')
 5
 6# 表格信息
 7print(tables)
 8print(tables[0])
 9# 表格数据
10print(tables[0].data)

运行结果:

1<TableList n=1>
2<Table shape=(7, 8)>
3[['1', '2', '3', '4', '5', '6', '7', '8'], 
4['B', 'D', 'G', 'H', 'I', 'J', '', 'A'],
5 ['E', '', '', 'F', '', 'K', '', ''],
6 ['', '', 'L', '', '', '。', '()', '【】'],
7 ['', '', 'M', '', '', 'N', 'O', 'P'], 
8['Q', 'R', 'S', '', '', 'T', 'U', 'V'],
9 ['W', 'X', 'Y', '', '', 'Z', '测', '试']]

这个尽管有空格,但是它默认会将合并的单元格进行隐藏拆分,这样原来显示的信息就占了第一个单元格,其余的将以空格进行填充。

上面代码中的camelot.read_pdf()就是camelot从表格中提取数据的函数,里面的参数为PDF文件存放的路径,pages是pdf的页数(默认为第一页),以及解析表格的方法(stream和lattice两个方法)。表格解析方法默认为lattice,stream方法默认会把整个PDF页面当做一个表格来解析。这样就有时候会产生严重的后果,所以大部分情况下,我们都是需要指定解析页面中的区域,你可以使用table_area这个参数来完成区域的指定。

我们第一个神器是将数据存为了txt,第二个神器是将数据存为了json,而第三个神器就比较流弊了,它可以将提取后的数据直接转化为pandas,csv,json,html等函数,就像前面的tables[0].df,tables[0].to_csv()函数那样。我们举个例子,将解析后的数据存为csv文件:

1# 从本地的PDF文件中提取表格数据,pages为pdf的页数,默认为第一页
2tables = camelot.read_pdf('I:\Python3.6\patest\PdfTest\special.pdf', pages='1', flavor='stream')
3
4tables[0].to_csv('special1.csv')

运行一下,当前位置就出现了该csv文件:

1"1","2","3","4","5","6","7","8"
2"B","D","G","H","I","J","","A"
3"E","","","F","","K","",""
4"","","L","","","。","()","【】"
5"","","M","","","N","O","P"
6"Q","R","S","","","T","U","V"
7"W","X","Y","","","Z","测","试"

看到没有,仅仅两行代码就将那么复杂的数据进行了提取,并保存为csv格式,这简直是太神奇了。

接下来我们继续将之前那个上市公司行业分类结果进行提取:

相应的代码如下:

 1import camelot
 2
 3
 4# 从PDF文件中提取表格
 5tables = camelot.read_pdf('I:\Python3.6\patest\PdfTest\\numberTest 1.pdf', pages='1', flavor='stream',strip_text=' .\n')
 6
 7# 绘制PDF文档的坐标,定位表格所在的位置
 8plt= camelot.plot(tables[0],kind='text')
 9plt.show()
10
11# 绘制PDF文档的坐标,定位表格所在的位置
12table_df = tables[0].df
13
14print(table_df.head(n=80))

运行结果:

 1           0       1                   2       3       4
 20                     2018年3季度上市公司行业分类结果                
 31    门类名称及代码  行业大类代码              行业大类名称  上市公司代码  上市公司简称
 42   农、林、牧、渔业      01                  农业  000998    隆平高科
 53        (A)                              002041    登海种业
 64                                         002772    众兴菌业
 75                                         300087    荃银高科
 86                                         300189    神农基因
 97                                         300511    雪榕生物
108                                         600108    亚盛集团
119                                         600313    农发种业
1210                                        600354    敦煌种业
1311                                        600359    新农开发
1412                                        600371    万向德农
1513                                        600506    香梨股份
1614                                        600540    新赛股份
1715                                        600598     北大荒
1816                                        601118    海南橡胶
1917                02                  林业  000592    平潭发展
2018                                        002200    云投生态
2119                                        002679    福建金森
2220                                        600265    ST景谷
2321                03                 畜牧业  000735     罗牛山
2422                                        002234    民和股份
2523                                        002299    圣农发展
2624                                        002321    华英农业
2725                                        002458    益生股份
2826                                        002477    雏鹰农牧
2927                                        002714    牧原股份
3028                                        002746    仙坛股份
3129                                        300106    西部牧业
3230                                        300498    温氏股份
3331                                        600965    福成股份
3432                                        600975     新五丰
3533                04                  渔业  000798    中水渔业
3634                                        002069     獐子岛
3735                                        002086    东方海洋
3836                                        002696    百洋股份
3937                                        200992     中鲁B
4038                                        300094    国联水产
4139                                        600097    开创国际
4240                                        600257    大湖股份
4341                                        600467     好当家

看到没有,这个代码就实现了该图片的提取,同时在pyvharm的右侧也出现了一张坐标图,所以你完全可以根据它进行坐标的选取:

看到没有,这里的蓝色柱就是代表数据,不过它也有一个缺点就是无法做到精确的范围限定,虽说有坐标,但是你很难获取它的精确坐标。但是就目前而言,它能做到这样已经很不错了。

上面介绍的三个神器,大家可以按照自己的具体应用场景来进行使用,尽管它们各有优缺点,但是你们可以结合起来使用,就能达到自己的目的了。

最后如果本文对你有点帮助,不妨分享出去,或者给点赞赏,给小编一包辣条吃。

— — Evny

关于本文,如果你有一些问题,由于微信公众号没放开留言功能,因此请点击阅读原文,给我留言吧。

原文发布于微信公众号 - 啃饼思录(kbthinking)

原文发表时间:2019-01-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券