用Python和OpenAI API打造自动记账工具:文本抽取与消费分类实战
2026/9/20 12:40:03 网站建设 项目流程

简介:这个基于OpenAI与Python开发的自动记账工具,面向需要提升财务记录效率的个人用户及中小企业,利用自然语言处理能力从银行流水、电子账单与PDF发票中提取日期、金额、类别等关键信息,自动生成结构化财务记录。压缩包共51个文件,以36个Python脚本为核心,辅以YAML配置文件、JSON/INI配置及测试用例,体积仅37KB,整体结构清晰,便于二次开发与部署。已有230人浏览学习。配套内容覆盖数据获取、OpenAI接口调用、信息抽取、数据验证及可视化报告生成等完整流程,并包含API日志、自定义路由、模型管理等模块,适合具备一定Python基础的开发者快速搭建智能记账原型,并进一步扩展为生产级工具。 记账这件事,很多人坚持不下来,不是因为懒,而是流程实在太繁琐。每次消费之后要手动记一笔,还得想这笔钱该归到哪个类目,月底再对着账单来回核对,光这个动作就能劝退一大半人。我这次做的这个小工具,核心就解决一个问题:把“记一笔”这个动作,简化成“丢一段文字或者一条账单记录过去”,剩下的金额抽取、消费分类、入库存储全部交给程序完成。它基于 Python 实现,识别和分类能力来自 OpenAI 接口,整个项目核心代码只有两三百行,适合有 Python 基础的开发者拿来练手,也适合真想解决记账痛点的人改造成自己的日常工具。

我花了一个周末把核心流程跑通,实测识别精度和分类合理度都超出预期。如果你也想做一个类似的自动化工具,或者单纯想看看 Python 怎么跟 OpenAI 接口配合,这篇博文会把我的设计思路、完整实现步骤、Prompt 写法和踩过的坑全部摊开来讲。

1. 项目整体设计与技术选型思路

1.1 为什么选 OpenAI + Python 这套组合

做自动记账工具有很多技术路线。传统做法是写规则:用正则表达式匹配“支付宝”“微信支付”“消费XX元”之类的关键词,再用一个映射表把商家名对应到消费类别。这种方案在固定格式下能用,但一旦账单格式变化,比如从微信换到支付宝,或者账单描述里出现了没见过的商家,规则就崩了,而且商家到类目的映射表会越维护越累。

用 OpenAI 接口做,最大的好处是绕开了“写规则”这件事。模型本身具备信息抽取和语义理解能力,你只要给它一段原始文本,它就能把金额、时间、商家、消费类目这几个关键字段抽出来。商家叫“沙县小吃”还是“某某咖啡店”它都能理解是餐饮消费,不需要你预先维护任何一个关键词映射表。改格式也只是换一段文本输入的事,不需要改程序逻辑。

Python 在这里的角色是“胶水层”:负责读取账单来源、调用接口、解析返回值、写入数据库、生成统计报表。Python 好就好在生态齐全、标准库够用,sqlite3、json、datetime 这些开箱即用,不需要额外引一堆重型依赖。做原型验证的时候,Python 的迭代速度比 Java 或 Go 高不少,这是它在这个项目里最大的优势。

1.2 整体工作流程设计

整个系统我设计成了四个环节,串起来就是一条完整的数据处理链路:

  • 输入层:接收用户手输的消费描述、支付平台导出的账单文本片段,后续还可以扩展出 OCR 识别图片账单的入口。
  • 抽取层:把原始文本交给 OpenAI 模型,让模型返回结构化的 JSON,包含金额、时间、商家、类目四个字段。
  • 存储层:把结构化数据写入本地 SQLite 数据库,保留原始文本作为追溯依据。
  • 分析层:从数据库里做月度汇总、消费分布统计,输出给用户看。

这个架构可以用一个生活类比来理解:OpenAI 相当于一个理解能力很强的记账员,你说“昨天中午在公司楼下吃了碗牛肉面,花了25”,它就能写成“25 元,餐饮消费,昨天中午”。Python 则是帮这个记账员把每一笔记录抄进账本、月底算总账的那个人。两边各干各的活,职责很清楚。

关键点在于:不要让 Python 做语义理解,也不要让模型做数值计算和数据管理。语义理解交给模型,结构化数据处理交给代码,各取所长,整个系统才稳定。

2. 环境搭建与依赖准备

2.1 Python 环境与项目初始化

建议使用 Python 3.9 以上版本,主要是为了能用上更完善的类型注解和字典合并语法,但这不是硬性要求,3.8 也完全能跑。我是用 venv 做的隔离环境,避免把包装到全局污染系统环境。

python3 -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install openai python-dotenv

