背景:因为每个月有报销发票的需求,但每次到报销发票的时候,都需要去邮箱上一个个把发票下载下来,然后分类整理。这些都是耗时且重复的动作,就想着能不能把它自动化,同时看看能不能结合上最近大火的AI
目标:能够按要求自动下载发票文件,同时解析重命名发票文件
这一步是很简单的啦,用Selenium就可以实现,我的是QQ邮箱。我们只需要打开浏览器按下F12一步步的查找从登录到进入邮件下载附件都需要点击哪些元素,然后用Selenium代替我们操作就行
BASE_URL = 'https://mail.qq.com/' def begin(): global origin_window_handle driver.get(Constant.BASE_URL) origin_window_handle = driver.current_window_handle
这里登录就没有做的那么麻烦,自己扫码或者输入账号密码即可。
登录到QQ邮箱后,通过F12可以看到主要功能区是在iframe里面,因此我们要先点击收件箱然后切换到iframe中,否则没法找到元素
def switch_to_frame(): recv_option = util.getDelayElement(By.PARTIAL_LINK_TEXT, "收件箱") recv_option.click() main_frame = util.getDelayElement(By.CSS_SELECTOR, "#mainFrame") driver.switch_to.frame(main_frame)
接着就获取邮件,每次只能获取当前页数的邮件,我这里是把未读和已读都获取到了
def get_mail_list(): return driver.find_elements_by_class_name("M") + driver.find_elements_by_class_name("F")
因为我只要处理发票的邮件,用了最简单的方式,只处理当前邮件标题是否包含发票两字,同时发票邮件包括两种,带有附件和不带有附件的,带有附件的是邮件里直接附上了发票文件。不带有附件的是邮件里给了一个链接,需要点击后才能下载或者跳转到其他网站下载。因为两种方式的处理方式不同,所以需要分开存储哪些是带有附件的哪些没带有附件。同时为了避免同名标题不同发票的情况,我们使用mailId来进行存储,后续通过mailId来定位每一个邮件
def handle_mail(): global end_flag invoice_list = [] mail_list = get_mail_list() mail_num = len(mail_list) print(f'mail_num:{mail_num}, page: {current_page}') for mail in mail_list: mailid = mail.find_element(By.CSS_SELECTOR, 'td.tl.tf ').find_element(By.TAG_NAME, 'nobr').get_attribute( 'mailid') title = mail.find_element_by_class_name("tt").text if '发票' in title: try: mail.find_element(By.CSS_SELECTOR, 'div.cij.Ju') invoice_list.append({'title': title, 'mailId': mailid}) except NoSuchElementException as e: invoice_list.append({'title': title, 'mailId': mailid}) print(f'-------发票: {len(invoice_list)}-------') for item in invoice_list: monitor.reset_create() mailId = item["mailId"] title = item["title"] tag = driver.find_element_by_xpath(f"//nobr[@mailid='{mailId}']") tag.click() time.sleep(1) if is_out_date(): end_flag = True break try: if exist_element(By.ID, 'attachment'): download_attach() check_file(item) else: handle_no_attach() check_file(item) except Exception as e: record_fail(item) switch_to_frame() time.sleep(3)
附件可能会有多个附件,我们只需要下载PDF文件即可
def download_attach(): attachment = util.getDelayElement(By.ID, 'attachment') attach_items = attachment.find_elements(By.CSS_SELECTOR, 'div.att_bt.attachitem') for attach in attach_items: util.getDelayElement(By.CSS_SELECTOR, 'div.name_big') if '.pdf' in attach.find_element(By.CSS_SELECTOR, 'div.name_big').find_element(By.TAG_NAME, 'span').text: attach.find_element_by_partial_link_text('下载').click() break time.sleep(3) # driver.back() driver.refresh() switch_to_frame() time.sleep(4)
这个才是本次的重点,对于没有附件只有下载链接的邮件,如何让selenium知道该点哪里。不同的邮件他们的展示也不同
image.png 解决方法就是让AI来告诉selenium该点哪里,通过F12可以发现,邮件的内容都是在一个固定的Div里面
image.png 那我们就可以获取这个Div里面的HTML片段,然后告诉AI,让它根据HTML片段解析出带有发票下载链接的标签文本,然后返回,selenium根据这个文本点击,以下是对于prompt
prompt = ''' 你是一名HTML解析助手,你需要解析用户上传的HTML片段。 1.解析出片段中带有发票下载链接的超链接标签文本。 2.如果有多个下载链接,则找出下载为PDF格式的超链接标签文本即可。 例如: 输入: <p style="display: flex;justify-content: flex-start;align-items: flex-start;font-size:14px;line-height:20px;"> <span style="white-space: nowrap;color: #333">下载PDF文件:</span> <img width="20" src="https://img.pdd-fapiao.com/biz/bG9uZ2p1bmd3YW5n.png"> <a href="https://www.hxpdd.com/s/Q3QQGcH49TCm" style="word-break:break-all;margin-left: 10px;color: #3786c7" rel="noopener" target="_blank">HelloWorld</a> </p> 输出: {"text":"HelloWorld"} 注意只需要返回对应的标签文本即可,不需要其他内容。结果输出为JSON:{'text':'xxx'}"} '''
点击过后,一般会有两种结果,一种是点击后能够直接下载发票,另一种是跳转到其他网站后,再点击下载才能下载
image.png 针对这种我们就可以全文搜索带有下载字样的标签进行点击下载。经过上面这一套下来,基本90%的都能成功下载下来
下载下来的发票命名各异
image.png 我希望能够通过文件名就能知道该发票的类型,金额和开票日期,这里用上了OCR+AI,用OCR来解析发票文件,然后将解析结果送给AI让其根据结果分析该发票属于哪种类别。最后再重命名发票
def parse_invoice(): invoice_list = get_invoice_list() location = config.get_location() for invoice in invoice_list: file_path = os.path.join(location, invoice) invoice_info = OcrUtil.ocr_invoice(file_path) new_file_name = get_new_file_name(invoice_info) util.rename(invoice, new_file_name) print(f'{invoice} : {new_file_name}') def get_new_file_name(invoice_info): date = util.parse_date(invoice_info['Date'], '%Y年%m月%d日', '%Y%m%d') summary = get_summary(invoice_info) return summary + '_' + invoice_info['Number'] + '_' + invoice_info['Total'].split('.')[0] + '_' + date def get_summary(item): if 'VatElectronicItems' in item: info = json.loads(json.dumps(item['VatElectronicItems'])) else: info = json.loads(json.dumps(item['VatInvoiceItemInfos'])) return AI.ai_summary(info[0]['Name'])
最后就是这个样子
在config.json文件设置文件下载地址和日期,日期的作用是在下载附件时只下载日期之后的附件,在OcrUtil.py文件里设置腾讯云的secret_id和secret_key,在AI.py文件里设置GPT的api_key后,然后运行main.py,等待浏览器拉起后,手动登录后即可
目前只实现了下载和解析重命名,邮箱目前只支持QQ邮箱,AI目前只支持GPT,后续会支持多个邮箱和AI 同时还在考虑可以加入哪些功能,有建议和优化欢迎大家在GitHub给我提,如果有帮助的话帮忙Star一下