用python 识别PDF和图片格式的发票并按照发票号重新命名,并生成汇总文件
功能简介
写个小程序对指定目录下所有发票进行行识别,并以发票号命名,生成一个汇总文件
开发思路,如果是图片直接识别,如果是PDF,转成图片在识别
本工具针对增值税发票,其它类型的发票二维内容不一样,可以另行调整
开发前准备
python版本
python版本3.8.10(其它版本不确定会不会有问题)
使用的包
参数**-i url**指定安装包使用代理,国内使用代理安装很快
PyMuPDF:读取pdf,转成图片用(这个包比较坑,不同的版本方法名不一样,变化还不小)
pip install PyMuPDF==1.21.0 -i https://pypi.douban.com/simple
opencv:二维码识别,这个需要导入两个包(这个包因为版权的问题,新的有些方法没有,一定要用我指定的版本)
pip install opencv-python==4.5.2.52 -i https://pypi.douban.com/simple
pip install opencv-contrib-python==4.5.2.52 -i https://pypi.douban.com/simple
openpyxl:生成excel的文件(版本没有要求,用默认的就可以)
pip install openpyxl -i https://pypi.douban.com/simple
开始代码
导入需要使用的包
import shutil # 复制文件
import cv2 # opencv包
import fitz # PyMuPDF
from openpyxl.styles import Side, Font, Border, Alignment # 设置excel格式的
from openpyxl import Workbook, load_workbook # 生成excel文件
import os
import tkinter as tk # 图形化界面开发的包(标准库里的,无需安装)
import tkinter.filedialog # 图形化界面开发的包(标准库里的,无需安装)
import traceback # 获取报错信息的(标准库里的,无需安装)
识别图片的二维码的方法
def read_qr_code(img_path):
"""
读取图片中的二维码
:param img_path:图片路径
:type img_path:str
:return:识别的内容
:rtype:tuple
"""
detector = cv2.wechat_qrcode_WeChatQRCode() # 微信贡献的代码,很好用
img = cv2.imread(img_path)
res, points = detector.detectAndDecode(img)
return res
识别PDF中的二维码
发票中的二维码在第一页,所以只转第一页成为图片
def read_pdf_one_page_qr_code(pdf_path):
"""
读取pdf第一页的二维码
:param pdf_path:PDF文件路径
:type pdf_path: str
:return:识别的内容
:rtype:tuple
"""
pdf_doc = fitz.open(pdf_path)
l = pdf_doc.page_count
if not l:
return ''
page = pdf_doc[0]
rotate = int(0)
zoom_x = 1.33333333
zoom_y = 1.33333333
mat = fitz.Matrix(zoom_x, zoom_y)
mat = mat.prerotate(rotate)
pix = page.get_pixmap(matrix=mat, alpha=False)
img_path = '123.png'
pix.save(img_path)
res = read_qr_code(img_path)
os.remove(img_path)
return res
封装了一个excel生成的类,本贴不详解
个人觉得很好用,把excel的代码过程全部封装,只需要传入字典类行的数据和配置格式就行
thin = Side(border_style="thin", color="000000") # 边框样式,颜色(细边框,黑色)
thick = Side(border_style="thick", color="000000") # 边框样式,颜色(粗边框,黑色)
class ExcelWrite(object):
def __init__(self, wb_data, wb_path=None):
if wb_path:
self.wb = load_workbook(wb_path)
self.st = self.wb.create_sheet(title=wb_data['title'] if 'title' in wb_data else 'sheet')
else:
self.wb = Workbook()
self.st = self.wb.active
self.st.title = wb_data['title'] if 'title' in wb_data else 'sheet'
self.wb_name = wb_data['wb_name'] if 'wb_name' in wb_data else 'test'
self.header_dict = wb_data['header_dict'] if 'header_dict' in wb_data else {}
self.header = wb_data['header'] if 'header' in wb_data else []
self.haveId = wb_data['haveId'] if 'haveId' in wb_data else 0
self.width = wb_data['width'] if 'width' in wb_data else {}
self.header_row_height = wb_data['header_row_height'] if 'header_row_height' in wb_data else 30
self.row_height = wb_data['row_height'] if 'row_height' in wb_data else 14
self.number_fmt = {}
self.__init_style()
def __init_style(self):
st = self.st
col = 1
if self.haveId:
col += 1
st.cell(1, 1).value = '序号'
if self.width:
for k in self.width:
st.column_dimensions[k].width = self.width[k]
if self.header_row_height:
st.row_dimensions[1].height = self.header_row_height
for k in self.header:
st.cell(1, col).value = self.header_dict[k]
col += 1
col_num = len(self.header)
col_num += 2 if self.haveId else 1
for i in range(1, col_num):
st.cell(1, i).font = Font(name=u'宋体', size=12)
st.cell(1, i).border = Border(left=thin, right=thin, top=thin, bottom=thin)
st.cell(1, i).alignment = Alignment(horizontal='center', vertical='center', wrapText=True)
def get_wb(self, data, call_back=None, path=None):
st = self.st
col = 2 if self.haveId else 1
row = 2
for i in data:
if self.row_height:
st.row_dimensions[row].height = self.row_height
if self.haveId:
st.cell(row, 1).value = row - 1
st.cell(row, 1).border = Border(left=thin, right=thin, top=thin, bottom=thin)
st.cell(row, 1).alignment = Alignment(horizontal='center', vertical='center', wrapText=True)
for k in self.header:
st.cell(row, col).value = i[k] if k in i else ''
st.cell(row, col).border = Border(left=thin, right=thin, top=thin, bottom=thin)
st.cell(row, col).alignment = Alignment(horizontal='center', vertical='center', wrapText=True)
if self.number_fmt and str(col) in self.number_fmt:
st.cell(row, col).number_format = self.number_fmt[str(col)]
col += 1
if call_back:
call_back(st, row)
row += 1
col = 2 if self.haveId else 1
path = self.wb_name + ".xlsx" if not path else path
self.wb.save(path)
return path
图形化界面开发
def init():
"""给按钮绑定的方法"""
try:
io = path.get() # 获取选择的路径
invoice(io)
text.set('完成')
root.update()
except Exception:
# 弹出运行的报错信息
root1 = tk.Tk()
root1.title("图书管理系统") # 设置窗口标题
root1.geometry("800x500")
text1 = traceback.format_exc()
text_ = tk.Text(root1, height=30, width=80, )
text_.insert('insert', text1)
text_.pack()
root1.mainloop()
def select_path():
"""
选择目录的方法
"""
path_ = tkinter.filedialog.askdirectory() # 选择文件path_接收文件地址
path_ = path_.replace("/", '\\\\') # 通过replace函数替换绝对文件地址中的/来使文件可被程序读取# 注意:\\转义后为\,所以\\\\转义后为\\
path.set(path_) # path设置path_的值
root = tk.Tk()
root.title("数据提取") # 设置窗口标题
root.geometry("500x500")
path = tk.StringVar() # 目录
text = tk.StringVar() # 提示文本
tk.Label(root, text="文件目录:").grid(row=1, column=0) # 输入框,标记,按键
tk.Entry(root, textvariable=path, width=50).grid(row=1, column=1) # 输入框绑定变量path
tk.Button(root, text="选择目录", command=select_path).grid(row=1, column=2)
tk.Label(root, text="").grid(row=5, column=0)
tk.Label(root, textvariable=text).grid(row=10, column=1)
tk.Button(root, text="发票处理", command=init).grid(row=10, column=2)
tk.Label(root, text="").grid(row=11, column=1)
tk.Label(root, text="").grid(row=12, column=1)
root.mainloop()
主要处理逻辑
关键代码都注释说明了
def invoice(path_dir):
# 在选择目录下创建一个生成结果的目录
result_path = os.path.join(path_dir, '发票整理结果')
if not os.path.exists(result_path):
os.mkdir(result_path)
"""
wb_name:文件名称
title:sheet名称
header_dict:数据key对应的列名
header:要在excel里写入的列
haveId:1,在第一列加入序号列并填充序号,0 不加
width:控制每列的宽度
"""
excel_data = {
'wb_name': '发票整理结果',
'title': '发票整理结果',
'header_dict': {"name": "销售方名称", "invoice_code": "发票代码", "invoice_no": "发票号码", "msg": "备注", "date": "开票日期", "money": "价税合计", '备注': '备注',
'报销单据号': '报销单据号', '记账凭证号': '记账凭证号'},
'header': ["date", "invoice_no", "name", "money", '报销单据号', "记账凭证号", "msg"],
'haveId': 1,
'width': {"B": 40, "C": 20, "D": 20, 'E': 15, 'F': 15, 'G': 30},
}
# 获取目录下的所有文件和文件夹(文件夹里的内容不会去识别)
files = os.listdir(path_dir)
data = [] # 存取文件识别数据
for file in files:
file_path = os.path.join(path_dir, file) # 文件全路径
if '.' not in file: # 排除文件夹
continue
file_format = file.split('.')[-1] # 获取文件格式
# 如果是PDF文件调用pdf的方法,否则调用图片,文件识别报错添加一条报错数据
try:
if file_format in ('pdf', 'PDF'):
code_text = read_pdf_one_page_qr_code(file_path)
else:
code_text = read_qr_code(file_path)
except Exception:
d = {'file_name': file, 'invoice_code': None, 'invoice_no': None, 'msg': '请检查二维码是否存在', 'date': None, 'money': None}
data.append(d)
continue
# 识别内容为空添加一条错误信息
if not code_text:
d = {'file_name': file, 'invoice_code': None, 'invoice_no': None, 'msg': '二维码识别有误', 'date': None, 'money': None}
data.append(d)
continue
# 把识别到的第一个二维码字符串分割
texts = code_text[0].split(',')
# 识别后的长度小于7就认为格式有问题
if len(texts) < 7:
d = {'file_name': file, 'invoice_code': None, 'invoice_no': None, 'msg': '二维码识别有误', 'date': None, 'money': None}
data.append(d)
continue
d = {'file_name': file, 'invoice_code': texts[2], 'invoice_no': texts[3], 'msg': '', 'date': texts[5], 'money': texts[4]}
data.append(d)
# 识别正常的文件重新命名,并放入发票整理结果目录
shutil.copy(file_path, os.path.join(result_path, '%s.%s' % (d['invoice_no'], file_format)))
# 在发票整理结果目录下生成excel文件
ExcelWrite(excel_data).get_wb(data, path=os.path.join(result_path, '发票识别记录.xlsx'))
打包生成可以给其它人使用的exe文件
如果想打包给其它人使用,可以在文件目录下使用下面命令打包,执行完毕,就会在该目录下生成一个dist目录,exe文件就在里面,给别人使用了。该打包需要安装pyinstaller 包。
pip install pyinstaller -i https://pypi.douban.com/simple
pyinstaller -F -w 文件名
全部代码
# -*- coding: utf-8 -*-
"""
@Time : 2022/5/24
@Author : cxk
"""
import shutil
import cv2
import fitz
from openpyxl.styles import Side, Font, Border, Alignment
from openpyxl import Workbook, load_workbook
import os
import tkinter as tk
import tkinter.filedialog
import traceback
thin = Side(border_style="thin", color="000000") # 边框样式,颜色(细边框,黑色)
thick = Side(border_style="thick", color="000000") # 边框样式,颜色(粗边框,黑色)
class ExcelWrite(object):
def __init__(self, wb_data, wb_path=None):
if wb_path:
self.wb = load_workbook(wb_path)
self.st = self.wb.create_sheet(title=wb_data['title'] if 'title' in wb_data else 'sheet')
else:
self.wb = Workbook()
self.st = self.wb.active
self.st.title = wb_data['title'] if 'title' in wb_data else 'sheet'
self.wb_name = wb_data['wb_name'] if 'wb_name' in wb_data else 'test'
self.header_dict = wb_data['header_dict'] if 'header_dict' in wb_data else {}
self.header = wb_data['header'] if 'header' in wb_data else []
self.haveId = wb_data['haveId'] if 'haveId' in wb_data else 0
self.width = wb_data['width'] if 'width' in wb_data else {}
self.header_row_height = wb_data['header_row_height'] if 'header_row_height' in wb_data else 30
self.row_height = wb_data['row_height'] if 'row_height' in wb_data else 14
self.number_fmt = {}
self.__init_style()
def __init_style(self):
st = self.st
col = 1
if self.haveId:
col += 1
st.cell(1, 1).value = '序号'
if self.width:
for k in self.width:
st.column_dimensions[k].width = self.width[k]
if self.header_row_height:
st.row_dimensions[1].height = self.header_row_height
for k in self.header:
st.cell(1, col).value = self.header_dict[k]
col += 1
col_num = len(self.header)
col_num += 2 if self.haveId else 1
for i in range(1, col_num):
st.cell(1, i).font = Font(name=u'宋体', size=12)
st.cell(1, i).border = Border(left=thin, right=thin, top=thin, bottom=thin)
st.cell(1, i).alignment = Alignment(horizontal='center', vertical='center', wrapText=True)
def get_wb(self, data, call_back=None, path=None):
st = self.st
col = 2 if self.haveId else 1
row = 2
for i in data:
if self.row_height:
st.row_dimensions[row].height = self.row_height
if self.haveId:
st.cell(row, 1).value = row - 1
st.cell(row, 1).border = Border(left=thin, right=thin, top=thin, bottom=thin)
st.cell(row, 1).alignment = Alignment(horizontal='center', vertical='center', wrapText=True)
for k in self.header:
st.cell(row, col).value = i[k] if k in i else ''
st.cell(row, col).border = Border(left=thin, right=thin, top=thin, bottom=thin)
st.cell(row, col).alignment = Alignment(horizontal='center', vertical='center', wrapText=True)
if self.number_fmt and str(col) in self.number_fmt:
st.cell(row, col).number_format = self.number_fmt[str(col)]
col += 1
if call_back:
call_back(st, row)
row += 1
col = 2 if self.haveId else 1
path = self.wb_name + ".xlsx" if not path else path
self.wb.save(path)
return path
def read_qr_code(img_path):
"""
读取图片中的二维码
:param img_path:图片路径
:type img_path:str
:return:识别的内容
:rtype:tuple
"""
detector = cv2.wechat_qrcode_WeChatQRCode() # 微信贡献的代码,很好用
img = cv2.imread(img_path)
res, points = detector.detectAndDecode(img)
return res
def read_pdf_one_page_qr_code(pdf_path):
"""
读取pdf第一页的二维码
:param pdf_path:PDF文件路径
:type pdf_path: str
:return:识别的内容
:rtype:tuple
"""
pdf_doc = fitz.open(pdf_path)
l = pdf_doc.page_count
if not l:
return ''
page = pdf_doc[0]
rotate = int(0)
zoom_x = 1.33333333
zoom_y = 1.33333333
mat = fitz.Matrix(zoom_x, zoom_y)
mat = mat.prerotate(rotate)
pix = page.get_pixmap(matrix=mat, alpha=False)
img_path = '123.png'
pix.save(img_path)
res = read_qr_code(img_path)
os.remove(img_path)
return res
def invoice(path_dir):
# 在选择目录下创建一个生成结果的目录
result_path = os.path.join(path_dir, '发票整理结果')
if not os.path.exists(result_path):
os.mkdir(result_path)
"""
wb_name:文件名称
title:sheet名称
header_dict:数据key对应的列名
header:要在excel里写入的列
haveId:1,在第一列加入序号列并填充序号,0 不加
width:控制每列的宽度
"""
excel_data = {
'wb_name': '发票整理结果',
'title': '发票整理结果',
'header_dict': {"name": "销售方名称", "invoice_code": "发票代码", "invoice_no": "发票号码", "msg": "备注", "date": "开票日期", "money": "价税合计", '备注': '备注',
'报销单据号': '报销单据号', '记账凭证号': '记账凭证号'},
'header': ["date", "invoice_no", "name", "money", '报销单据号', "记账凭证号", "msg"],
'haveId': 1,
'width': {"B": 40, "C": 20, "D": 20, 'E': 15, 'F': 15, 'G': 30},
}
# 获取目录下的所有文件和文件夹(文件夹里的内容不会去识别)
files = os.listdir(path_dir)
data = [] # 存取文件识别数据
for file in files:
file_path = os.path.join(path_dir, file) # 文件全路径
if '.' not in file: # 排除文件夹
continue
file_format = file.split('.')[-1] # 获取文件格式
# 如果是PDF文件调用pdf的方法,否则调用图片,文件识别报错添加一条报错数据
try:
if file_format in ('pdf', 'PDF'):
code_text = read_pdf_one_page_qr_code(file_path)
else:
code_text = read_qr_code(file_path)
except Exception:
d = {'file_name': file, 'invoice_code': None, 'invoice_no': None, 'msg': '请检查二维码是否存在', 'date': None, 'money': None}
data.append(d)
continue
# 识别内容为空添加一条错误信息
if not code_text:
d = {'file_name': file, 'invoice_code': None, 'invoice_no': None, 'msg': '二维码识别有误', 'date': None, 'money': None}
data.append(d)
continue
# 把识别到的第一个二维码字符串分割
texts = code_text[0].split(',')
# 识别后的长度小于7就认为格式有问题
if len(texts) < 7:
d = {'file_name': file, 'invoice_code': None, 'invoice_no': None, 'msg': '二维码识别有误', 'date': None, 'money': None}
data.append(d)
continue
d = {'file_name': file, 'invoice_code': texts[2], 'invoice_no': texts[3], 'msg': '', 'date': texts[5], 'money': texts[4]}
data.append(d)
# 识别正常的文件重新命名,并放入发票整理结果目录
shutil.copy(file_path, os.path.join(result_path, '%s.%s' % (d['invoice_no'], file_format)))
# 在发票整理结果目录下生成excel文件
ExcelWrite(excel_data).get_wb(data, path=os.path.join(result_path, '发票识别记录.xlsx'))
def init():
"""给按钮绑定的方法"""
try:
io = path.get() # 获取选择的路径
invoice(io)
text.set('完成')
root.update()
except Exception:
# 弹出运行的报错信息
root1 = tk.Tk()
root1.title("图书管理系统") # 设置窗口标题
root1.geometry("800x500")
text1 = traceback.format_exc()
text_ = tk.Text(root1, height=30, width=80, )
text_.insert('insert', text1)
text_.pack()
root1.mainloop()
def select_path():
"""
选择目录的方法
"""
path_ = tkinter.filedialog.askdirectory() # 选择文件path_接收文件地址
path_ = path_.replace("/", '\\\\') # 通过replace函数替换绝对文件地址中的/来使文件可被程序读取# 注意:\\转义后为\,所以\\\\转义后为\\
path.set(path_) # path设置path_的值
root = tk.Tk()
root.title("数据提取") # 设置窗口标题
root.geometry("500x500")
path = tk.StringVar() # 目录
text = tk.StringVar() # 提示文本
tk.Label(root, text="文件目录:").grid(row=1, column=0) # 输入框,标记,按键
tk.Entry(root, textvariable=path, width=50).grid(row=1, column=1) # 输入框绑定变量path
tk.Button(root, text="选择目录", command=select_path).grid(row=1, column=2)
tk.Label(root, text="").grid(row=5, column=0)
tk.Label(root, textvariable=text).grid(row=10, column=1)
tk.Button(root, text="发票处理", command=init).grid(row=10, column=2)
tk.Label(root, text="").grid(row=11, column=1)
tk.Label(root, text="").grid(row=12, column=1)
root.mainloop()