Python實(shí)現(xiàn)發(fā)票自動(dòng)校核微信機(jī)器人的方法
制作初衷:
外地開了票到公司后發(fā)現(xiàn)信息有錯(cuò)誤,無法報(bào)銷; 公司的行政和財(cái)務(wù)經(jīng)常在工作日被問及公司開票信息,影響心情和工作; 引入相應(yīng)的專業(yè)APP來解決發(fā)票問題對(duì)于一般公司成本較高; 看到朋友孟要早睡寫過腳本來解決這個(gè)問題,但因?yàn)楣緢?chǎng)景不相同,無法復(fù)用,所以新寫了一個(gè)本代碼使用簡(jiǎn)單的封裝方法,并做了比較走心的注釋,希望能給初學(xué)Python的小伙伴提供一些靈感,也能讓有實(shí)際需求的人可以快速修改、使用。
源碼地址:https://github.com/yc2code/WechatInvoiceParser
P.S. 工具基于微信網(wǎng)頁(yè)版,因?yàn)槲⑿殴俜綄?duì)于賬號(hào)有限制,新建的賬號(hào)可能無法使用,會(huì)報(bào):KeyError: ’pass_ticket’,如圖:
所以工具只能使用注冊(cè)時(shí)間較早的賬號(hào)
發(fā)票自動(dòng)校核微信機(jī)器人代碼部分
1. 工具文件 ? Utils包含三個(gè)部分:發(fā)票校核類 Invoice、解析數(shù)據(jù)類 DataParser 和推送日志類 Pushover
Invoice 調(diào)用的百度API,上傳圖片信息,得到解析數(shù)據(jù); DataParser 對(duì)得到的解析數(shù)據(jù)進(jìn)行整理,得到發(fā)送給用戶的信息; Pushover 出現(xiàn)調(diào)用問題時(shí),第一時(shí)間相關(guān)信息推送到維護(hù)者的設(shè)備上。# -*- coding: utf-8 -*-# Utils.pyimport base64import csvimport osimport timeimport requestsfrom Config import configclass Invoice: ''' 發(fā)票識(shí)別類 使用百度發(fā)票識(shí)別API,免費(fèi)使用 官方地址 https://ai.baidu.com/docs#/OCR-API/5099e085 其它功能及配置請(qǐng)移步官網(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): ''' 方法--識(shí)別圖片 調(diào)用百度接口,返回識(shí)別后的發(fā)票數(shù)據(jù) 以下內(nèi)容基本根據(jù)API調(diào)用的要求所寫,無需糾結(jié) 各類報(bào)錯(cuò)碼在官網(wǎng)文檔可查 百度API注冊(cè)及使用教程:http://ai.baidu.com/forum/topic/show/867951 ''' # 識(shí)別質(zhì)量可選high及normal # normal(默認(rèn)配置)對(duì)應(yīng)普通精度模型,識(shí)別速度較快,在四要素的準(zhǔn)確率上和high模型保持一致, # high對(duì)應(yīng)高精度識(shí)別模型,相應(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,最長(zhǎng)邊最大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ā)票號(hào)碼’: result[’InvoiceNum’], ’開票日期’: result[’InvoiceDate’], ’合計(jì)金額’: result[’TotalAmount’], ’價(jià)稅合計(jì)’: result[’AmountInFiguers’], ’銷售方名稱’: result[’SellerName’], ’銷售方稅號(hào)’: result[’SellerRegisterNum’], ’購(gòu)方名稱’: result[’PurchaserName’], ’購(gòu)方稅號(hào)’: result[’PurchaserRegisterNum’], '發(fā)票類型': result['InvoiceType'] } return invoice_data except: message = '發(fā)票識(shí)別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): ''' 方法--日志保存 將識(shí)別記錄寫入文件夾下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 Noneclass DataParser: ''' 數(shù)據(jù)分析類 對(duì)識(shí)別返回后的數(shù)據(jù)進(jìn)行整理,并于默認(rèn)信息對(duì)比,查看有無錯(cuò)誤 這里只簡(jiǎn)單實(shí)現(xiàn)整理信息和檢查名稱和稅號(hào)的方法,有興趣可以增加其他豐富的方法 ''' def __init__(self, invoice_data): self.invoice_data = invoice_data def get_detail_message(self): ''' 對(duì)得到的發(fā)票信息的格式進(jìn)行整理 :return: 返回整理好的發(fā)票信息 ''' values = [value for value in self.invoice_data.values()] detail_mess = f'完整信息為:' f'n發(fā)票代碼: {values[1]}n發(fā)票號(hào)碼: {values[2]}n開票日期: {values[3]}' f'n合計(jì)金額: {values[4]}n價(jià)稅合計(jì): {values[5]}n銷售方名稱: {values[6]}' f'n銷售方稅號(hào): {values[7]}n購(gòu)方名稱: {values[8]}n購(gòu)方稅號(hào):{values[9]}' return detail_mess def get_brief_message(self): ''' 將信息中的名稱和稅號(hào)和默認(rèn)值進(jìn)行對(duì)比 只做對(duì)錯(cuò)判斷,讀者豐富一下可以增加指出錯(cuò)誤位置的信息 :return: 返回判斷的信息 ''' if self.invoice_data['購(gòu)方名稱'] == config['company_name']: brief_mess = '購(gòu)方名稱正確' else: brief_mess = '!購(gòu)方名稱錯(cuò)誤!' if self.invoice_data['購(gòu)方稅號(hào)'] == config['company_tax_number']: brief_mess += 'n購(gòu)方稅號(hào)正確' else: brief_mess += 'n!購(gòu)方稅號(hào)錯(cuò)誤!' return brief_mess def parse(self): brief_mess = self.get_brief_message() detail_mess = self.get_detail_message() return brief_mess, detail_messclass 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ī)器人,對(duì)微信的消息進(jìn)行處理,分析并作出回應(yīng)。
# -*- coding: utf-8 -*-# Wechat.pyimport osfrom wxpy import *class Wechat: ''' 微信處理類 對(duì)微信的消息進(jìn)行處理,分析并作出回應(yīng) ''' def __init__(self, group_name, admin_name): self.bot = Bot() # 類被實(shí)例化的時(shí)候即對(duì)機(jī)器人實(shí)例化 self.group_name = group_name # 指定群聊名 self.admin_name = admin_name # 管理員微信名 self.received_mess_list = [] # 過濾后的消息列表 self.order_list = [] # 管理命令列表 self.pic_list = [] # 待解析圖片絕對(duì)路徑列表 def get_group_mess(self): ''' 方法--獲取消息 獲取所有正常消息,進(jìn)行過濾后存進(jìn)消息列表 ''' # 調(diào)用此方法時(shí)先清空上次調(diào)用時(shí)列表所存儲(chǔ)的數(shù)據(jù) self.received_mess_list = [] for message in self.bot.messages: # 如果為指定群聊或管理員的消息,存入group_mess sender = message.sender.name # >>>這里有一點(diǎn)要注意,如果你是用一個(gè)微信作為機(jī)器人且作為管理員<<< # >>>然后用這個(gè)微信號(hào)在群聊發(fā)消息,則信息sender會(huì)之指向自己而不是群聊<<< # >>>建議使用單獨(dú)一個(gè)微信號(hào)作為機(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è)定所有新增群聊圖片的絕對(duì)路徑及群聊中產(chǎn)生的文字命令 ''' # 調(diào)用此方法時(shí)先清空上次調(diào)用時(shí)列表所存儲(chǔ)的數(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): ''' 方法--存儲(chǔ)圖片 這里使用靜態(tài)方法,是因?yàn)楸痉椒ê皖悰]有內(nèi)部交互,靜態(tài)方法可以方便其他程序的調(diào)用 解析名稱,設(shè)定絕對(duì)路徑,存儲(chǔ) :param image: 接收到的圖片(可以看成是wxpy產(chǎn)生的圖片類,它具有方法和屬性) :return: 返回圖片的絕對(duì)路徑 ''' path = os.getcwd() # 如果路徑下沒有Pictures文件夾,則創(chuàng)建,以存放接收到的待識(shí)別圖片 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 # 賦予絕對(duì)路徑 file_path = path + ’/Pictures/’ + file_name + ’.’ + file_postfix # 將圖片存儲(chǔ)到指定路徑下 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ā)票識(shí)別和處理,另一部分對(duì)于指令做出反應(yīng)。
# -*- coding: utf-8 -*-# Main.pyimport timefrom Utils import Invoice, DataParserfrom Config import configfrom Wechat import *# Author : 達(dá)希# Email : way2go.dash@gmail.comdef main(): ''' 主方法 一部分為發(fā)票識(shí)別和處理,另一部分對(duì)于指令做出反應(yīng) ''' # 輸出重定向,將print語(yǔ)句都寫進(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ā)票識(shí)別詳細(xì)信息 time.sleep(0.5) wechat.send_group_mess(brief_mess) # 返回名稱和稅號(hào)是否有錯(cuò)誤 else: wechat.send_group_mess('請(qǐng)求未成功,請(qǐng)重試或聯(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 Noneif __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)購(gòu)方名稱 'company_tax_number': 'XXX00000000000XXX' # 默認(rèn)購(gòu)方稅號(hào)}
另外,代碼在運(yùn)行時(shí)會(huì)在同文件夾下創(chuàng)建一個(gè)Picture的文件夾,用于存儲(chǔ)待解析的圖片,會(huì)創(chuàng)建 work_log.csv 文件,用于存儲(chǔ)識(shí)別信息的記錄,還有 system_log.text 用于輸出運(yùn)行相應(yīng)的日志。
由于本身需求較少,所以以上代碼功能相對(duì)單薄,僅僅作為一個(gè)輔助的小腳本使用。若要進(jìn)行優(yōu)化完善,wxpy庫(kù)提供了很多豐富的功能,可以在此基礎(chǔ)上打造更加合理完善的,符合個(gè)性化需求的微信機(jī)器人。
總結(jié)
到此這篇關(guān)于Python制作發(fā)票自動(dòng)校核微信機(jī)器人的文章就介紹到這了,更多相關(guān)Python制作發(fā)票自動(dòng)校核微信機(jī)器人內(nèi)容請(qǐng)搜索好吧啦網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持好吧啦網(wǎng)!
相關(guān)文章:
1. idea給項(xiàng)目打war包的方法步驟2. PHP的curl常用的5個(gè)例子3. Python importlib模塊重載使用方法詳解4. php變量與字符串的增刪改查操作示例5. 解決Android Studio XML編輯界面不顯示下面的Text和Design選項(xiàng)卡6. SpringBoot生成二維碼的實(shí)現(xiàn)7. JAVA如何轉(zhuǎn)換樹結(jié)構(gòu)數(shù)據(jù)代碼實(shí)例8. php操作redis常見方法示例【key與value操作】9. 在vue中使用Echarts畫曲線圖的示例10. PHP代碼加密的方法總結(jié)
