Python實(shí)現(xiàn)發(fā)票自動(dòng)校核微信機(jī)器人的方法
制作初衷:
- 外地開了票到公司后發(fā)現(xiàn)信息有錯(cuò)誤,無法報(bào)銷;
- 公司的行政和財(cái)務(wù)經(jīng)常在工作日被問及公司開票信息,影響心情和工作;
- 引入相應(yīng)的專業(yè)APP來解決發(fā)票問題對于一般公司成本較高;
- 看到朋友孟要早睡寫過腳本來解決這個(gè)問題,但因?yàn)楣緢鼍安幌嗤?,無法復(fù)用,所以新寫了一個(gè)
本代碼使用簡單的封裝方法,并做了比較走心的注釋,希望能給初學(xué)Python的小伙伴提供一些靈感,也能讓有實(shí)際需求的人可以快速修改、使用。
源碼地址:https://github.com/yc2code/WechatInvoiceParser
P.S. 工具基于微信網(wǎng)頁版,因?yàn)槲⑿殴俜綄τ谫~號有限制,新建的賬號可能無法使用,會(huì)報(bào):KeyError: 'pass_ticket',如圖:

所以工具只能使用注冊時(shí)間較早的賬號
發(fā)票自動(dòng)校核微信機(jī)器人代碼部分
1. 工具文件 – Utils
包含三個(gè)部分:發(fā)票校核類 Invoice、解析數(shù)據(jù)類 DataParser 和推送日志類 Pushover
- Invoice 調(diào)用的百度API,上傳圖片信息,得到解析數(shù)據(jù);
- DataParser 對得到的解析數(shù)據(jù)進(jìn)行整理,得到發(fā)送給用戶的信息;
- Pushover 出現(xiàn)調(diào)用問題時(shí),第一時(shí)間相關(guān)信息推送到維護(hù)者的設(shè)備上。
# -*- coding: utf-8 -*-
# Utils.py
import base64
import csv
import os
import time
import requests
from Config import config
class Invoice:
"""
發(fā)票識別類
使用百度發(fā)票識別API,免費(fèi)使用
官方地址 https://ai.baidu.com/docs#/OCR-API/5099e085
其它功能及配置請移步官網(wǎng)
"""
@staticmethod
def get_pic_content(image_path):
"""
方法--打開圖片
以二進(jìn)制格式打開
"""
with open(image_path, 'rb') as pic:
return pic.read()
@staticmethod
def parse_invoice(image_binary):
"""
方法--識別圖片
調(diào)用百度接口,返回識別后的發(fā)票數(shù)據(jù)
以下內(nèi)容基本根據(jù)API調(diào)用的要求所寫,無需糾結(jié)
各類報(bào)錯(cuò)碼在官網(wǎng)文檔可查
百度API注冊及使用教程:http://ai.baidu.com/forum/topic/show/867951
"""
# 識別質(zhì)量可選high及normal
# normal(默認(rèn)配置)對應(yīng)普通精度模型,識別速度較快,在四要素的準(zhǔn)確率上和high模型保持一致,
# high對應(yīng)高精度識別模型,相應(yīng)的時(shí)延會(huì)增加,因?yàn)槌瑫r(shí)導(dǎo)致失敗的情況也會(huì)增加(錯(cuò)誤碼282000)
access_token = "你的access_token"
api_url = f"https://aip.baidubce.com/rest/2.0/ocr/v1/vat_invoice?access_token={access_token}"
quality = "high"
header = {"Content-Type": "application/x-www-form-urlencoded"}
# 圖像數(shù)據(jù),base64編碼后進(jìn)行urlencode,要求base64編碼和urlencode后大小不超過4M,
# 最短邊至少15px,最長邊最大4096px,支持jpg/jpeg/png/bmp格式
image_data = base64.b64encode(image_binary)
try:
data = {"accuracy": quality, "image": image_data}
response = requests.post(api_url, data=data, headers=header)
if response.status_code != 200:
print(time.ctime()[:-5], "Failed to get info")
return None
else:
result = response.json()["words_result"]
invoice_data = {
'檢索日期': '-'.join(time.ctime().split()[1:3]),
'發(fā)票代碼': result['InvoiceCode'],
'發(fā)票號碼': result['InvoiceNum'],
'開票日期': result['InvoiceDate'],
'合計(jì)金額': result['TotalAmount'],
'價(jià)稅合計(jì)': result['AmountInFiguers'],
'銷售方名稱': result['SellerName'],
'銷售方稅號': result['SellerRegisterNum'],
'購方名稱': result['PurchaserName'],
'購方稅號': result['PurchaserRegisterNum'],
"發(fā)票類型": result["InvoiceType"]
}
return invoice_data
except:
message = "發(fā)票識別API調(diào)用出現(xiàn)錯(cuò)誤"
Pushover.push_message(message)
return None
finally:
print(time.ctime()[:-5], "產(chǎn)生一次了調(diào)用")
@staticmethod
def save_to_csv(invoice_data):
"""
方法--日志保存
將識別記錄寫入文件夾下work_log.csv文件
若無此文件則自動(dòng)創(chuàng)建并寫入表頭
"""
if "work_log.csv" not in os.listdir():
not_found = True
else:
not_found = False
with open('./work_log.csv', 'a+') as file:
writer = csv.writer(file)
if not_found:
writer.writerow(invoice_data.keys())
writer.writerow(invoice_data.values())
@staticmethod
def run(image_path):
"""
主方法
解析完成返回信息,否則返回None
"""
image_binary = Invoice.get_pic_content(image_path)
invoice_data = Invoice.parse_invoice(image_binary)
if invoice_data:
Invoice.save_to_csv(invoice_data)
return invoice_data
return None
class DataParser:
"""
數(shù)據(jù)分析類
對識別返回后的數(shù)據(jù)進(jìn)行整理,并于默認(rèn)信息對比,查看有無錯(cuò)誤
這里只簡單實(shí)現(xiàn)整理信息和檢查名稱和稅號的方法,有興趣可以增加其他豐富的方法
"""
def __init__(self, invoice_data):
self.invoice_data = invoice_data
def get_detail_message(self):
"""
對得到的發(fā)票信息的格式進(jìn)行整理
:return: 返回整理好的發(fā)票信息
"""
values = [value for value in self.invoice_data.values()]
detail_mess = f"完整信息為:" \
f"\n發(fā)票代碼: {values[1]}\n發(fā)票號碼: {values[2]}\n開票日期: {values[3]}" \
f"\n合計(jì)金額: {values[4]}\n價(jià)稅合計(jì): {values[5]}\n銷售方名稱: {values[6]}" \
f"\n銷售方稅號: {values[7]}\n購方名稱: {values[8]}\n購方稅號:{values[9]}"
return detail_mess
def get_brief_message(self):
"""
將信息中的名稱和稅號和默認(rèn)值進(jìn)行對比
只做對錯(cuò)判斷,讀者豐富一下可以增加指出錯(cuò)誤位置的信息
:return: 返回判斷的信息
"""
if self.invoice_data["購方名稱"] == config["company_name"]:
brief_mess = "購方名稱正確"
else:
brief_mess = "!購方名稱錯(cuò)誤!"
if self.invoice_data["購方稅號"] == config["company_tax_number"]:
brief_mess += "\n購方稅號正確"
else:
brief_mess += "\n!購方稅號錯(cuò)誤!"
return brief_mess
def parse(self):
brief_mess = self.get_brief_message()
detail_mess = self.get_detail_message()
return brief_mess, detail_mess
class Pushover:
"""
消息推送類
本次使用Pushover為推送消息軟件(30 RMB,永久,推薦)
官網(wǎng) https://pushover.net/
可以向微信一樣把相關(guān)信息推送至不同設(shè)備
如果不需要可以把相關(guān)代碼注釋掉
"""
@staticmethod
def push_message(message):
message += ">>>來自Python發(fā)票校驗(yàn)"
try:
requests.post("https://api.pushover.net/1/messages.json", data={
"token": "你的Token",
"user": "你的User",
"message": message
})
except Exception as e:
print(time.ctime()[:-5], "Pushover failed", e, sep="\n>>>>>>>>>>\n")
2. 微信機(jī)器人文件 – Wechat
包含一個(gè)部分:微信處理類 Wechat
作用是初始化機(jī)器人,對微信的消息進(jìn)行處理,分析并作出回應(yīng)。
# -*- coding: utf-8 -*-
# Wechat.py
import os
from wxpy import *
class Wechat:
"""
微信處理類
對微信的消息進(jìn)行處理,分析并作出回應(yīng)
"""
def __init__(self, group_name, admin_name):
self.bot = Bot() # 類被實(shí)例化的時(shí)候即對機(jī)器人實(shí)例化
self.group_name = group_name # 指定群聊名
self.admin_name = admin_name # 管理員微信名
self.received_mess_list = [] # 過濾后的消息列表
self.order_list = [] # 管理命令列表
self.pic_list = [] # 待解析圖片絕對路徑列表
def get_group_mess(self):
"""
方法--獲取消息
獲取所有正常消息,進(jìn)行過濾后存進(jìn)消息列表
"""
# 調(diào)用此方法時(shí)先清空上次調(diào)用時(shí)列表所存儲的數(shù)據(jù)
self.received_mess_list = []
for message in self.bot.messages:
# 如果為指定群聊或管理員的消息,存入group_mess
sender = message.sender.name
# >>>這里有一點(diǎn)要注意,如果你是用一個(gè)微信作為機(jī)器人且作為管理員<<<
# >>>然后用這個(gè)微信號在群聊發(fā)消息,則信息sender會(huì)之指向自己而不是群聊<<<
# >>>建議使用單獨(dú)一個(gè)微信號作為機(jī)器人
if sender == self.group_name or sender == self.admin_name:
self.received_mess_list.append(message)
# 其他的消息過濾掉
self.bot.messages.remove(message)
return None
def parse_mess(self):
"""
方法--處理群聊消息
過濾獲得的指定群聊消息
設(shè)定所有新增群聊圖片的絕對路徑及群聊中產(chǎn)生的文字命令
"""
# 調(diào)用此方法時(shí)先清空上次調(diào)用時(shí)列表所存儲的數(shù)據(jù)
self.pic_list = []
self.order_list = []
# self.group_order = []
for message in self.received_mess_list:
# 如果信息類型為圖片,則保存圖片并添加到圖片列表
if message.type == 'Picture' and message.file_name.split('.')[-1] != 'gif':
self.pic_list.append(Wechat.save_file(message))
# 如果消息類型為文字,則視為命令,保存到命令列表中
if message.type == 'Text':
self.order_list.append(message)
return None
@staticmethod
def save_file(image):
"""
方法--存儲圖片
這里使用靜態(tài)方法,是因?yàn)楸痉椒ê皖悰]有內(nèi)部交互,靜態(tài)方法可以方便其他程序的調(diào)用
解析名稱,設(shè)定絕對路徑,存儲
:param image: 接收到的圖片(可以看成是wxpy產(chǎn)生的圖片類,它具有方法和屬性)
:return: 返回圖片的絕對路徑
"""
path = os.getcwd()
# 如果路徑下沒有Pictures文件夾,則創(chuàng)建,以存放接收到的待識別圖片
if "Pictures" not in os.listdir():
os.mkdir("Pictures")
# 設(shè)定一個(gè)默認(rèn)的圖片格式后綴
file_postfix = "png"
try:
# 嘗試把圖片的名稱拆分,分別獲取名稱和后綴
file_name, file_postfix = image.file_name.split('.')
except Exception:
# 當(dāng)然有時(shí)候可能拆分不了,就把默認(rèn)的后綴給它
file_name = image.file_name
# 賦予絕對路徑
file_path = path + '/Pictures/' + file_name + '.' + file_postfix
# 將圖片存儲到指定路徑下
image.get_file(file_path)
return file_path
def send_group_mess(self, message):
"""
方法--發(fā)送群消息
:param message: 需要發(fā)送的內(nèi)容
"""
try:
# 如果群聊名稱被改變,搜索時(shí)會(huì)報(bào)錯(cuò),如果找不到群聊,消息不會(huì)發(fā)送
group = self.bot.groups().search(self.group_name)[0]
group.send(message)
except IndexError:
print("找不到指定群聊,信息發(fā)送失敗")
return None
def send_parse_log(self):
"""
方法--發(fā)送查詢?nèi)罩?
向群聊內(nèi)發(fā)送查詢?nèi)罩?
"""
try:
# 如果群聊名稱被改變,搜索時(shí)會(huì)報(bào)錯(cuò),如果找不到群聊,消息不會(huì)發(fā)送
group = self.bot.groups().search(self.group_name)[0]
except IndexError:
print("找不到指定群聊,查詢?nèi)罩景l(fā)送失敗")
return None
try:
group.send_file("./work_log.csv")
except:
group.send("Oops, no log yet")
return None
def send_system_log(self):
"""
方法--發(fā)送系統(tǒng)日志
向群聊內(nèi)發(fā)送查詢?nèi)罩?
"""
try:
# 如果群聊名稱被改變,搜索時(shí)會(huì)報(bào)錯(cuò),如果找不到群聊,消息不會(huì)發(fā)送
group = self.bot.groups().search(self.group_name)[0]
except IndexError:
print("找不到指定群聊,系統(tǒng)日志發(fā)送失敗")
return None
try:
group.send_file("./system_log.text")
except:
group.send("System log not found")
return None
3. 主文件 – Main
包含一個(gè)main函數(shù),一部分為發(fā)票識別和處理,另一部分對于指令做出反應(yīng)。
# -*- coding: utf-8 -*-
# Main.py
import time
from Utils import Invoice, DataParser
from Config import config
from Wechat import *
# Author : 達(dá)希
# Email : way2go.dash@gmail.com
def main():
"""
主方法
一部分為發(fā)票識別和處理,另一部分對于指令做出反應(yīng)
"""
# 輸出重定向,將print語句都寫進(jìn)系統(tǒng)日志文件
file = open("./system_log.text", "a+")
sys.stdout = file
# 實(shí)例化微信機(jī)器人,傳入群聊名和管理員名
wechat = Wechat(config["group_name"], config["admin_name"])
while True:
time.sleep(1)
wechat.get_group_mess()
wechat.parse_mess()
# 若群聊有要處理的圖片,則迭代解析
if wechat.pic_list:
for pic in wechat.pic_list:
invoice_data = Invoice.run(pic)
if invoice_data:
data_parser = DataParser(invoice_data)
brief_mess, detail_mess = data_parser.parse()
wechat.send_group_mess(detail_mess) # 先發(fā)送發(fā)票識別詳細(xì)信息
time.sleep(0.5)
wechat.send_group_mess(brief_mess) # 返回名稱和稅號是否有錯(cuò)誤
else:
wechat.send_group_mess("請求未成功,請重試或聯(lián)系管理員")
# 若有相關(guān)命令,則做出相應(yīng)反應(yīng)
if wechat.order_list:
for order in wechat.order_list:
if "開票信息" in order.text:
wechat.send_group_mess(config["company_name"])
time.sleep(0.5)
wechat.send_group_mess(config["company_tax_number"])
elif "SEND LOG" in order.text:
wechat.send_parse_log()
elif "SEND SYSTEM LOG" in order.text:
wechat.send_system_log()
elif "BREAK" in order.text:
wechat.send_group_mess("收到關(guān)機(jī)指令,正在關(guān)機(jī)")
file.close()
return None
if __name__ == "__main__":
main()
4. 配置文件 – Config
包含微信的配置文件信息
config = {
"group_name": "發(fā)票校核ASAP", # 校核群聊名稱,由于本代碼默認(rèn)沒有同名群聊,所以建議設(shè)為復(fù)雜值
"admin_name": "達(dá)希", # 管理員微信名(非備注)
"company_name": "代碼網(wǎng)絡(luò)技術(shù)無限公司", # 默認(rèn)購方名稱
"company_tax_number": "XXX00000000000XXX" # 默認(rèn)購方稅號
}

