python 实现ETC电子发票管理系统

总任务

针对“发票样例”中的以压缩文件形式存放的发票文件

  1. 通过python 解压成单独的pdf文件格式的电子发票文件\
  2. 读取pdf文件中的信息,在mysql数据库中建立相应的数据表,将读取的信息存入数据表中\
  3. 建立web服务器,连接第2步得到的mysql数据库,为用户提供发票的查询服务与下载服务,将满足查询条件的多张发票打包下载\
  4. 批量打印满足查询条件的多张发票

(1)解压zip文件

通过python 解压成单独的pdf文件格式的电子发票文件。
通过手动解压缩发现该压缩文件里面还有压缩文件,所以需要递归的进行解压缩。

由于我们系统中只有zip格式的压缩文件,所以下面的代码只能用于解压zip格式的压缩文件,但是其他压缩类型的文件可以以此类推。

在这里面需要学会几个函数的使用:

  • zipfile.ZipFile():传入文件路径,获取压缩文件内的信息
  • zipfile.namelist():获取该目录下的文件夹名和文件名
  • zipfile.extract():解压文件到指定目录,包括文件夹
  • str.endswith():匹配文件后缀
  • list_dir():列出指定路径下的文件夹名和文件名

代码笔记:https://www.jianshu.com/p/77786894f40d

(2.1)读取pdf文件并提取信息

参考博客:https://www.jianshu.com/p/65eae86116c9

读取pdf文件,使用到pdfplumber库。读取出的文本内容使用正则匹配来获取信息。使用之前需要使用pip命令安装该库。

这里匹配的是invoice文件夹中的发票信息。没有匹配invoiceDetail里面的信息。
!pip install pdfplumber

关于PDF文件的读取:

  • pdfplumber.open():打开pdf文件
  • pdf.pages[0]:查看第一页的内容
  • first_page.extract_text():读取文本信息

这里对PDF文件信息的提取使用的是正则匹配,会用到re库。
关于库里的函数可以参考:https://blog.csdn.net/qq_39962271/article/details/123884585

对于表格里面的信息可以使用extract_table提取。

代码笔记:https://www.jianshu.com/p/a8a572dd73ef

(2.2)将信息写入MySQL中

在连接数据库之前,我们需要使用到一个模块pymysql,使用pip命令安装该库:
pip install pymysql

还需要开启MySQL服务,在命令行中输入:net start mysql 即可
如果报错,有两种可能:

  1. mysql没有加入环境变量,加入环境变量再执行下一步即可
  2. MySQL不在服务列表中,在管理员模式下的命令行中输入mysqld -install

代码笔记:https://www.jianshu.com/p/f6afa5b735a2

(3.1) web服务器设想

整体采用前后端分离的架构(VUE + Flask + MySQL)。
前端服务和后端服务之间使用JSON格式的数据进行交互。

后端

Flask实现简单接口和路由,完成和数据库的交互。

  • /:主页面
  • /query: 查询
  • /download: 下载,多发票打包下载
    单个文件可以直接发送,批量文件的话需要对多个文件进行打包后再发送。
前端

使用Vue框架进行开发,使用element UI做组件渲染。
使用axios插件发送HTTP请求(GET,POST)。
实现简单的文件下载功能。

(3.2)Flask 后端框架

flask是一个非常轻量化的后端框架,与django相比,它拥有更加简洁的框架。django功能全而强大,它内置了很多库包括路由,表单,模板,基本数据库管理等。flask框架只包含了两个核心库(Jinja2 模板引擎和 Werkzeug WSGI 工具集),需要什么库只需要外部引入即可,让开发者更随心所欲的开发应用。

使用之前需要先安装Flask库pip install flask

flask项目快速构建,似乎只有pycharm企业版能够自动帮你构建项目,其他编程软件只能通过手动创建。因为flask框架对项目目录没有要求,所以项目的目录我们可以根据自己的需求设计,即使是单个文件也可以执行。

在项目根目录下构建:

  • webapp包目录,存放flask代码,包内有init.py文件
  • templates目录,存放模板文件
  • static目录,存放js,css等静态文件。其下建立js目录,放入jquery、echarts的js文
  • app.py入口文件