这里插一句,项目根目录下一定要建一个.env文件存放 API Key,配合python-dotenv读取。硬编码 API Key 这事我干过一次,后来把仓库推到远端才反应过来,虽然马上删掉并轮换了密钥,但那种后背发凉的感觉不想体验第二次。API Key 务必视为密码,不要把包含密钥的代码提交到任何远程仓库。

2.2 OpenAI SDK 接入与模型选择

新版openaiSDK 的写法比旧版简洁很多,直接实例化OpenAI客户端,然后调用chat.completions.create就完事。初始化的时候只需要传api_key,别的都用默认值。

模型我选的是gpt-4o-mini。记账这件事不是高难度推理任务,它的关键是:抽取字段准、按类别归类合理、输出格式稳定。这几个维度gpt-4o-mini都能胜任,而且速度快、成本低。我在测试阶段也试过更大的模型,说实话在分类准确率上差异不大,考虑到批量导入账单时的调用量,性价比优先更实在。

from openai import OpenAI import os from dotenv import load_dotenv load_dotenv() client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), timeout=30)

timeout=30这个参数值得单独说。默认情况下 SDK 的超时时间比较保守,账单多的时候接口响应变慢,很容易触发超时异常。设成 30 秒后,绝大多数请求都能在超时前返回,遇到个别慢请求也不至于卡死整个程序。

2.3 完整依赖清单

这个项目用到的第三方库很少,我列一下当时的requirements.txt

openai>=1.30.0 python-dotenv>=1.0.0

就两个。SQLite 用标准库sqlite3,数据清洗用标准库jsonre,日期处理用datetime。整个项目在新增依赖方面的负担几乎为零,这也是我一开始就决定的:能用标准库解决的,不额外引入第三方包。一方面减少安装出问题的概率,另一方面代码在任何机器上都能直接跑,不用折腾环境。

3. 自动记账核心功能实现

3.1 账单文本的信息抽取:Prompt 怎么设计

Prompt 是整个工具的灵魂。做自动记账,Prompt 需要让模型稳定地完成三件事:抽取字段、输出 JSON、遵守类别约束。

先说抽取字段。用户可能输入“昨天午饭 25”“支付宝 4月2日 滴滴打车 18.5元”“工资 8000”等等,格式千奇百怪,但模型的语义理解能力可以从中提炼出统一的字段结构。我在 Prompt 里明确告诉模型要抽取四个字段:金额、时间、商家、类别,缺省的字段怎么补。

你是一个私人的记账助手。用户会给你一段消费记录,可能是随手输入的文本, 也可能是从支付平台复制过来的账单片段。 任务: 1. 抽取消费金额并转为数字,比如 "23.5元" -> 23.5 2. 抽取消费时间,格式为 YYYY-MM-DD;如果用户没有提供时间,使用 today 3. 抽取商家或消费项目名称;如果无法识别,使用"未知" 4. 判断这笔记录的类别,只能从候选类别里选择:餐饮、交通、购物、居住、 娱乐、医疗、教育、人情、收入、其他 输出要求: - 只输出合法的 JSON 对象,不要输出任何解释文字 - JSON 字段固定为:amount, time, merchant, category 候选类别说明: - 餐饮:外卖、餐厅、咖啡奶茶、超市购买食品饮料也归这里 - 交通:地铁、公交、打车、加油、停车 - 购物:服饰、数码、日用品、网购 - 居住:房租、水电燃气、物业、宽带 - 娱乐:电影、游戏、健身、旅游 - 医疗:药品、医院、体检 - 教育:课程、书籍、培训 - 人情:请客、红包、礼物 - 收入:工资、退款、转账收入 - 其他:以上都不属于的情况 用户输入: {user_input}

这段 Prompt 看起来长,但每个部分都有明确作用。类别说明不是摆设,它直接决定了分类的准确率。“超市买食品饮料归餐饮”这种约定如果不写清楚,模型很容易把超市购物全归到“购物”里。少一行说明,分类结果就会有偏差,这就是 Prompt 细节的价值。

3.2 消费分类:让模型按你的规则做事

分类这件事,关键不是让模型“自由发挥”,而是让它“在规定里发挥”。上面那段 Prompt 已经做了两件事来约束分类:候选类别固定,且每个类别给了正例说明。

实际操作中还有一个隐藏问题:模型偶尔会返回一个不在候选类别里的类别。比如你定义了 10 个类别,它返回“饮食”而不是“餐饮”,这时候程序需要一个兜底逻辑。我的做法是做一个白名单校验,不在白名单里的强制改成“其他”。

ALLOWED_CATEGORIES = ["餐饮", "交通", "购物", "居住", "娱乐", "医疗", "教育", "人情", "收入", "其他"] def fix_category(category: str) -> str: if category in ALLOWED_CATEGORIES: return category return "其他"

