又要报销了,还在手动下载整理发票吗?

大多数公司都是每个月定期提交报销,一般报销用的发票都是电子发票发到邮箱,每次要报销时都需要登录邮箱,点开邮件,一个个下载整理,工作量不大,但是发票多了也着实很烦。这个月终于下决心把这个过程自动化一下。

思路

查看了一下邮箱里的发票邮件,虽然主题内容格式不固定,但是基本都包含“发票”,所以可以用“发票”关键词将发票邮件筛选出来。然后解析发票邮件内容,将发票pdf文件提取下载,并整理到指定文件夹。

嗯,就这么简单。

实现

这种事还是用python比较快吧,搞起。

EMAIL相关库

在Python中,有一些常用的库可以用来操作和处理邮件(email):

  1. smtplibemail 模块

    Python标准库中的smtplibemail模块可以用来发送邮件。smtplib模块负责连接到SMTP服务器并发送邮件,而email模块负责创建和解析邮件内容。这两个模块结合使用可以实现邮件的发送。

    示例代码:

    import 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()
    
  2. imaplibpoplib 模块

    Python的imaplibpoplib模块可以用来接收邮件。imaplib模块用于连接到IMAP服务器,而poplib模块用于连接到POP3服务器。这两个模块可以用来检索邮件。

    示例代码:

    import 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()
    
  3. yagmail

    yagmail是一个简化了发送和接收邮件的Python库。它简化了使用SMTP发送邮件和IMAP接收邮件的过程,提供了更加友好的API。

    示例代码:

    import 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类中:

class EmailClient:
    def __init__(self, server, username, password):
        self.server = server
        self.username = username
        self.password = password
        self.client = None

    def connect(self):
        try:
            self.client = imapclient.IMAPClient(self.server, ssl=True)
            self.client.login(self.username, self.password)
            self.client.id_({"name": "IMAPClient", "version": "1.0.0"})
            return True
        except Exception as e:
            print(f"Failed to connect to the email server: {e}")
            return False

    def list_folders(self):
        if not self.client:
            print("Not connected to the email server. Call 'connect' method first.")
            return []

        folder_list = self.client.list_folders()
        return [folder_info[2] for folder_info in folder_list]

    def fapiao_folder_exists(self):
        if "发票" in self.list_folders():
            return True
        else:
            print("'发票'文件夹不存在,正在创建...")
            self.client.create_folder("发票")
            print("已创建'发票'文件夹,请登录邮箱创建收信规则。")
            return False

    def get_fapiao_emails(self, search_criteria=["UNSEEN"]):
        fapiao_emails = []

        if not self.client:
            print("Not connected to the email server. Call 'connect' method first.")
            return []

        self.client.select_folder("发票")
        email_ids = self.client.search(search_criteria)

        for email_id in email_ids:
            email_message = self.fetch_email_content(email_id)
            decoded_subject = email.header.decode_header(email_message["Subject"])

            # 初始化一个空字符串来存储解码后的主题文本
            subject_text = ""

            # 遍历解码结果
            for part, encoding in decoded_subject:
                # 如果编码为None,则假定使用UTF-8编码
                if encoding is None:
                    subject_text += part
                else:
                    # 使用指定编码解码部分
                    subject_text += part.decode(encoding, errors="ignore")

            # 检查解码后的主题文本是否包含"发票"
            if "发票" in subject_text:
                fapiao_emails.append([email_id, email_message])
            else:
                print(f"邮件主题不包含'发票',已标记'未读',请登录邮箱检查")
                print(f"email_id: {email_id}")
                print(f"email_subject: {subject_text}")
                print("-" * 80)
                self.set_email_unread(email_id)

        return fapiao_emails

