基于Python与SQLite的个人财务管理系统设计与实现
发布时间:2026/10/9 3:15:31 锦皓数字建站

前阵子我把自己手上的个人财务管理系统完整重写了一遍项目结构从之前的单文件脚本改成了一套包含 Python 源码、独立数据库文件、详细说明文档的完整工程。起因很简单我一直在用的记账 App 突然改版导出账单变麻烦我又想按自己的分类维度做年度复盘索性就把这套系统彻底梳理了一遍。这套系统核心能力就三块记账、统计、预警。底层是 SQLite 数据库业务逻辑用 Python 处理可视化部分用 matplotlib 出图整体代码量不大但把“增删改查”和“数据统计”这两条主线练得很扎实。适合正在做课程设计、毕业设计或者想给自己的日常收支找个可控管理工具的 Python 学习者参考。先说一个很多人问的问题市面上记账软件那么多为什么要自己写一套我的答案很直接个人财务管理这件事真正麻烦的不是记一笔账而是记账之后的数据怎么用。我用过好几款记账 App记账界面都做得很好但导出数据要么收费要么格式残缺做年度总结时还得手动复制到 Excel。自己写一套就不存在这个问题数据表完全可控想按餐饮、交通、人情往来哪个维度切片都行还能把多年积累的账目一次性倒入数据库里做趋势分析。当然另一个私心是练手个人项目里如果能把数据建模、CRUD 操作、统计报表、异常处理这套链路完整走一遍比刷十遍教程都管用。项目雏形与需求拆解——从记账 App 的小痛点开始的完整方案1.1 为什么选择自研而非直接用现成工具做这个项目前我先列了一页需求核心只有一句话能记收入、能记支出、能按月看分类汇总。但“能记账”这三个字背后藏着不少坑比如分类要不要固定、金额精度怎么处理、月底结余怎么算、历史账单怎么导入。现成工具往往把这些问题封得很死自研则可以完全按自己的口径来。我的建议是先想清楚你要管什么再动手建表。我第一版方案是 Excel 宏表格后来发现数据量到几千行时 Excel 公式卡得很筛选和透视表能做统计但没法做自动预警而且宏代码不好维护所以才转到 Python 数据库这套技术方案。从工程角度看个人财务系统最适合的架构是“本地数据库 脚本/界面调用”。数据库负责持久化存储Python 负责业务逻辑界面组件负责录入和展示。这套架构的好处是每个部分都能单独替换比如今天用命令行交互明天把数据层接上 PyQt 或 Flask 做成网页端业务代码不用重写。这里还有个隐藏的好处数据库里的数据结构和统计口径一旦固定下来后面加什么功能都只是往表里加字段、往外加接口的事不会把账目数据搞乱。1.2 核心功能模块与数据库设计思路我最终确认的功能模块是五个账户管理、交易流水、分类管理、预算预警、统计分析。账户管理负责维护银行卡、现金、虚拟账户的余额交易流水记录每一笔收入和支出包含金额、日期、分类、账户、备注分类管理提供“餐饮”“交通”“工资”“理财收益”这类标签预算预警每个月自动对比预算和已消费金额超支给出提醒统计分析负责按天、按月汇总输出表格和图表。第一版不要贪多这五个模块已经把个人财务管理的闭环覆盖住了。对应到数据库设计先拆成四张核心表accounts、categories、transactions、budgets。accounts 存账户信息categories 存分类树transactions 存流水明细budgets 存每月的预算计划。关键设计点在于 transactions 表通过 account_id、category_id 关联另外三张表这样做有三个好处一是流水数据只存 ID 不重复存文字减少冗余二是修改账户名或分类名后历史流水记录不用跟着改三是统计时可以直接 JOIN 出分类名称和账户名称查询逻辑清晰。我在第一版时图省事把分类名称直接写进 transactions 表后来改分类名时不得不同步更新几百条历史记录踩过一次坑就明白外键关联的意义了。1.3 技术选型Python SQLite 的组合优势技术选型上我几乎没有纠结直接用 Python 3.10 SQLite。Python 的优势不只是写起来快它自带的 sqlite3 模块不需要额外安装数据库驱动处理数据时又能无缝配合 pandas 和 matplotlib这三个库正好覆盖“存取、分析、展示”三件事。SQLite 适合这个场景是因为它本身就是嵌入式数据库一个 .db 文件就能承载十几万条流水日常个人记账完全是降维使用还能通过文件复制直接备份这点对个人项目非常友好。如果未来数据量增长到多用户并发写入再迁移 PostgreSQL 也不晚SQL 语法大部分通用。我也有一些朋友选择 MySQL 或 PostgreSQL原因是他们计划把系统部署到云服务器上多设备同时访问。这个思路没问题但个人财务管理系统的瓶颈从来不是数据库性能而是功能逻辑。先用 SQLite 把功能跑通再考虑迁移成本和风险都更可控。表格对比会更直观一些对比项SQLiteMySQL/PostgreSQL安装复杂度Python 自带模块零配置需要单独安装服务端并发能力适合单用户读写适合多用户并发备份方式直接复制文件导出 SQL 或物理备份适合场景本地单机、课设毕设Web 部署、多人使用数据库设计与核心实现——四张表把账算清楚2.1 表结构设计与字段约束解析数据库设计是整个项目的地基我把最终版的表结构贴出来配合说明每个字段为什么这么定。-- 账户表 CREATE TABLE accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, type TEXT NOT NULL DEFAULT bank, -- bank/cash/other balance REAL NOT NULL DEFAULT 0, created_at DATETIME NOT NULL ); -- 分类表 CREATE TABLE categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, kind TEXT NOT NULL, -- income/expense parent_id INTEGER DEFAULT NULL ); -- 交易流水表 CREATE TABLE transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, account_id INTEGER NOT NULL, category_id INTEGER NOT NULL, amount REAL NOT NULL CHECK(amount 0), trans_type TEXT NOT NULL CHECK(trans_type IN (income, expense)), trans_date DATETIME NOT NULL, note TEXT, FOREIGN KEY (account_id) REFERENCES accounts(id), FOREIGN KEY (category_id) REFERENCES categories(id) ); -- 月度预算表 CREATE TABLE budgets ( id INTEGER PRIMARY KEY AUTOINCREMENT, category_id INTEGER, month TEXT NOT NULL, -- 格式2025-03 limit_amount REAL NOT NULL CHECK(limit_amount 0), UNIQUE(category_id, month) );重点解释 transactions 表的三处设计细节。第一amount 统一存正数用 CHECK(amount 0) 约束同时用 trans_type 字段区分收入还是支出这样统计时不至于出现正负金额混用的逻辑混乱。第二trans_date 用 DATETIME 类型方便按日期范围和月份做 GROUP BY避免字符串格式不统一的麻烦。第三外键约束必须开SQLite 默认不启用外键需要执行一句PRAGMA foreign_keys ON;否则联表删除时会出现孤儿数据。字段长度上我尽量克制name 和 note 没设最大长度因为个人项目数据量小过度限制反而容易在录入时报错。accounts 表里我加了一个 UNIQUE(name)避免同一个账户被重复创建balance 字段代表当前余额后面做余额核验时可以直接拿出来对比。categories 表多了一个 parent_id 字段支持二级分类比如“餐饮”下面可以挂“早餐”“外卖”统计时可以按父分类汇总也可以按子分类细化这个设计在年度总结时非常好用。2.2 连接数据库与增删改查最佳实践数据库写完表之后Python 这边的核心就是连接和 CRUD。sqlite3 的标准连接方式很简单但我建议每次都开连接、用完就关不要长期保持一个全局连接对象个人项目里并发风险低短连接更安全。我习惯把所有数据库操作封装在一个 db 模块里对外只暴露 add_transaction、get_monthly_summary 这类业务函数界面上不出现任何 SQL 语句。下面这个函数是“记一笔支出”的完整流程包含了事务处理这部分很容易写错import sqlite3 def add_expense(account_id, category_id, amount, date_str, note): conn sqlite3.connect(finance.db) try: conn.execute(PRAGMA foreign_keys ON;) cur conn.cursor() # 插入流水 cur.execute( INSERT INTO transactions (account_id, category_id, amount, trans_type, trans_date, note) VALUES (?, ?, ?, expense, ?, ?) , (account_id, category_id, amount, date_str, note) ) # 同步扣减账户余额 cur.execute( UPDATE accounts SET balance balance - ? WHERE id ?, (amount, account_id) ) conn.commit() return True except Exception as e: conn.rollback() print(记账失败已回滚, e) return False finally: conn.close()这段代码里有三个实操要点。第一所有 SQL 都使用?占位符参数通过元组传进去这是防 SQL 注入的标准做法即使个人项目没有攻击者也能避免金额里有引号或特殊字符时语法报错。第二插入流水和更新账户余额必须放在同一个事务里要么都成功要么都回滚否则就会出现流水记了但余额没减的脏状态。我在第一版时没加事务测试时断断续续发现余额对不上排查半天才发现是某个异常分支只提交了流水没提交更新。第三每一条新增、修改、删除操作返回成功还是失败并且要打印异常信息UI 层拿到返回值后给用户明确反馈不要静默吞掉异常。2.3 数据备份、恢复与统计口径说明个人财务数据最怕丢失所以备份机制必须提前设计好。SQLite 的备份最简单直接复制 finance.db 文件即可但注意不要在连接打开时直接复制最好先调用 sqlite3 的 backup API 或者等写入事务结束后再复制。我写了个定时备份脚本每天把 .db 文件复制到 backups 目录文件名词加上日期后缀再压缩一下整个过程不到十行代码。MySQL 场景则用 mysqldump 命令导出 sql 文件后同样可以定期归档。统计口径这块容易被忽略。收入、支出、结余的计算式子并不复杂但日期边界、精度取舍、分类归属要统一。我的月汇总逻辑是支出金额 本月 trans_typeexpense 的流水求和收入同理结余 收入 - 支出不把账户原有余额掺进来。另外金额统一保留两位小数计算时避免直接浮点累加误差。用 SQL 表达就是这样SELECT strftime(%Y-%m, trans_date) AS month, SUM(CASE WHEN trans_type income THEN amount ELSE 0 END) AS total_income, SUM(CASE WHEN trans_type expense THEN amount ELSE 0 END) AS total_expense FROM transactions WHERE trans_date BETWEEN ? AND ? GROUP BY strftime(%Y-%m, trans_date) ORDER BY month DESC;提示strftime(%Y-%m, trans_date)在 SQLite 里专门用来取月度标识比在 Python 里处理完日期再传字符串进去更高效。为什么用 BETWEEN 而不是 MONTH()因为后者只能算某一个月的而跨年统计时一截就出问题。核心代码逻辑与界面实现——从命令行到可视化报表3.1 录入校验与金额处理细节录入模块是整个系统的入口校验规则直接关系到数据库质量。我的规则很简单但很有效金额必须大于 0日期必须是 YYYY-MM-DD 格式分类必须存在且和收支类型匹配账户必须存在。界面层判断完格式数据层再判断约束两层防线缺一不可。我在程序里写了一个通用的验证函数如下from datetime import datetime def validate_expense_input(account_id, category_id, amount, date_str): if not isinstance(amount, (int, float)) or amount 0: return False, 金额必须大于 0 try: datetime.strptime(date_str, %Y-%m-%d) except ValueError: return False, 日期格式应为 YYYY-MM-DD # 分类存在性交由数据库外键约束兜底 return True, 为什么要把这些校验写在入库前因为一旦脏数据进了表后期的统计就会出连锁反应比如金额为负数导致支出汇总变小分类不存在导致 JOIN 出空行。宁可录入时多几行判断也不要在清洗数据时后悔。金额字段我强制要求传浮点数但入库前会做一次 round(value, 2)防止界面传进来 19.999999 这种精度问题。有人问为什么不直接用 Decimal我的回答是个人项目里数据量不大用 Decimal 更严谨但 sqlite3 存取时要额外转类型float round 已经够用只要不是账目大到需要精确到分毫性能上完全没问题。3.2 分类统计与图表可视化统计是个人财务管理系统里最有价值的部分。月度分类支出统计我直接用 GROUP BY 实现然后拿 pandas 做二次加工最后用 matplotlib 画柱状图和饼图。图表部分中文字体是最常见的坑Windows 上默认字体识别不了中文会显示方块字。解决方案是手动指定字体import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [Microsoft YaHei, SimHei] plt.rcParams[axes.unicode_minus] False柱状图的输出逻辑是这样先查当月每个分类的支出总额再按金额降序排列只取前几名展示头部支出结构。饼图则用于看占比如果分类太多就把金额小于阈值的小类合并为“其他”避免饼图碎成十几片看不清楚。这两张图的代码并不复杂核心思路是 SQL 出数据、pandas 转 DataFrame、matplotlib 落图三个环节解耦。如果后面想换成网页版展示只要把 SQL 查询结果转成 JSON 接口前端直接接手即可查询层的代码不用动。3.3 预算预警与超支检查逻辑预算模块的触发逻辑相对独立。每个月首次运行系统时自动读取 budgets 表中当月预算记录没有记录就跳过。然后汇总当月支出与预算对比超过 80% 时给一条黄色提示超过 100% 时给红色警告。实现上不需要什么复杂算法一个简单的判断函数即可def check_budget_overrun(month_str): conn sqlite3.connect(finance.db) cur conn.cursor() cur.execute( SELECT SUM(amount) FROM transactions WHERE trans_typeexpense AND strftime(%Y-%m, trans_date)?, (month_str,) ) used cur.fetchone()[0] or 0 cur.execute(SELECT limit_amount FROM budgets WHERE month?, (month_str,)) row cur.fetchone() conn.close() if not row: return None limit row[0] percent used / limit * 100 if percent 100: return danger, used, limit, percent elif percent 80: return warning, used, limit, percent return ok, used, limit, percent这里的细节在于“没有预算记录”和“预算为 0”要区分开前者返回 None 表示不用检查后者则应视为不允许消费。另外预算表设计为按 category_id 可按分类预警为空时则对整个账户做全局预算两种粒度各有用途。用到实际生活里我一般只给“娱乐”“餐饮”这类弹性支出设预算固定支出不设避免天天弹警告反而麻木。实操流程从零跑通整个系统——包含我踩过的几个坑4.1 环境准备与依赖安装新手拿到源码第一关往往是跑不起来。我的建议是先把 Python 3.9 以上版本装好再用虚拟环境隔离项目依赖。很多人图省事直接用系统 Python 装三方库后面容易把环境搞乱所以我反复强调虚拟环境虽然多敲两行命令但出了问题好收拾。依赖清单不复杂核心就三个pip install pandas matplotlib openpyxlopenpyxl 不是必需的但如果你像我一样要把报表导出成 Excel 文件它就用得上。sqlite3 是 Python 标准库不需要单独装。这里插一句网上很多资料给出pip install mysql-connector-python那是因为他们用的 MySQL 方案如果你跟着本文用 SQLite 完全可以跳过。虚拟环境创建可以用python -m venv venvWindows 下激活命令是venv\Scripts\activatemacOS/Linux 下是source venv/bin/activate激活后命令行前缀会变成 (venv)这时再 pip install 就不会污染全局环境。4.2 初始化数据库并导入历史账单数据库初始化我先执行 schema.sql 建表然后插入默认的账户、分类、预算。很多代码仓库会把建表和插入数据的 SQL 写在一起方便一次性初始化。接着导入历史账单这一步是最容易被低估的因为 CSV 文件里的日期格式不一、金额带千分位符、分类名称和系统不一致导入脚本要写好几层清洗逻辑。我建议直接用 pandas 的 read_csv 把文件读进来再统一处理日期和金额import pandas as pd df pd.read_csv(history.csv, encodingutf-8) df[金额] df[金额].astype(str).str.replace(,, ).astype(float) df[日期] pd.to_datetime(df[日期]).dt.strftime(%Y-%m-%d)数据清理完再逐条插入数据库插入时直接调 add_expense 或 add_income复用同一套校验逻辑。第一次跑导入脚本时我碰到编码问题CSV 文件是 GBK 编码pandas 默认按 utf-8 读直接报错需要把 read_csv 的 encoding 参数改成 gbk。这类问题在实操中很常见遇到不要慌大概率就是编码或分隔符的问题。4.3 启动系统与常见报错速查表整个系统跑起来后我把运行时可能遇到的报错整理成了一张速查表方便自己以后排查也方便使用者快速定位问题报错信息可能原因解决方案ModuleNotFoundError: No module named pandas缺少三方依赖激活虚拟环境后执行 pip install pandassqlite3.OperationalError: table transactions has no column named ...旧库表结构不匹配删除旧库重新初始化或写 ALTER TABLE 增加字段UnicodeDecodeError: utf-8 codec cant decode byte ...CSV 文件编码不是 utf-8read_csv 时指定 encodinggbkdatabase is lockedSQLite 并发写入冲突检查是否有长连接未关闭或改用短连接模式AttributeError: NoneType object has no attribute fetchall查询返回值空先确认 table 是否初始化其次确认查询条件是否有数据图表中的中文显示为方块matplotlib 默认字体缺少中文设置 rcParams[font.sans-serif] 为中文字体排查思路其实有个通用套路先看报错行号定位是数据库层、逻辑层还是可视化层再看数据是否存在很多时候是查询区间没数据导致拿到 None最后看编码和类型Python 对类型比较严格字符串和浮点数混用会出各种隐藏问题。掌握了这个套路90% 的报错都能自己解决。项目文档、打包分发与扩展方向5.1 一份能“换人也能跑”的文档怎么写源码和数据库齐全的项目如果缺了文档换台电脑照样跑不起来。我的文档结构是项目概览、环境要求、安装步骤、数据库初始化、运行方式、常见问题。其中运行方式要写清楚从命令行执行哪个文件启动比如python main.py。数据库初始化也要写明是先执行 schema.sql 还是直接复制附带好的 finance.db避免使用者重复建表导致主键冲突。另外把核心函数的输入输出示例写进文档读者一看就知道 add_expense 应该传什么参数不需要去翻源码。写文档有个技巧按别人的视角写。写完后过一个小时假装自己是从未接触过这个项目的陌生人按文档从头到尾走一遍哪里卡住了就补充哪里。这份文档不求文笔只求可操作。之前我把文档写得像开发笔记充斥着“这里需要注意”却没说注意什么后来全部改成步骤式的说明反而更实用。项目文档不只是给别人看的过半年你自己回来看代码有清晰文档能节省大量回忆时间。5.2 打包成可执行程序与 Web 化改造如果要把这套系统给不懂 Python 的家人用直接丢源码让他们跑命令行显然不现实。我用 PyInstaller 把项目打包成一个 exe命令很简单pip install pyinstaller pyinstaller -F -w main.py-F 参数表示打包成单文件-w 表示不显示控制台窗口。但要注意数据库文件的路径问题exe 运行时工作目录可能和源码目录不同需要把数据库路径改为相对路径或者把 .db 文件复制到指定目录。这个坑我在打包后遇到过exe 启动后找不到 finance.db后来在代码里加了一个自动探测逻辑如果当前目录找不到数据库就在项目根目录下找二者都没有再提示初始化。如果你有多设备同步记账的需求把系统 Web 化是更好的路线。用 Flask 把查询和记账接口暴露出来前端做一套简单的页面数据层完全可以复用现有的 db 模块。迁移成本主要集中在接口和前端交互上财务逻辑不用动。再往后可以在内网或者云服务器上挂服务手机浏览器访问地址即可记账。这条路我目前只做到一半但踩过的坑告诉我数据库设计保持稳定是Web化最大的红利。5.3 值得继续扩展的功能清单项目做完不等于结束我给这套系统列了一个后续扩展清单按优先级排序第一是导入银行流水现在很多银行支持导出 CSV 交易明细如果能直接识别并映射到分类记账成本会大幅降低第二是定时账单把房租、信用卡、话费这类固定支出做进待办提醒第三是资产趋势图按月画出净资产变化曲线这对判断消费习惯很有价值第四是预算分类细化将预算预警精确到每个分类而不是只看总支出。这些功能都不需要改表结构基本都是新增查询和界面入口。我个人对这套项目最大的体会是个人财务管理系统的难度不在技术而在对自己真实需求的抽象。技术层面无非是建表、增删改查、汇总统计可一旦把需求想清楚代码写起来是很快的。如果你也是想拿 Python 练手我的建议是先别追求大而全把一张流水表和一张统计报表打通你就能真实感受到从数据到决策的完整链路这比看任何教程都有用。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。