小爬最近接到一个棘手任务:需要提取手机话费电子发票PDF文件中的数据。接到这个任务的第一时间,小爬决定搜集各个地区各个时间段的电子发票文件,看看其中的差异点。粗略统计下来,PDF文件的表格框架是统一的,但是数据部分则有较大差异:

.

  小爬首先想到的是借助工具提取发票的文本内容,然后用re正则表达式进行规则化的匹配数据,找到每个字都信息;这其中大部分的python-pdf解析库都能胜任.

可关键的问题是,提取出来的文本差异性非常大,比如说:各段文字出现的顺序并不是按照PDF中的文字的Z序排列.举个例子:"名称:"后面紧跟的未必是真实的用户名称字符,可能是"单价".这就给RE表达式带来了极大难度.后来小爬才意识到,我需要的是一个能够对"表格"数据的支持非常友好的PDF解析库.同时对表格外的图片&文字信息也具备很好的提取能力.

  我们得搞定二维码:发票PDF文件的左上角位置是一个二维码对象,该二维码中可以解析到 "机器编号","发票代码","发票号码","开票日期" 和"校验码".这个时候需要用到fitz.很多人不知道fitz库是啥,其实它是pymupdf中的一个模块,操作PDF非常舒服,只需要pip安装即可:

pip install pymupdf

  该方法基本借鉴了这篇博客的方法:Python提取PDF中的图片,代码示例如下:

def pdf2pic(pdf_path):
#t0 = time.clock() # 生成图片初始时间
checkXO = r"/Type(?= */XObject)" # 使用正则表达式来查找图片
checkIM = r"/Subtype(?= */Image)"
doc = fitz.open(pdf_path) # 打开pdf文件
imgcount = 0 # 图片计数
lenXREF = doc._getXrefLength() # 获取对象数量长度
# 遍历每一个对象
for i in range(1, lenXREF):
text = doc._getXrefString(i) # 定义对象字符串
isXObject = re.search(checkXO, text) # 使用正则表达式查看是否是对象
isImage = re.search(checkIM, text) # 使用正则表达式查看是否是图片
if not isXObject or not isImage: # 如果不是对象也不是图片,则continue
continue
imgcount += 1
pix = fitz.Pixmap(doc, i) # 生成图像对象
#new_name = "图片{}.png".format(imgcount) # 生成图片的名称
new_name=pdf_path.replace("pdf","png").replace("手机话费发票","二维码图片")
if pix.n < 5: # 如果pix.n<5,可以直接存为PNG
pix.writePNG(new_name)
else: # 否则先转换CMYK
pix0 = fitz.Pixmap(fitz.csRGB, pix)
pix0.writePNG(new_name)
pix0 = None
pix = None
break
return new_name # 释放资源

使用它,我们就可以解析得到PDF文件中的二维码图片元素,并保存为PNG图片.再利用pyzbar我们可以很轻易地解析出二维码中的信息.之所以要单独提取出二维码并保存为图片,再识别和解析二维码,是因为如果我们把所有的pdf内容处理为png图片,再让Pyzbar库来解析二维码,则pyzbar需要先定位二维码在图片中的位置,才能开始解析.这样png图片中干扰元素变多,识别率必然下降.而单纯地识别PDF文件中的提取到的二维码图片,则pyzbar库的识别率非常高,几乎达到100%.

但是凡事总有例外,有些PDF文件不规则,无法通过遍历元素的方法得到这个二维码对象元素.此时,我们可以借助截取PDF画面右上角的固定区域来得到二维码图片.具体代码如下:

def crop_to_png(pdfPath):
'''
假定pdf只有一页,只转换第一页的内容的左上角部分(二维码区域)为png图片
tl:TopLeft
br:BottomRight
mp:MiddlePoint
'''
doc = fitz.open(pdfPath)
pngPath=pdfPath[:-4]
page = doc[0]
rotate = int(0)
# 每个尺寸的缩放系数为3,这将为我们生成分辨率提高九倍的图像。
zoom_x = 3.0
zoom_y = 3.0
trans = fitz.Matrix(zoom_x, zoom_y).preRotate(rotate)
rect = page.rect
mp = rect.tl + (rect.br - rect.tl) * 1/5 #二维码矩形区域右下角坐标
clip = fitz.Rect(rect.tl,mp) #裁切的二维码区域的左上角、右下角坐标,定位裁切的矩形区位置
pm = page.getPixmap(matrix=trans, alpha=False,clip=clip)
pngPath=pdfPath.replace("pdf","png").replace("手机话费发票","二维码图片")
pm.writePNG(pngPath)
doc.close()
return pngPath