别小看这四行代码。它保证无论模型怎么抽风,最终入库的类别永远是预设集合里的一个,后续做月度统计、图表分析时才不会出现“饮食”“餐饮”“吃饭”三个值其实是同一类的情况。数据口径统一了,报表才可信。

3.3 数据入库与月度汇总

数据库我用 SQLite,单文件部署、零配置,记账工具这个量级的数据完全够用。表结构设计得也很简单:

import sqlite3 def init_db(db_path="accounting.db"): conn = sqlite3.connect(db_path) conn.execute(""" CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, amount REAL NOT NULL, time TEXT NOT NULL, merchant TEXT, category TEXT NOT NULL, raw_text TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) """) conn.commit() return conn

这里我把raw_text原始输入也存了下来。好处是任何时候发现某条记录分类不对,都能回溯原始文本,定位是模型的问题还是输入的问题。这个设计在调试阶段帮了我大忙。

月度汇总用的是标准 SQL 聚合,不做任何花哨操作:

def monthly_summary(conn, year_month: str): rows = conn.execute(""" SELECT category, COUNT(*) as cnt, SUM(amount) as total FROM records WHERE substr(time, 1, 7) = ? GROUP BY category ORDER BY total DESC """, (year_month,)).fetchall() return rows

取到结果后可以直接打印成纯文本报表,也可以配合 pandas 做进一步分析。记账工具到这个程度,核心链路已经完整了:输入文本、抽取字段、归类、入库、统计。

4. 完整实操:从账单文本到分类记账

4.1 主流程代码演示

把上面的模块串起来,主流程其实很简洁。核心函数就一个:接收文本,返回结构化记录。

import json from openai import OpenAI from datetime import date from dotenv import load_dotenv import os load_dotenv() client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), timeout=30) PROMPT_TEMPLATE = """ 你是一个私人记账助手。请从用户输入的文本中抽取记账信息。 金额转成数字,时间格式化为 YYYY-MM-DD(没有时间就用 {today})。 类别只能从以下列表中选择:餐饮、交通、购物、居住、娱乐、医疗、教育、人情、收入、其他。 只输出 JSON,包含字段:amount, time, merchant, category。 用户输入: {user_input} """ def parse_and_classify(user_input: str) -> dict: prompt = PROMPT_TEMPLATE.format(today=date.today().isoformat(), user_input=user_input) resp = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "你是一个严谨的记账助手,只输出结构化 JSON。"}, {"role": "user", "content": prompt}, ], temperature=0, response_format={"type": "json_object"}, ) data = json.loads(resp.choices[0].message.content) data["amount"] = float(data["amount"]) if data.get("category") not in ALLOWED_CATEGORIES: data["category"] = "其他" return data

temperature=0是必须写的。记账这种事需要确定性优先,不需要创造性。温度越高模型输出越随机,同一个输入两次分类可能不一样,这在记账工具里是不允许的。response_format={"type": "json_object"}则是在接口层面强制模型输出 JSON,比让模型自由发挥再靠正则去解析结果稳得多。

主函数读取输入、调用抽取、写库:

# 测试 if __name__ == "__main__": test_inputs = [ "昨天中午在楼下吃了碗牛肉面 25", "支付宝 2025-05-02 滴滴打车 18.5元", "招商银行工资入账 8000", "淘宝买了双跑鞋 399", ] conn = init_db() for item in test_inputs: record = parse_and_classify(item) conn.execute( "INSERT INTO records (amount, time, merchant, category, raw_text) VALUES (?, ?, ?, ?, ?)", (record["amount"], record["time"], record["merchant"], record["category"], item), ) conn.commit() for row in monthly_summary(conn, "2025-05"): print(row)

跑一轮,看看模型返回的真实效果。

4.2 实测效果与 API 成本估算

我用上面四段输入做了实测,gpt-4o-mini返回结果如下:

原始输入金额时间商家类别
昨天中午在楼下吃了碗牛肉面 2525.02025-05-03楼下牛肉面餐饮
支付宝 2025-05-02 滴滴打车 18.5元18.52025-05-02滴滴出行交通
招商银行工资入账 80008000.02025-05-05招商银行收入
淘宝买了双跑鞋 399399.02025-05-04淘宝购物

四笔全部正确识别,金额转换、时间格式化、类别归类都没问题。最让我意外的是“楼下牛肉面”这种口语化描述,模型也能正常处理,这要是写正则,光匹配规则就得写十几行。

成本方面我也算过一笔账。gpt-4o-mini单次请求的 token 消耗大约在 200 到 400 token 之间,输入占大头,输出因为限定 JSON 格式所以很精简。按照一个月记 300 笔消费来算,总 token 消耗不到 12 万,费用可以忽略不计。对比记账 app 的订阅费、或者你自己每周末花半小时整理账目的时间成本,这个工具基本是零成本高收益。

