2026/9/20 12:40:08

用Python和OpenAI API打造自动记账工具:文本抽取与消费分类实战

用Python和OpenAI API打造自动记账工具:文本抽取与消费分类实战 简介这个基于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_keyos.getenv(OPENAI_API_KEY), timeout30)timeout30这个参数值得单独说。默认情况下 SDK 的超时时间比较保守账单多的时候接口响应变慢很容易触发超时异常。设成 30 秒后绝大多数请求都能在超时前返回遇到个别慢请求也不至于卡死整个程序。2.3 完整依赖清单这个项目用到的第三方库很少我列一下当时的requirements.txtopenai1.30.0 python-dotenv1.0.0就两个。SQLite 用标准库sqlite3数据清洗用标准库json和re日期处理用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_pathaccounting.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_keyos.getenv(OPENAI_API_KEY), timeout30) 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(todaydate.today().isoformat(), user_inputuser_input) resp client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是一个严谨的记账助手只输出结构化 JSON。}, {role: user, content: prompt}, ], temperature0, 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 datatemperature0是必须写的。记账这种事需要确定性优先不需要创造性。温度越高模型输出越随机同一个输入两次分类可能不一样这在记账工具里是不允许的。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, encodingutf-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}, filesys.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 做更细的子类目推荐甚至做成定时任务自动处理每天导出的账单。这个项目的乐趣恰恰在于它是一块可以不断往上搭积木的底子。本文还有配套的精品资源点击获取