以上代码定义了一个名为EmailClient的Python类,用于连接到邮件服务器,检索特定文件夹中的未读邮件,并再次检查主题是否包含“发票”关键词。

  1. __init__(self, server, username, password):

    • 这是类的构造函数,用于初始化EmailClient对象。它接受三个参数:server(邮件服务器地址)、username(邮箱用户名)和password(邮箱密码)。
    • 它将这些参数存储在类的实例变量中,以便在整个类中使用。
  2. connect(self):

    • 这个方法用于连接到邮件服务器。它使用imapclient库创建了一个IMAP客户端连接,该连接使用SSL加密。
    • 如果连接成功,方法返回True,否则返回False
  3. list_folders(self):

    • 这个方法用于列出邮箱中的所有文件夹。它首先检查是否已连接到邮件服务器,如果没有连接,它会打印一条错误消息并返回一个空列表。
    • 如果已连接,它使用IMAP客户端的list_folders()方法获取文件夹列表,并返回这些文件夹的名称。
  4. fapiao_folder_exists(self):

    • 这个方法用于检查是否存在名为“发票”的文件夹。如果存在,它返回True。如果不存在,它尝试创建这个文件夹,并返回False
    • 如果文件夹不存在,它会打印一条消息,然后使用IMAP客户端的create_folder()方法创建一个名为“发票”的文件夹。
  5. get_fapiao_emails(self, search_criteria=["UNSEEN"]):

    • 这个方法用于获取未读邮件中主题包含“发票”的邮件。它首先检查是否已连接到邮件服务器,如果没有连接,它会打印一条错误消息并返回一个空列表。
    • 如果已连接,它选择“发票”文件夹,并使用IMAP客户端的search()方法获取满足指定搜索条件(默认为未读邮件)的邮件ID。
    • 然后,它遍历这些邮件,解码邮件主题,并检查主题中是否包含“发票”关键词。如果包含,将邮件的ID和消息存储在一个列表中,并返回这个列表。
    • 如果主题不包含“发票”,它会将邮件标记为“未读”状态,然后打印一条消息。

下载发票

获取发票邮件的内容后,接下来就是对发票内容进行解析,找到发票文件并下载。

这里必须吐槽下,不同商家的发票邮件真的是五花八门,邮件内容格式各有特色。其实也是带给我们一些思考,像类似发票这种票据,应该从政府或行业层面出台统一标准,规定好数据交换格式,这样才能最大化数据流通效率。当然了,标准的制定通常是滞后行业发展的,欧美发达经济体那么标准体系那么健全,依然有大量企业使用自定义格式,导致数据交换效率低下。这就只能靠全社会一起努力了,尽早实现标准化、系统化。

目前下载发票使用了下面3中解析策略:

  1. 以附件发送发票pdf文件的邮件最好处理,这个符合邮件标准,不管正文说了啥,我们直接下载pdf附件就好了。
  2. 没有pdf文件附件的邮件,需要解析正文中的链接,找到pdf文件链接,然后下载。
  3. 最坑的就是正文中的链接连下载链接都不是,而是一个版式文件预览,然后还得手动点击按钮下载。这种目前就只能浏览器打开链接,模拟点击下载按钮了。这个过程可能需要人工干预。

下载了发票pdf文件后就比较好办了,把他们存到对应文件夹就好,按照日期整理到对应"年份/月份"文件夹。

发票下载被封装到FapiaoDownloader这个类:

class FapiaoDownloader:
    def __init__(
        self,
        download_dir=os.path.abspath(
            os.path.join(os.path.dirname(__file__), "../fapiao")
        ),
    ):
        # 创建输出目录

        if not os.path.exists(download_dir):
            os.makedirs(download_dir)

        self.download_dir = download_dir

    def download_fapiao(self, fapiao_emails):
        fapiao_pdfs = []
        for fapiao_email in fapiao_emails:
            # 'Mon, 9 Oct 2023 17:05:39 +0800'
            fapiao_email_date_str = decode_header(fapiao_email[1]["Date"])[0][0]
            fapiao_email_date = datetime.strptime(
                fapiao_email_date_str, "%a, %d %b %Y %H:%M:%S %z"
            )
            fapiao_email_month = fapiao_email_date.strftime("%Y/%m")
            download_dir = os.path.abspath(os.path.join(self.download_dir, fapiao_email_month))
            if not os.path.exists(download_dir):
                os.makedirs(download_dir)

            # 解析邮件附件
            pdf_attachments = self._download_attachments(
                fapiao_email, download_dir, fapiao_email_date
            )
            if pdf_attachments:
                # 邮件包含附件,跳过解析邮件正文
                fapiao_pdfs.extend(pdf_attachments)
                continue
            pdfs = self._download_url(fapiao_email, download_dir, fapiao_email_date)
            if pdfs:
                fapiao_pdfs.extend(pdfs)

        return fapiao_pdfs

下载后的效果:

发票PDF文件