4.3 批量场景扩展与命令行封装

单条输入跑通之后,我顺手做了一个命令行批量处理版本:读取导出的账单文件,把每一行当成一条记录丢给接口处理,最后统一入库。

import sys def process_file(file_path: str, conn): with open(file_path, "r", encoding="utf-8") as f: lines = [line.strip() for line in f if line.strip()] for line in lines: try: record = parse_and_classify(line) conn.execute( "INSERT INTO records (amount, time, merchant, category, raw_text) VALUES (?, ?, ?, ?, ?)", (record["amount"], record["time"], record["merchant"], record["category"], line), ) print(f"[OK] {record['category']} {record['amount']} {record['merchant']}") except Exception as e: print(f"[FAIL] {line} -> {e}", file=sys.stderr) conn.commit()

这里加了异常捕获,某一行失败了不会让整个程序中断,而是打印失败信息后继续。批量处理任务最怕的就是“一条脏数据搞挂整个任务”,这种策略能保证 1000 条记录里有几条异常时,剩下的 990 几条照样正常处理。失败记录可以单独输出,后续手动补录或者修复格式后重新跑。

5. 常见问题与避坑指南

5.1 API 调用层的坑

我实际开发中遇到的第一个坑是返回内容解析失败。虽然用了response_format强制 JSON,但偶尔模型还是会返回带代码块标记的内容,比如```json {...} ```。这时直接json.loads会抛异常。稳妥做法是先做一层预处理,把代码块标记去掉再解析。

第二个坑是网络超时。账单批量导入时接口响应明显变慢,不加timeout和重试机制的话,程序很容易中断。我的经验是设置 30 秒超时,再做最多三次重试,每次重试间隔递增。OpenAI 的 Python SDK 底层已经支持retry,但你最好在业务层面也做一次控制。

第三个坑比较隐蔽:API Key 权限不够会调用失败。有些 Key 只能访问特定模型,换成新模型时突然报 404。排查方式很简单,先做一个最简请求确认 Key 本身没问题,再怀疑上游配置。

5.2 分类结果不稳定怎么办

曾经有段时间我换了 Prompt 写法,分类就开始乱跳,同一个“超市买牛奶”有时候归餐饮,有时候归购物。排查下来发现是我在 Prompt 里去掉了“超市购买食品饮料归餐饮”这行说明。

模型分类本质上是在做语义概率判断,它不知道你的个人偏好是什么。想让分类稳定,有三个手段:第一,Prompt 里把每个类别的边界写清楚,正例越具体越好;第二,temperature设为 0;第三,对模型返回的类别做白名单校验,不在名单里的强制归“其他”。前三板斧下去,分类稳定性基本能解决九成问题。

如果这三个手段都做了还是不满意,那就上少样本示例,在 Prompt 里给两三个完整的输入输出对,告诉模型“长的就是这样的范式”。Few-shot 对分类准确率的提升非常明显,代价只是每次请求多消耗一点 token。

5.3 隐私、安全与数据自重

自动记账工具会接触到真实的收入信息和个人消费习惯,这类数据比普通聊天记录敏感得多,所以隐私处理是必须考虑的部分。

我的建议是:第一,所有数据默认存在本地 SQLite,不要为了“方便”把数据传到任何云端服务;第二,发给接口的文本尽量只保留必要信息,能从原始账单里只截取“金额+时间+商家”就只发这三样,不要带上银行卡号、订单号、手机号等无关字段;第三,API Key 永远放在环境变量或 .env 文件里,加入.gitignore,绝对不提交到代码仓库。第四,如果你要做团队分享或开源,记得用脱敏的假数据测试,不要拿真实账单当示例。

把这些事项当成默认约束,而不是事后补救,工具才能真正日常用起来。我自己的做法是本地建了一个专用记账目录,脚本、数据库和 .env 都在里面,整个目录不做任何云端同步。

一点个人体会

做完这个工具到现在,我养成了一个新的记账习惯:每晚睡前打开命令行,把当天的几笔大额消费随手敲进去,剩下的交给程序处理。虽然每次还是要花十几秒输入,但比起以前拿起手机又放下、月底账单拉出来完全不想看的拖延心态,已经轻松太多了。工具真正改变的不只是记账效率,而是让人愿意去记账了。

如果你也想照着做,我的建议是从最小闭环开始:先不追求界面、不追求批量导入,用命令行把“输入一行文本 -> 返回一条记录”这条链路跑通,感受一下整套流程顺不顺。等核心体验满意了,再去加 Web 界面、加定时任务、加图表统计。方向上还可以继续扩展:接入 OCR 识别支付截图、用 embedding 做更细的子类目推荐,甚至做成定时任务自动处理每天导出的账单。这个项目的乐趣恰恰在于,它是一块可以不断往上搭积木的底子。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询