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

2022-11-09  本文已影响0人  小黄不头秃

总任务

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

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

(1)解压zip文件

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

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

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

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

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

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

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

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

关于PDF文件的读取:

这里对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实现简单接口和路由,完成和数据库的交互。

前端

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

(3.2)Flask 后端框架

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

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

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

在项目根目录下构建:

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

前端:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>实例1-ETC电子发票管理</title>
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
</head>

<body>
    <div id="app" v-cloak>
        <h1 class="title">实例1 - ETC电子发票管理</h1>
        <el-row>
            <el-select v-model="value" style="width: 120px;" placeholder="请选择">
                <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value">
                </el-option>
            </el-select>
            <el-input v-model="input" style="width: 240px;" placeholder="请输入内容"></el-input>
            <el-button type="primary" plain @click="query">查询</el-button>
            <el-button type="primary" plain @click="download">下载</el-button>
        </el-row>
        <br>
        <el-table ref="multipleTable" :data="index_list" :stripe=true :border=true tooltip-effect="drak"
            style="width: 100%; color:#444;text-align: center;" @selection-change="handleSelectionChange">
            <el-table-column type="selection" width="55"></el-table-column>
            <el-table-column label="发票类型" width="120" prop="t1"></el-table-column>
            <el-table-column label="项目名称" width="120" prop="pro_name"> </el-table-column>
            <el-table-column label="发票代码" width="120" prop="code"> </el-table-column>
            <el-table-column label="发票号码" width="120" prop="num"> </el-table-column>
            <el-table-column label="日期" width="120" prop="date"> </el-table-column>
            <el-table-column label="购买方名称" width="120" prop="client_name"> </el-table-column>
            <el-table-column label="购买方税号" width="120" prop="client_itin"> </el-table-column>
            <el-table-column label="销售方名称" width="120" prop="seller_name"> </el-table-column>
            <el-table-column label="销售方税号" width="120" prop="seller_itin"> </el-table-column>
            <el-table-column label="车牌号" width="120" prop="car_num"> </el-table-column>
            <el-table-column label="车辆类型" width="120" prop="car_type"> </el-table-column>
            <el-table-column label="总金额" width="120" prop="total_price"> </el-table-column>
            <el-table-column label="金额" width="120" prop="price"> </el-table-column>
            <el-table-column label="税率" width="120" prop="tax_rate"> </el-table-column>
            <el-table-column label="税价" width="120" prop="tax_price"> </el-table-column>
        </el-table>
    </div>
    <!-- <script src="./vue/vue.js"></script> -->
    <script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script>
    <script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>
    <script src="https://cdn.bootcss.com/qs/6.7.0/qs.min.js"></script>
    <script src="https://unpkg.com/element-ui/lib/index.js"></script>
    <script>
        var vm = new Vue(
            {
                el: '#app',
                data: function () {
                    return {
                        visible: false,
                        index_list: [],
                        true: true,
                        select_box: [],
                        input: "",
                        options: [
                            { label: "项目名称", value: "pro_name" },
                            { label: "发票代码", value: "code" },
                            { label: "发票号码", value: "num" },
                            { label: "年份", value: "year" },
                            { label: "购买方名称", value: "client_name" },
                            { label: "销售方名称", value: "seller_name" },
                            { label: "车牌号", value: "car_num" },
                            { label: "车辆类型", value: "car_type" },
                        ],
                        value: ""
                    }
                },
                mounted() {
                    this.data = this.get_index_info()
                },
                methods: {
                    // 获取主页面列表数据
                    get_index_info: async function () {
                        var info = await axios.get("http://127.0.0.1:5000/")
                        if (info.data.status == 200) {
                            // 数据获取成功
                            this.index_list = info.data.data
                        } else {
                            // 数据获取失败
                            this.$message({
                                    message: '数据获取失败了哦',
                                    type: 'error'
                                });
                        }
                    },
                    // 选项发生改变触发事件
                    handleSelectionChange: function (e) {
                        this.select_box = []
                        for (var i = 0; i < e.length; i++) {
                            this.select_box.push(e[i].num)
                        }
                    },
                    // 查询信息
                    query: async function () {
                        if (this.value == "" || this.input == "") {
                            // 内容不能为空
                            this.$message({
                                message: '请先输入内容',
                                type: 'warning'
                            });
                        } else {
                            data = new FormData
                            data.append("key", this.value)
                            data.append("value", this.input)
                            var info = await axios.post("http://127.0.0.1:5000/query", data)
                            if (info.data.status = "200") {
                                // 查询成功
                                this.index_list = info.data.data
                                console.log(data.data)
                                this.$message({
                                    message: "内容查询成功",
                                    type: "success"
                                })
                            } else {
                                // 查询失败
                                this.$message({
                                    message: '查询失败了哦',
                                    type: 'error'
                                });
                            }
                        }
                    },
                    // 下载文件
                    download: async function () {
                        if (this.select_box[0] == null) {
                            // 未选中
                            this.$message({
                                message: '请先选择下载文件',
                                type: 'warning'
                            });
                        } else {
                            let data = new FormData();
                            data.append('num', JSON.stringify(this.select_box))
                            var info = await axios.post("http://127.0.0.1:5000/download", data, { responseType: 'blob' })
                            
                            if (info.status == 200) {
                                // 数据获取成功
                                let fileName = "test." + info.headers['content-type'].split('/')[1]
                                let url = window.URL.createObjectURL(new Blob([info.data], { type: info.headers['content-type'] }))
                                const a = document.createElement('a')
                                a.style.display = 'none'
                                a.download = fileName
                                a.href = url
                                document.body.appendChild(a)
                                a.click()
                                if (document.body.contains(a)) {
                                    document.body.removeChild(a)
                                }
                            } else {
                                // 数据获取失败
                                this.$message({
                                    message: '哦吼?文件跑路了',
                                    type: 'error'
                                });
                            }
                        }

                    }
                }
            }
        )
    </script>
    <style>
        body {
            padding: 20px;
            margin: 0;
            width: 100%;
            height: 100%;
            background-color: #eee;
        }

        #id {
            text-align: center;
            width: 100%;
        }

        .title {
            font-size: 22px;
            font-weight: 400;
            width: 80%;
        }
    </style>
</body>

</html>
上一篇下一篇

猜你喜欢

热点阅读