美文网首页
python 实现ETC电子发票管理系统

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

作者: 小黄不头秃 | 来源:发表于2022-11-09 23:41 被阅读0次

    总任务

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

    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)
        
    

    前端:

    <!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>
    

    相关文章

      网友评论

          本文标题:python 实现ETC电子发票管理系统

          本文链接:https://www.haomeiwen.com/subject/qgtazrtx.html