得到了二维码图片,我们先按照  pdf2pic(pdf_path)  方法,交给pyzbar解析,如果识别不了,再用第二种裁切画面的方法:crop_to_png(pdfPath)   得到二维码图片的方法,交给pyzbar解析.如果两种方法都不能通过pyzbar解析,则返回信息提示用户.具体方法如下:

def parse_invoice_qrcode(pdfPath,pngPath):
"""
通过解析二维码信息,得到发票的发票代码、发票号码
开票日期、检验码、机器编号等信息
"""
invoice_code,invoice_number,total_money,invoice_date,check_code=(None,None,None,None,None)
img=Image.open(pngPath)
#img_size=img.size
#print(img_size)
qrcodes=pyzbar.decode(img)
#print(qrcodes)
try:
qrcodeInfo=qrcodes[0].data.decode("utf-8").split(",")
except:
print("%s:decode error,try another way to decode it"%pngPath)
pngPath=crop_to_png(pdfPath) #使用另一种裁切图片的方法得到二维码图片,并返回图片的路径
img=Image.open(pngPath)
qrcodes=pyzbar.decode(img)
try:
qrcodeInfo=qrcodes[0].data.decode("utf-8").split(",")
except:
print("%s:still decode error"%pngPath)
return invoice_code,invoice_number,total_money,invoice_date,check_code invoice_code=qrcodeInfo[2] #发票代码
invoice_number=qrcodeInfo[3] #发票号码
total_money=qrcodeInfo[4] #不含税总金额
invoice_date=qrcodeInfo[5] #发票日期
check_code=qrcodeInfo[6] #检验码
print("二维码图片名称:%s\n发票代码:%s\n发票号码:%s\n不含税金额:%s\n开票日期:%s\n检验码:%s"%(pngPath,invoice_code,invoice_number,total_money,invoice_date,check_code))
return invoice_code,invoice_number,total_money,invoice_date,check_code

之后,我们用pdfPlumber库来重点提取pdf发票的表格信息.

解决思想:pdfplumber库的 extract_text()提取文本,辅助以extract_tables()方法来提取表格内容.

考虑到extract_tables()方法得到的是一个表格列表,我们的发票PDF文件中只有一个表格,所以使用extract_tables()[0]来获得第一个表格对象,该对象内部是一个二维列表.遍历列表元素,我们就可以得到表格每个区域的内容然后用RE表达式来重点提取.具体代码示例如下:
with pdfplumber.open(pdf_path) as pdf:
p0 = pdf.pages[0]
print("pdf名称:%s"%pdf_path)
contents=p0.extract_text()
#print(contents)
if contents is None:
print("%s:pdf文件格式异常,提取不到文本内容\n"%pdf_path)
continue
else:
contents=contents.replace(" ","").replace(":",":") #打印所有的文本
"""地区"""
if p0.extract_tables()==[]:
print("%s:格式不规范,提取不到表格内容\n"%pdf_path) elif len(p0.extract_tables()[0])<4: #话费pdf文件中表格共有4块
print("%s:格式不规范,提取到的表格内容不完整\n"%pdf_path)
else:
table = p0.extract_tables()[0]
"""销售方名称:运营商"""
pattern=re.search(r".*?(中国.*?)\n.*?",table[3][1],re.S)
salesName=pattern.group(1) if pattern else ""
print("salesname:",salesName)
if salesName:
if "中国移动通信集团" in salesName:
operation_corp="移动" """提取用户名、电话和账期字段"""
txt=table[0][1].replace(":",":").replace(" ","") #名称 &纳税人识别号字段
userName=re.search(r"名称:(.*?)\n.*?",txt).group(1)
if "(号码" in userName:
userName=userName.split("(")[0]
#pattern=re.search(r".*?\D(1[0-9]{10}).*?",txt)
if not tel:
pattern=re.findall(r'\d+',txt) #这串数字(11位)前后都不是数字字符,避免从其他数字中提取了一段当作电话号码
if pattern:
for element in pattern:
if element[0]=="" and len(element)==11:
tel=element
break comment=table[3][7].replace("\n","").replace(" ","").replace(":",":") #发票的备注栏,往往有手机号和账期等信息,考虑到换行,去掉这些必要的换行符、空格符,账期有时在规格型号栏,云南区域
pattern=re.search(".*?(20[0-9]{4})-(20[0-9]{4}).*?",comment) #账期:201812-201902 ,多个月的发票开在一起
if pattern:
if pattern.group(1)!=pattern.group(2):
print("%s:(%s-%s)---不建议将几个月的话费开在同一张发票内!\n"%(pdf_path,pattern.group(1),pattern.group(2)))
continue
pattern=re.search(".*?(20[0-9]{2}[0-1]{1}[0-9]{1}).*?",comment)
if pattern:
period=pattern.group(1) #账期:诸如201906 或者2019年06月
else:
pattern=re.search(".*?(20[0-9]{2}年[0-1]{1}[0-9]{1}月).*?",comment)
if pattern:
period=pattern.group(1)
else:
pattern=re.search(".*?(20[0-9]{2}.[0-1]{1}[0-9]{1}).*?",comment) #格式:2019.02
if pattern:
period=pattern.group(1)
else:
type_size=table[1][2] #规格型号栏
pattern=re.search(".*?(20[0-9]{2}[0-1]{1}[0-9]{1}).*?",type_size) #格式:201902
if pattern:
period=pattern.group(1)
else:
content=table[1][0]
pattern=re.search(".*?(20[0-9]{2}年[0-9]{2}月).*?",content) #格式:2019年02月
period=pattern.group(1) if pattern else "unknown" period=period.replace("年","").replace("月","").replace(".","").replace("-","")
if not tel:
pattern=re.findall(r'号码:\d+',comment)
if pattern:
for element in pattern:
if element[3]=="" and len(element)==14:
tel=element[3:]
break # 该方法提取连续的一段数字,判断数字如果刚好是11位且以数字1打头,则认为在该场景下的这段数字应该是电话号码.
if not tel: #如果最终还是不能提取到对应的号码,则退出当次循环
continue
print("用户名:%s"%userName)
print("电话:%s"%tel)
print("账期:%s"%period)
print("\n")
else:
("运营商不属于中国移动!")
continue
else:
print("不规范的运营商名称(必须以“中国”开头)")
continue

