用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()

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值