使用pip freeze >requirements.txt可以记录所有依赖包和精确的版本号,以便在新环境中进行操作部署。

使用pip install -r requirements.txt可以在新的环境中安装所有依赖包。

快速入门传送门:https://www.bilibili.com/video/BV17W41177oE?p=1&vd_source=9e5b81656aa2144357f0dca1094e9cbe

flask:

# 先用一个文件启动Flask服务
# -*- coding:utf-8 -*-

# 1.导入flask扩展
from flask import Flask, send_file
from flask import make_response
from flask import request
from flask import send_from_directory
from flask import g 
import pymysql
import json
import zipfile 
import random 
import shutil 
import os
import time

# 2.创建flask应用程序实例
# 需要传入__name__,作用是为了确定资源所在的路径
app = Flask(__name__)
# app.config['ENV'] = "development"
app.config['SECRET_KEY']="demo"

# 连接数据库
connect = pymysql.connect(
    host='localhost',
    user='root',
    passwd="",
    charset="utf8",
    autocommit=True,
    database="pdf_info"
)
cur = connect.cursor() # 创建游标,用于读取数据

# 3. 定义路由和视图函数
# Flask中定义路由是通过装饰器实现的
# 这是主页返回所有的数据
@app.route('/',methods=["GET","POST"])
def index():
    """主页返回所有文件列表"""
    try:
        query_info = "select * from info;"
        cur.execute(query_info)
        res = cur.fetchall()
    except Exception as e:
        info = {
            "data":[],
            "status":400,
            "info":"数据表获取失败:"+e
        }
        return json.dumps(info)
    else:
        info = {
            "data":[],
            "status":200,
            "info":"数据查找成功!"
        }
        for data in res: 
            dic = {
                't1': '','pro_name': '','code': '','num': '','date': '','year': '',
                'month': '','day': '','client_name': '','client_itin': '',
                'seller_name': '','seller_itin': '','car_num': '','car_type': '',
                'total_price': '','price': '','tax_rate': '','tax_price': '','dir': ""
            }
            item = list(data)
            for i,key in enumerate(dic.keys()):
                dic[key] = item[i]
            info['data'].append(dic)
        #设置响应头
        resp = make_response(json.dumps(info))
        resp.status = "200"            # 设置状态码
        resp.headers["Content-Type"] = "application/json"      # 设置响应头 
        resp.headers["Access-Control-Allow-Origin"] = "*"      # 设置响应头 
        return resp

@app.route('/query',methods=["GET","POST"])
def query():
    """根据键值对对数据进行查找 参数列表为:(key=字段, value=值)"""
    info = {
        "data":[],
        "status":200,
        "info":"数据查找成功!"
    }
    if request.method == 'POST':
        key = request.form.get("key","")
        value = request.form.get("value","")
        if key == "" or value == "":
            info["info"] = "数据为空"
            info["status"] = 400
            #设置响应头
            resp = make_response(json.dumps(info))
            resp.status = "400"            # 设置状态码
            resp.headers["Content-Type"] = "application/json"      # 设置响应头 
            resp.headers["Access-Control-Allow-Origin"] = "*"      # 设置响应头 
            return resp
        query_sql = "select * from info where {}='{}';".format(key,value)
        cur.execute(query_sql)
        res = cur.fetchall()
        for data in res:
            dic = {
                't1': '','pro_name': '','code': '','num': '','date': '','year': '',
                'month': '','day': '','client_name': '','client_itin': '',
                'seller_name': '','seller_itin': '','car_num': '','car_type': '',
                'total_price': '','price': '','tax_rate': '','tax_price': '','dir': ""
            }
            item = list(data)
            for i,key in enumerate(dic.keys()):
                dic[key] = item[i]
            info['data'].append(dic)
        #设置响应头
        resp = make_response(json.dumps(info))
        resp.status = "200"            # 设置状态码
        resp.headers["Content-Type"] = "application/json"      # 设置响应头
        resp.headers["Access-Control-Allow-Origin"] = "*"      # 设置响应头  
        return resp
    else:
        info["info"] = "查询失败"
        info["status"] = 400
        #设置响应头
        resp = make_response(json.dumps(info))
        resp.status = "400"            # 设置状态码
        resp.headers["Content-Type"] = "application/json"      # 设置响应头 
        resp.headers["Access-Control-Allow-Origin"] = "*"      # 设置响应头 
        return resp