原则上借助该方法,可以提取发票的明细项和对应内容,发票账期,人名,电话号码,税率等内容.

  但是实际的数据提取过程中,部分PDF发票用pdfPlumber模块的 extract_tables()==[],可能捕获的是一个空列表.小爬的例子证明 pdfPlumber不是万能的.知乎上力推的 tabula-py库是基于java的tabula的二次封装.要在python下使用该库,我们还需要安装Java的JRE环境,将来的封装exe也是一个大问题.小爬因此没有继续尝试.其他的python  pdf2htmlEX库,小爬亲测了下,对表格的适用性不太好,尤其是(合并单元格的不规则表格)效果达不到要求.

  小爬试了下 camelot库.这个pdf解析库在windows系统下的安装非常不顺利,几番折腾才得以成功安装.由于网上的诸多教程都没有很好的阐述这个库的安装过程.这里小爬特此 说明该如何正确安装.首先不是直接安装:PIP install camelot.我们需要的库名叫camelot-py.正确的方法是,进到camelot在github上的仓库,下载zip文件解压后,用setup方法运行.地址如下:

https://github.com/camelot-dev/camelot

安装方法:

步骤
打开cmd或者powerShell
到达安装目录
python setup.py build
python setup.py install

由于该库还依赖于tk库,CV库以及Ghostscript(一个exe文件),pandas,numpy等库,我们需要逐个安装这些依赖文件.

如果只想用PIP安装,则需要通过pip install tk,然后再用pip install camelot-py[cv]

就可以在安装camelot-py的同时安装上兼容的CV库.
而Ghostscript并不是通过pip install来安装,它在windows系统下有exe的安装文件,下载地址如下:
https://www.ghostscript.com/download/gsdnld.html
我们只需要下载系统对应的版本就好.

安装该软件,并记得添加用户环境变量路径.

有了这些步骤,我们的camelot库才算可以正常运行.下面是官网给出的一个例子,供参考:

>>> import camelot
>>> tables = camelot.read_pdf('foo.pdf')
>>> tables
<TableList n=1>
>>> tables.export('foo.csv', f='csv', compress=True) # json, excel, html, sqlite
>>> tables[0]
<Table shape=(7, 7)>
>>> tables[0].parsing_report
{
'accuracy': 99.02,
'whitespace': 12.24,
'order': 1,
'page': 1
}
>>> tables[0].to_csv('foo.csv') # to_json, to_excel, to_html, to_sqlite
>>> tables[0].df # get a pandas DataFrame!

具体的操作文档见官方的手册,路径如下:

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

事实证明,camelot和pdfplumber都有各自擅长的pdf解析领域.我们可以在实际的项目中,同时使用这两个库,互为补充.当其中一个提取表格失败时,另一个库有可能可以很理想的得到我们想要的数据.

PS:使用camelot得到的库可以很方便转成pandas需要的DataFrame格式,并结合pandas功能,方便导出CSV或者xlsx格式的文件,进行后继处理!

  赶紧动手试试吧!

