又要报销了,还在手动下载整理发票吗?
大多数公司都是每个月定期提交报销,一般报销用的发票都是电子发票发到邮箱,每次要报销时都需要登录邮箱,点开邮件,一个个下载整理,工作量不大,但是发票多了也着实很烦。这个月终于下决心把这个过程自动化一下。
思路
查看了一下邮箱里的发票邮件,虽然主题内容格式不固定,但是基本都包含“发票”,所以可以用“发票”关键词将发票邮件筛选出来。然后解析发票邮件内容,将发票pdf文件提取下载,并整理到指定文件夹。
嗯,就这么简单。
实现
这种事还是用python比较快吧,搞起。
EMAIL相关库
在Python中,有一些常用的库可以用来操作和处理邮件(email):
smtplib
和email
模块:Python标准库中的
smtplib
和email
模块可以用来发送邮件。smtplib
模块负责连接到SMTP服务器并发送邮件,而email
模块负责创建和解析邮件内容。这两个模块结合使用可以实现邮件的发送。示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13import smtplib
from email.message import EmailMessage
msg = EmailMessage()
msg.set_content('This is a test email sent from Python.')
msg['Subject'] = 'Test Email'
msg['From'] = 'sender@example.com'
msg['To'] = 'recipient@example.com'
server = smtplib.SMTP('smtp.example.com')
server.send_message(msg)
server.quit()imaplib
和poplib
模块:Python的
imaplib
和poplib
模块可以用来接收邮件。imaplib
模块用于连接到IMAP服务器,而poplib
模块用于连接到POP3服务器。这两个模块可以用来检索邮件。示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import imaplib
import email
mail = imaplib.IMAP4_SSL('imap.example.com')
mail.login('username', 'password')
mail.select('inbox')
status, messages = mail.search(None, 'ALL')
messages = messages[0].split()
for mail_id in messages:
_, msg = mail.fetch(mail_id, '(RFC822)')
for response_part in msg:
if isinstance(response_part, tuple):
email_message = email.message_from_bytes(response_part[1])
print(email_message['From'])
print(email_message['Subject'])
print(email.utils.parsedate(email_message['Date']))
mail.close()
mail.logout()yagmail
库:yagmail
是一个简化了发送和接收邮件的Python库。它简化了使用SMTP发送邮件和IMAP接收邮件的过程,提供了更加友好的API。示例代码:
1
2
3
4
5
6import yagmail
yag = yagmail.SMTP('sender@example.com', 'password')
contents = ['This is the body of the email', '/path/to/attachment.pdf']
yag.send('recipient@example.com', 'Subject', contents)
yag.close()yagmail
库会处理SMTP服务器的连接和邮件发送,同时也可以方便地添加附件。
这些库提供了不同的功能和灵活性,可以根据自己的需求选择合适的库来操作邮件。
筛选发票邮件
这里一开始走了一点弯路。最初的想法是通过程序筛选发票邮件,然后将发票邮件移动到”发票”文件夹。
但是这样有两个问题,一是这样需要读取全部邮件,筛选出发票邮件,不符合最小权限原则,筛选完还需要把非发票邮件重新标记为“未读”;二是移动到”发票”文件夹这个操作的兼容性不太好,并不是所有邮箱服务商都支持这个操作。
后来想到其实邮箱都有设置收信规则的功能,可以设置一个规则,将收到的发票邮件移动到”发票”文件夹。这样我们就完全可以用自己常用的邮箱来接收发票邮件。
建议还是给接收发票邮件单独设置一个邮箱。
有了前面的基础,程序中就只需要检查指定的“发票”文件夹中的邮件就好了。
这部分功能被封装到EmailClient
类中:
1 | class EmailClient: |
以上代码定义了一个名为EmailClient
的Python类,用于连接到邮件服务器,检索特定文件夹中的未读邮件,并再次检查主题是否包含“发票”关键词。
__init__(self, server, username, password)
:- 这是类的构造函数,用于初始化EmailClient对象。它接受三个参数:
server
(邮件服务器地址)、username
(邮箱用户名)和password
(邮箱密码)。 - 它将这些参数存储在类的实例变量中,以便在整个类中使用。
- 这是类的构造函数,用于初始化EmailClient对象。它接受三个参数:
connect(self)
:- 这个方法用于连接到邮件服务器。它使用
imapclient
库创建了一个IMAP客户端连接,该连接使用SSL加密。 - 如果连接成功,方法返回
True
,否则返回False
。
- 这个方法用于连接到邮件服务器。它使用
list_folders(self)
:- 这个方法用于列出邮箱中的所有文件夹。它首先检查是否已连接到邮件服务器,如果没有连接,它会打印一条错误消息并返回一个空列表。
- 如果已连接,它使用IMAP客户端的
list_folders()
方法获取文件夹列表,并返回这些文件夹的名称。
fapiao_folder_exists(self)
:- 这个方法用于检查是否存在名为“发票”的文件夹。如果存在,它返回
True
。如果不存在,它尝试创建这个文件夹,并返回False
。 - 如果文件夹不存在,它会打印一条消息,然后使用IMAP客户端的
create_folder()
方法创建一个名为“发票”的文件夹。
- 这个方法用于检查是否存在名为“发票”的文件夹。如果存在,它返回
get_fapiao_emails(self, search_criteria=["UNSEEN"])
:- 这个方法用于获取未读邮件中主题包含“发票”的邮件。它首先检查是否已连接到邮件服务器,如果没有连接,它会打印一条错误消息并返回一个空列表。
- 如果已连接,它选择“发票”文件夹,并使用IMAP客户端的
search()
方法获取满足指定搜索条件(默认为未读邮件)的邮件ID。 - 然后,它遍历这些邮件,解码邮件主题,并检查主题中是否包含“发票”关键词。如果包含,将邮件的ID和消息存储在一个列表中,并返回这个列表。
- 如果主题不包含“发票”,它会将邮件标记为“未读”状态,然后打印一条消息。
下载发票
获取发票邮件的内容后,接下来就是对发票内容进行解析,找到发票文件并下载。
这里必须吐槽下,不同商家的发票邮件真的是五花八门,邮件内容格式各有特色。其实也是带给我们一些思考,像类似发票这种票据,应该从政府或行业层面出台统一标准,规定好数据交换格式,这样才能最大化数据流通效率。当然了,标准的制定通常是滞后行业发展的,欧美发达经济体那么标准体系那么健全,依然有大量企业使用自定义格式,导致数据交换效率低下。这就只能靠全社会一起努力了,尽早实现标准化、系统化。
目前下载发票使用了下面3中解析策略:
- 以附件发送发票pdf文件的邮件最好处理,这个符合邮件标准,不管正文说了啥,我们直接下载pdf附件就好了。
- 没有pdf文件附件的邮件,需要解析正文中的链接,找到pdf文件链接,然后下载。
- 最坑的就是正文中的链接连下载链接都不是,而是一个版式文件预览,然后还得手动点击按钮下载。这种目前就只能浏览器打开链接,模拟点击下载按钮了。这个过程可能需要人工干预。
下载了发票pdf文件后就比较好办了,把他们存到对应文件夹就好,按照日期整理到对应”年份/月份”文件夹。
发票下载被封装到FapiaoDownloader
这个类:
1 | class FapiaoDownloader: |
下载后的效果:
未来路线
这个小工具的代码量不大,但是已经基本可以满足我的需求了。
未来主要的更新方向有3个:
- 跟进发票改革,适应新政策,比如即将全面实施的全电发票。
- 减少漏收漏下,提高发票下载的准确性和成功率。
- 增加发票信息提取功能,提取发票中的关键信息,比如发票金额、发票时间、发票类型、发票抬头等。这个功能对个人意义不大,但可以用于企业发票管理,比如发票到期提醒、发票金额统计等。良好的财务实践必要要求规范的票据管理,同时还要积极处理流程中的数据要素。
获取方式
- 无痛使用可移步 发票自动下载整理机器人
- 贡献源码可访问 github仓库
题外:fapiao vs invoice
因为国内的发票和国外的invoice有些细节上的差别,并不完全等同,所以这个小工具的命名中没有使用invoice,而是fapiao😀