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

news2025/1/9 1:42:42

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

思路

查看了一下邮箱里的发票邮件,虽然主题内容格式不固定,但是基本都包含“发票”,所以可以用“发票”关键词将发票邮件筛选出来。然后解析发票邮件内容,将发票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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

事务管理 vs. 锁控制:你真的分得清吗?何时使用何种并发控制策略?

分布式锁和事务是分布式系统中两个重要的概念,它们都用于解决分布式环境下的数据一致性问题。 一、概念 分布式锁 分布式锁是一种用于在分布式环境中控制对共享资源访问的锁。分布式锁可以防止多个进程或线程同时访问共享资源,从而避免数据冲突和资源…

Mini小主机All-in-one搭建教程4-安装Windows11系统

Mini小主机All-in-one搭建教程4-安装Windows11系统 硬件介绍 在狗东买的 极摩客M2 到手价是2799元 具体配置如下: 酷睿英特尔11代标压i7 11390H 64G1TB固态。 以下是 安装Windows11系统的教程。 安装Windows11系统 下载镜像包 首先下Windows系统的懒人镜像包&…

【特纳斯电子】基于物联网的智能油烟机-仿真设计

视频及资料链接:基于物联网的智能油烟机-仿真设计 - 电子校园网 (mcude.com) 编号: T0332203M-FZ 设计简介: 本设计是基于物联网的智能油烟机系统,主要实现以下功能: 1.通过OLED显示燃气浓度; 2.可通过…

多媒体应用设计师 第4章 移动多媒体技术基础

1.移动多媒体技术基础 1.1.移动互联网的定义 移动互联网是指利用互联网提供的技术、平台、应用以及商业模式,与移动通信技术相结合并用于实践活动的总称。 1.2.移动互联网的特征 移动互联网三个层面:终端、软件、应用 移动互联网特征:2版…

在搜狗浏览器中设置代理

要在搜狗浏览器中设置代理,请按照以下步骤操作: 打开搜狗浏览器。在浏览器顶部菜单栏中点击“设置”(一般位于右上角)。在设置菜单中点击“代理设置”。在代理设置页面中,将“使用代理”选项设置为“自动检测”或“al…

通讯网关软件026——利用CommGate X2ORACLE-U实现OPC UA数据转入ORACLE

本文介绍利用CommGate X2ORACLE-U实将OPC UA数据源中的数据转入到ORACLE数据库。CommGate X2ORACLE-U是宁波科安网信开发的网关软件,软件可以登录到网信智汇(http://wangxinzhihui.com)下载。 【案例】如下图所示,将OPC UA数据源的数据写入到ORACLE数据…

ssh 报错:Permission denied, please try again.

报错问题:执行一条远程scp远程拷贝,在此之前已配置好ssh无密登录, sudo scp -r hadoop-3.2.0 slave2:/usr/local/src/ 确保 /etc/ssh/sshd_config文件下 PasswordAuthentication no 改为 PasswordAuthentication yes 和 PermitRootLogin no …

云爬虫系统设计:云平台资源管理优化爬虫性能

目录 1、云爬虫系统概述 2、云平台资源管理优化爬虫性能的关键措施 2.1 资源池化 2.2 负载均衡 2.3 任务调度 2.4 异常处理和恢复 2.5 数据存储与处理 2.6 数据清洗和去重 2.7 分布式爬虫 2.8 任务优先级与质量 2.9节能与环保 2.10监控与日志 总结 随着互联网的快…

成都瀚网科技:如何有效运营抖店来客呢?

随着电子商务的快速发展和移动互联网的普及,越来越多的企业开始将目光转向线上销售渠道。其中,抖音成为备受关注的平台。作为中国最大的短视频社交平台之一,抖音每天吸引数亿用户,这也为企业提供了巨大的商机。那么,如…

解决github打开慢的问题

1,修改hosts(可以从这个链接 https://raw.hellogithub.com/hosts 获取对应的host配置)。 140.82.112.3 github.com 151.101.1.194 github.global.ssl.fastly.net 2,刷新dns缓存。 # 打开CMD运行如下命令 ipconfig /flushdns 之…

MATLAB-自动批量读取文件,并按文件名称或时间顺序进行数据处理

我在处理文件数据时,发现一个一个文件处理效率太低,因此学习了下MATLAB中自动读取特定路径下文件信息的程序,并根据读取信息使用循环进行数据处理,提高效率,在此分享给大家这段代码并给予一些说明,希望能为…

小程序新增功能页面

需求背景: 小程序主页面有个报名板块,我打算替换主页面报名板块菜单,迁移到我的页面里面, 替换成资讯栏目,我喜欢分享最新技术,开源课题,IT资讯,本想做成论坛的效果,由于时间问题,先替换添加板块 替换后效果: 模块功能: 添加、修改、删除、查看 文件目录:// 添…

git本地仓库及远端仓库推送【linux】

git本地仓库及远端仓库推送【linux】 一.git上创建仓库二.linux中git三板斧i.检查是否安装gitii.克隆仓库到本地iii.提交到本地仓库iiii.上传到远端仓库 三.其他内容补充git loggit status.gitignore 一.git上创建仓库 已经创建好的可以直接跳到第二步进入到创建仓库界面&…

Qt应用开发(基础篇)——头部视图 QHeaderView

一、前言 QHeaderView类继承于QAbstractItemView,为项目视图(QTableView、QTreeView等)提供标题行或标题列。 树结构视图 QTreeView 表格视图 QTableView 视图基类 QAbstractItemView QHeaderView有section的概念,表示整条标题栏的一个个小部分&#xff…

为小公司申请企业邮箱的步骤和方法

对于小公司来说,拥有自己的企业邮箱不仅可以提高公司的专业形象,还可以更好地管理内部和外部的通信。小公司应该如何申请企业邮箱呢?以下是一份详尽的指南。 小公司应该如何申请企业邮箱呢?基本上由三步组成:确定自己的…

class的get和set

class的get和set 一、使用场景二、代码实现 一、使用场景 当我们需要在用户获取或设置实例某个属性的时候做一些附加的操作的时候,就能利用这个特性。 二、代码实现 class Person {#name #age 0 // 设置私有属性存储值,避免被外部修改constructor(na…

智能微秘书+FastGPT,打造你的超级微信助手!

自 ChatGPT 面世以来,它开创了一种交互方式的新革命,引领着技术与人类交互方式的全新演变。特别是在 GPT-3.5 全面开放 API 接口后,基于语言模型(LLM)的各种应用如雨后春笋般涌现,显示了无限的可能性和创新…

RabbitMQ的LazyQueue

在默认情况下,RabbitMQ会将接收到的信息保存在内存中以降低消息收发的延迟。但在某些特殊情况下,这会导致消息积压,比如: 消费者宕机或出现网络故障消息发送量激增,超过了消费者处理速度消费者处理业务发生阻塞 一旦…

C51--基本认知

单片机基本认知: 1、什么是单片机 单片机是一种集成电路芯片。 把具有数据处理能力的中央处理器 CPU、随机存储器RAM、只读存储器ROM。 多种 I / O 口和中断系统、定时器/计数器等功能(可能还包括显示驱动电路、脉宽调制电路、模拟多路转换器、A/D转换器…

JAVAEE初阶相关内容第十四弹--网络初识

写在前: 这一部分开启网络部分的相关知识,这一弹内容初始网络将主要进行网络相关知识的简单介绍,以及着重介绍协议、协议分层、OSI七层模型、TCP/IP五层模型、封装和分用。 需要认识协议,并知道协议的效果是什么;知道…