利用python第三方库提取PDF文件的表格内容的更多相关文章

  1. 利用Python将多个PDF文件合并

    from PyPDF2 import PdfFileMerger import os files = os.listdir()#列出目录中的所有文件 merger = PdfFileMerger() ...

  2. python第三方库——xlrd和xlwt操作Excel文件学习

    python第三方库——xlrd和xlwt操作Excel文件学习 1安装: C:\Users\Lenovo>pip install xlwtCollecting xlwt  Downloadin ...

  3. python文件打开模式&time&python第三方库

    r:以只读方式打开文件.文件的指针将会放在文件的开头.这是默认模式. w:打开一个文件只用于写入.如果该文件已存在则将其覆盖.如果该文件不存在,创建新文件. a:打开一个文件用于追加.如果该文件已存在 ...

  4. python第三方库,你要的这里都有

    Python的第三方库多的超出我的想象. python 第三方模块 转 https://github.com/masterpy/zwpy_lst   Chardet,字符编码探测器,可以自动检测文本. ...

  5. 深入学习python解析并读取PDF文件内容的方法

    这篇文章主要学习了python解析并读取PDF文件内容的方法,包括对学习库的应用,python2.7和python3.6中python解析PDF文件内容库的更新,包括对pdfminer库的详细解释和应 ...

  6. Python第三方库资源

    [转载]Python第三方库资源   转自:https://weibo.com/ttarticle/p/show?id=2309404129469920071093 参考:https://github ...

  7. Python使用Tabula提取PDF表格数据

    今天遇到一个批量读取pdf文件中表格数据的需求,样式大体是以下这样: python读取PDF无非就是三种方式(我所了解的),pdfminer.pdf2htmlEX 和 Tabula.综合考虑后,选择了 ...

  8. 深入学习Python解析并解密PDF文件内容的方法

    前面学习了解析PDF文档,并写入文档的知识,那篇文章的名字为深入学习Python解析并读取PDF文件内容的方法. 链接如下:https://www.cnblogs.com/wj-1314/p/9429 ...

  9. 【Python基础】安装python第三方库

    pip命令行安装(推荐) 打开cmd命令行 安装需要的第三方库如:pip install numpy 在安装python的相关模块和库时,我们一般使用“pip install  模块名”或者“pyth ...

随机推荐

  1. 【算法】leetcode之 Palindrome Partitioning I&II(转载)

    1 Palindrome Partitioning 问题来源:Palindrome Partitioning 该问题简单来说就是给定一个字符串,将字符串分成多个部分,满足每一部分都是回文串,请输出所有 ...

  2. 2017 ACM-ICPC 亚洲区(南宁赛区)网络赛 M. Frequent Subsets Problem【状态压缩】

    2017 ACM-ICPC 亚洲区(南宁赛区)网络赛  M. Frequent Subsets Problem 题意:给定N和α还有M个U={1,2,3,...N}的子集,求子集X个数,X满足:X是U ...

  3. Light OJ 1272 Maximum Subset Sum 高斯消元 最大XOR值

    版权声明:本文为博主原创文章.未经博主同意不得转载. https://blog.csdn.net/u011686226/article/details/32337735 题目来源:problem=12 ...

  4. form表单提交,后台怎么获取select的值?后台直接获取即可,和input方式一样。

    form表单提交,后台怎么获取select的值? 后台直接获取即可,和后台获取input的值方式一样. form提交后,后台直接根据select的name获取即可,即getPara("XXX ...

  5. @atcoder - CODE FESTIVAL 2017 Final - J@ Tree MST

    目录 @description@ @solution@ @accepted code@ @details@ @description@ 给定 N 个点,第 i 点有一个点权 Xi,再给定一棵边带权的树 ...

  6. React Native中pointerEvent属性

    在React Native界面开发中, 如果使用绝对定位布局,在代码运行时的某个时刻有可能会遮盖住它的下方的某个组件.这是因为绝对定位只是说这个组件的位置由它父组件的边框决定. 绝对定位的组件可以被认 ...

  7. 自定义View系列教程07--详解ViewGroup分发Touch事件

    深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...

  8. Pytorch使用tensorboardX网络结构可视化。超详细!!!

    https://www.jianshu.com/p/46eb3004beca 1 引言 我们都知道tensorflow框架可以使用tensorboard这一高级的可视化的工具,为了使用tensorbo ...

  9. vue简单总结

    首先  介绍几个常见指令 指令:以属性的形式出现在标签上 v-xxx 1.内置指令 数据绑定指令  v-html v-text    举例 <span v-html="msg" ...

  10. pytorch 优化器调参

    torch.optim 如何使用optimizer 构建 为每个参数单独设置选项 进行单次优化 optimizer.step() optimizer.step(closure) 算法 如何调整学习率 ...