未来路线

这个小工具的代码量不大,但是已经基本可以满足我的需求了。

未来主要的更新方向有3个:

  1. 跟进发票改革,适应新政策,比如即将全面实施的全电发票。
  2. 减少漏收漏下,提高发票下载的准确性和成功率。
  3. 增加发票信息提取功能,提取发票中的关键信息,比如发票金额、发票时间、发票类型、发票抬头等。这个功能对个人意义不大,但可以用于企业发票管理,比如发票到期提醒、发票金额统计等。良好的财务实践必要要求规范的票据管理,同时还要积极处理流程中的数据要素。

获取方式

  1. 无痛使用可移步 发票自动下载整理机器人
  2. 贡献源码可访问 github仓库

题外:fapiao vs invoice

因为国内的发票和国外的invoice有些细节上的差别,并不完全等同,所以这个小工具的命名中没有使用invoice,而是fapiao😀

公众号 | FunIO
微信搜一搜 “funio”,发现更多精彩内容。
个人博客 | blog.boringhex.top

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/117741.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《005.SpringBoot+vue之学生选课管理系统01》

《005.SpringBootvue之学生选课管理系统01》 项目简介 [1]本系统涉及到的技术主要如下: 推荐环境配置:DEA jdk1.8 Maven MySQL 前后端分离; 后台:SpringBootMybatis; 前台:vueElementUI; [2]功能模块展示: 管理端 1…

BO(Business Object)是一种用于表示业务对象的设计模式