另外,代碼在運(yùn)行時(shí)會(huì)在同文件夾下創(chuàng)建一個(gè)Picture的文件夾,用于存儲待解析的圖片,會(huì)創(chuàng)建 work_log.csv 文件,用于存儲識別信息的記錄,還有 system_log.text 用于輸出運(yùn)行相應(yīng)的日志。
由于本身需求較少,所以以上代碼功能相對單薄,僅僅作為一個(gè)輔助的小腳本使用。若要進(jìn)行優(yōu)化完善,wxpy庫提供了很多豐富的功能,可以在此基礎(chǔ)上打造更加合理完善的,符合個(gè)性化需求的微信機(jī)器人。
總結(jié)
到此這篇關(guān)于Python制作發(fā)票自動(dòng)校核微信機(jī)器人的文章就介紹到這了,更多相關(guān)Python制作發(fā)票自動(dòng)校核微信機(jī)器人內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Python實(shí)現(xiàn)的掃雷游戲?qū)嵗a
這篇文章主要介紹了基于Python實(shí)現(xiàn)的掃雷游戲?qū)嵗a,對于Python的學(xué)習(xí)以及Python游戲開發(fā)都有一定的借鑒價(jià)值,需要的朋友可以參考下2014-08-08
python之lambda表達(dá)式與sort函數(shù)中的key用法
這篇文章主要介紹了python之lambda表達(dá)式與sort函數(shù)中的key用法,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08
Numpy中創(chuàng)建數(shù)組的9種方式小結(jié)
本文主要介紹了Numpy中創(chuàng)建數(shù)組的9種方式小結(jié),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
使用Python的開發(fā)框架Brownie部署以太坊智能合約
在本文中,我們將使用Python部署智能合約。這篇文章可能是您走向智能合約和區(qū)塊鏈開發(fā)的橋梁!2021-05-05
python GoogleIt庫實(shí)現(xiàn)在Google搜索引擎上快速搜索
這篇文章主要為大家介紹了python GoogleIt庫實(shí)現(xiàn)在Google搜索引擎上快速搜索功能探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01
Python標(biāo)準(zhǔn)庫time使用方式詳解
這篇文章主要介紹了Python標(biāo)準(zhǔn)庫time使用方式詳解,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-07-07