@app.route('/download',methods=["GET","POST"])
def send():
    """批量下载文件"""
    info = {
        "data":[],
        "status":200,
        "info":"数据查找成功!"
    }
    if request.method == "POST":
        downloadlist = request.form.get("num","")
        if downloadlist !="":
            downloadlist = json.loads(downloadlist)
            # 批量文件打包发送和单文件发送
            if len(downloadlist) == 1:
                # single file
                query_sql = "select path,file from info where num={};".format(downloadlist[0])
                cur.execute(query_sql)
                res = cur.fetchall()[0]
                # response = make_response(send_from_directory(res[0],res[1],as_attachment=True))
                response = make_response(send_file(res[0]+res[1],as_attachment=True))
                # 如果 response.header 中没有添加  Access-Control-Expose-Headers 这个参数(代表:服务器允许浏览器访问的头(headers)的白名单),vue中就无法获取 content-disposition,即 res.headers['content-disposition'];无法找到
                response.headers["content-disposition"] = "attachment;filename=test.pdf"
                response.headers["FileName"] = "test.pdf"
                response.headers[" Access-Control-Expose-Headers"] = "FileName"
                response.headers["content-type"] = "application/pdf"
                response.headers["access-control-allow-origin"] = "*" 
                return response
            else:
                # multiple file https://www.cnblogs.com/hahaa/p/16512432.html
                query_sql = "select dir from info where num in{};".format(tuple(downloadlist))
                cur.execute(query_sql)
                res = cur.fetchall()
                times = str(int(time.time())+random.randint(0,100000))# 防止冲突,因为有可能有人同时下载文件
                des_path = "./temp/"+times+"/"
                if not os.path.exists("./temp/"):
                    os.mkdir("./temp/")
                mkdir_path = os.path.join(os.getcwd(),"temp",times)
                os.mkdir(mkdir_path)
                zip = zipfile.ZipFile(des_path+"/test.zip","w",zipfile.ZIP_DEFLATED)
                # 将指定文件复制到临时文件夹下
                for i,data in enumerate(res):
                    path = des_path + str(i) + ".pdf"
                    f = open(path,"wb")
                    src_file = open(data[0],"rb")
                    f.write(src_file.read())
                    f.close()
                    src_file.close()
                # 压缩批量文件
                for file in os.listdir(des_path):
                    if file.endswith('.pdf'):
                        zip.write(des_path+file) 
                zip.close()
                
                resp = make_response(send_from_directory(des_path,"test.zip",as_attachment=True))
                resp.headers["Content-Disposition"] = "attachment; filename=test.zip"
                resp.headers["Content-Type"] = "application/zip"
                resp.headers["Access-Control-Allow-Origin"] = "*"      # 设置响应头 
                
                g.dir = des_path
                # # 发送之后需要删除文件夹
                # @response.call_on_close
                # def on_close():
                #     shutil.rmtree(des_path)
                return resp
        else:
            info["info"] = "内容而为空"
            info["status"] = 400
            #设置响应头
            resp = make_response(json.dumps(info))
            resp.status = "400"            # 设置状态码
            resp.headers["Content-Type"] = "application/json"      # 设置响应头 
            resp.headers["Access-Control-Allow-Origin"] = "*"      # 设置响应头 
            return resp
    else:
        info["info"] = "下载失败"
        info["status"] = 400
        #设置响应头
        resp = make_response(json.dumps(info))
        resp.status = "400"            # 设置状态码
        resp.headers["Content-Type"] = "application/json"      # 设置响应头 
        resp.headers["Access-Control-Allow-Origin"] = "*"      # 设置响应头 
        return resp

# @app.after_request
# def on_close(res):
#     # PermissionError: [WinError 32] 另一个程序正在使用此文件,进程无法访问。
#     shutil.rmtree(g.dir)
#     return res

# 4. 启动服务
if __name__ == '__main__':
    app.run(port=5000)
    

前端:





    
    
    
    实例1-ETC电子发票管理
    



    

实例1 - ETC电子发票管理

查询 下载

你可能感兴趣的:(python 实现ETC电子发票管理系统)