1、Service层 BO 1.1、FruitService接口 package com.csdn.fruit.service; import com.csdn.fruit.dto.PageInfo; import com.csdn.fruit.dto.PageQueryParam; import com.csdn.fruit.pojo.Fruit; public interface FruitService {PageInfo<Fruit> getFruitPageInfo(Page…

C++性能优化笔记-6-C++元素的效率差异-7-类型转换

C元素的效率差异 类型转换signed与unsigned转换整数大小转换浮点精度转换整数到浮点转换浮点到整数转换指针类型转换重新解释对象的类型const_caststatic_castreinterpret_castdynamic_cast转换类对象 类型转换 在C语法中&#xff0c;有几种方式进行类型转换&#xff1a; // …

unity【动画】脚本_角色动画控制器 c#

首先创建一个代码文件夹Scripts 从人物角色Player的基类开始 创建IPlayer类 首先我们考虑到如果不挂载MonoBehaviour需要将角色设置成预制体实例化到场景上十分麻烦&#xff0c; 所以我们采用继承MonoBehaviour类的角色基类方法写代码 也就是说这个脚本直接绑定在角色物体…

VBA之正则表达式(44)-- 拆分商品和规格

实例需求&#xff1a;商品组清单保存在A列中&#xff0c;现需要将其拆分为商品名称&#xff0c;保存在从B列开始的后续单元格中&#xff0c;部分商品包含规格&#xff0c;并且多种规格属性使用了逗号分隔&#xff0c;因此无法直接使用Excel分列功能完成数据拆分。 示例代码如下…

“第六十一天”

这三个也算一类的&#xff0c;减和加的处理差不多&#xff0c;不过这个题多了限制是被减数大于减数&#xff0c;要是想再完整一点&#xff0c;可以把小于的情况也考虑进去&#xff0c;不过这个我是如果被减数小于减数的话&#xff0c;我就用减数加被减数&#xff0c;然后最后打…

【MySQL数据库】 四

本文主要介绍了mysql数据库的几种常见的约束. 一.数据库约束 我们希望存储的数据是靠谱的,mysql提供一些机制来辅助我们自动的依赖程序对数据进行检查 . 这类查数据的机制,就是约束 一旦约束好了,后续在进行增 删 改的时候,mysql就会自动的对修改的数据做出检查,如果不符合…

wscat

wscat 是一个用于 WebSocket 通信测试的命令行工具 安装wscat flynnsinflynnsin:~$ sudo npm install -g wscat loadDep:ws → afterAdd ▄ ╢████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░…

爱上C语言:函数递归,青蛙跳台阶图文详解

&#x1f680; 作者&#xff1a;阿辉不一般 &#x1f680; 你说呢&#xff1a;生活本来沉闷&#xff0c;但跑起来就有风 &#x1f680; 专栏&#xff1a;爱上C语言 &#x1f680;作图工具&#xff1a;draw.io(免费开源的作图网站) 如果觉得文章对你有帮助的话&#xff0c;还请…

大数据毕业设计选题推荐-旅游景点游客数据分析-Hadoop-Spark-Hive

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

修复RGBA的png为RGB的png

修改IHDR里面的color type 修改IHDR的crc 删除sBit和sRGB两个chunk

linux中各种最新网卡2.5G网卡驱动,不同型号的网卡需要不同的驱动,整合各种网卡驱动,包括有线网卡、无线网卡、Wi-Fi热点

linux中各种最新网卡2.5G网卡驱动&#xff0c;不同型号的网卡需要不同的驱动&#xff0c;整合各种网卡驱动&#xff0c;包括有线网卡、无线网卡、自动安装Wi-Fi热点。 最近在做路由器二次开发&#xff0c;现在市面上卖的新设备&#xff0c;大多数都采用了2.5G网卡&#xff0c;…

crond服务

目录 一、crond服务基础知识 1、crond服务介绍 2、查看crond服务的状态 3、crond服务配置文件详解 4、额外的配置文件目录 二、crond服务基础命令 1、crond服务使用 2、 管理和操作 crond 服务 3、crond服务命令举例 一、crond服务基础知识 1、crond服务介绍 1、crond…

bug: https://aip.baidubce.com/oauth/2.0/token报错blocked by CORS policy

还是跟以前一样&#xff0c;我们先看报错点&#xff1a;&#xff08;注意小编这里是H5解决跨域的&#xff0c;不过解决跨域的原理都差不多&#xff09; Access to XMLHttpRequest at https://aip.baidubce.com/oauth/2.0/token from origin http://localhost:8000 has been blo…

HarmonyOS 数据管理与应用数据持久化(二)

通过键值型数据库实现数据持久化 场景介绍 键值型数据库存储键值对形式的数据&#xff0c;当需要存储的数据没有复杂的关系模型&#xff0c;比如存储商品名称及对应价格、员工工号及今日是否已出勤等&#xff0c;由于数据复杂度低&#xff0c;更容易兼容不同数据库版本和设备…

100量子比特启动实用化算力标准!玻色量子重磅发布相干光量子计算机

2023年5月16日&#xff0c;北京玻色量子科技有限公司&#xff08;以下简称“玻色量子”&#xff09;在北京正大中心成功召开了2023年首场新品发布会&#xff0c;重磅发布了自研100量子比特相干光量子计算机——“天工量子大脑”。 就在3个月前&#xff0c;因“天工量子大脑”在…

ModbusTcp通信(S7-1200PLC作为服务器端)

S7-200Smart plc作为ModbusTcp服务器端的通信 S7-200SMART PLC ModbusTCP通信(ModbusTcp服务器)_s7-200 modbustcp-CSDN博客文章浏览阅读2.3k次。S7-200SMART PLC 作为ModbusTCP通信(客户端)编程应用和程序详细讲解可以查看下面的博客,链接地址如下:S7-200SMART PLC Modbus…

你的停机真的优雅么?第二弹来袭 | 京东云技术团队

1. 前言 之前总结了一篇基于现有业务线在停机重启时会产生RPC和MQ调用强杀导致业务数据不一致文章&#xff0c;文中通过优雅停机改造对RPC服务进行反注册和MQ进行暂停消费&#xff0c;进而可以解决在停机时强制kill掉RPC线程或者MQ线程导致数据不一致现象&#xff0c;具体的原…

服务号能升级成订阅号吗

服务号和订阅号有什么区别&#xff1f;服务号转为订阅号有哪些作用&#xff1f;一、文章推送的篇数不同服务号在文章的推送篇数上是有所限制的&#xff08;每月推4次&#xff09;订阅号则每天可推送一篇文章。二、定义不同服务号主要是为关注用户提供服务使用的&#xff1b;订阅…

SQL数据库使用方法

首先打开sqlite3.exe所在文件夹&#xff0c;如图1 图1 在文件夹路径中将路径改为cmd&#xff0c;如图2所示 图2 在弹出的cmd窗口中输入如图3所示。 图3 sqlite3 tichiceliang.db 其中tichiceliang是数据库名称。然后按enter&#xff0c;再在cmd中输入.table,可以看到文件夹目…