news 2026/10/2 9:09:17

基于Python与SQLite的个人财务管理系统开发实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Python与SQLite的个人财务管理系统开发实战

1. 项目设计与技术选型

1.1 需求分析:记账工具到底需要解决什么问题

记账这件事,大多数人坚持不了几天,不是因为懒,而是因为“记账”和“看账”被割裂了。随手在手机备忘录里记了几笔,月底想看开销结构,还得自己打开Excel抄一遍分类再拉透视表,繁琐到劝退。我做这套基于Python的个人财务管理系统时,第一原则就是“录入之后无需再做任何手工整理”,所有统计、汇总、可视化全部自动完成。

功能清单我一开始列得并不长,只挑最核心的部分:

  • 收支录入,字段控制在最少:分类、金额、日期、备注
  • 分类管理,收入和支出分类分开维护
  • 月度汇总,自动算总收入、总支出、结余
  • 可视化,月度收支趋势折线图、分类占比饼图
  • 数据导出,CSV备份

像预算预警、多人账户、信用卡账单导入这些,都属于第二版功能,第一版不做。原因很简单:功能越多,代码调试成本和维护成本越高,对一个练手项目来说,完成闭环比堆功能重要得多。如果你刚学完Python基础,想找一个既能练手、又能真正用起来的项目,这套系统的复杂度非常合适——有数据库操作、有业务逻辑、有界面交互,但每一块都不至于把人劝退。

1.2 技术选型:为什么是Python、SQLite、Tkinter

先说语言。Python在这种个人工具类项目里的优势是压倒性的:脚本型语言迭代快,核心逻辑几百行就能写完;生态里有matplotlib、pandas这些现成库,统计和绘图完全不用自己造轮子。数据库连接也有标准库sqlite3,不需要额外装驱动。

数据库层面,我选的是SQLite而不是MySQL,核心原因是“零配置”。SQLite就是一个单文件数据库,不需要安装服务、不需要管用户名密码,连接即用。对个人桌面应用来说,性能和容量完全够用,几万条收支记录查询毫秒级返回。有人担心SQLite太小众,其实它是Python标准库内置的,而且如果将来要放到服务器上多人共用,把连接层从SQLite切到MySQL,改动成本也不大,因为SQL语句本身是通用的,业务逻辑不用动。

界面框架我用了Tkinter。它是Python标准库自带的GUI工具包,最大的好处是不用额外安装、跨平台表现一致,对表单输入、列表展示、按钮触发这类简单场景足够。如果你嫌它外观朴素,可以换成PyQt或PySide,但那些包体积大、学习曲线陡,第一版没必要上。也有人喜欢做成本地Web版,用Flask包一个网页当界面,那是另一种思路,以后可以单独展开。我的建议是:先选定一套能跑通全流程的方案,界面好看不好看不影响核心逻辑的价值。

层面方案优势劣势
语言Python生态丰富、开发迭代快运行性能弱于C++/Java
数据库SQLite单文件、零配置、内置支持不适合高并发场景
界面Tkinter标准库自带、部署简单美观度一般

1.3 项目源码结构规划

源码的组织方式直接影响后续维护心情。我见过很多课设项目把所有代码塞进一个几百行的main.py里,界面逻辑、数据库代码、业务计算混在一起,改一个字段要翻半天的代码。这套系统我按职责拆成了几个文件:

personal-finance/ ├── main.py # 程序入口与界面布局 ├── db.py # 数据库连接与初始化 ├── category_dao.py # 分类相关的增删改查 ├── transaction_dao.py # 收支记录相关的增删改查 ├── report.py # 统计汇总与图表绘制 ├── docs/ │ ├── requirements.md # 需求说明 │ ├── design.md # 设计文档 │ └── user_manual.md # 使用手册 └── finance.db # SQLite数据库文件

数据访问单独放一层,界面代码只管布局和交互,数据库代码只管SQL,中间通过函数调用衔接。这样做的好处是:改界面不会碰到数据逻辑,加统计功能不用动现有页面。这个分层习惯即使将来换Web框架也一样适用,算是我在项目里最想强调的一点。

2. 核心功能拆解与数据库设计

2.1 功能模块的完整闭环:从记账到报表

整套系统的流程大概是:录一笔账 → 写入数据库 → 月度汇总自动更新 → 可视化图表刷新 → 一键导出CSV备份。闭环的意思是每个功能都不是孤立的,录入的每一笔数据最终都会汇总到统计和图表里,不需要用户手动去更新任何中间结果。

录入模块是入口,表单包含日期、分类、金额、类型和备注,点击保存后做数据校验再入库。分类管理负责维护收入和支出两个维度的分类列表,系统里会预置一批常用分类,用户也可以随时加。统计模块按月份聚合数据,算出收入合计、支出合计和结余,同时按分类聚合支出,得到每个分类的花费占比。可视化模块把统计结果画成折线图和饼图,月度趋势看收入支出的变化,饼图看各类支出占比。CSV导出则是把查询结果写成本地文件,用于备份和用Excel做进一步分析。

这里有一个设计上的经验:业务逻辑不要写在界面的事件回调里。比如“点击统计按钮”这件事,正确做法是按钮回调只负责调用report.py里的函数,拿到结果再刷新界面。否则统计代码和界面代码耦合后,很难单独测试,也不方便复用。

2.2 数据库表结构设计

数据库设计是这套系统的地基。我建了三张表:categories(分类表)、transactions(收支流水表)、budgets(预算表),建表SQL如下:

CREATE TABLE IF NOT EXISTS categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, type TEXT NOT NULL CHECK(type IN ('income', 'expense')), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, category_id INTEGER NOT NULL, type TEXT NOT NULL CHECK(type IN ('income', 'expense')), amount REAL NOT NULL, note TEXT, trans_date DATETIME NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES categories(id) ); CREATE TABLE IF NOT EXISTS budgets ( id INTEGER PRIMARY KEY AUTOINCREMENT, category_id INTEGER NOT NULL, monthly_limit REAL NOT NULL, month TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(category_id, month), FOREIGN KEY (category_id) REFERENCES categories(id) ); CREATE INDEX IF NOT EXISTS idx_trans_date ON transactions(trans_date); CREATE INDEX IF NOT EXISTS idx_trans_category ON transactions(category_id);

几个关键选择的理由:

  • 日期字段用DATETIME类型,不用字符串。原因是数据库层面的日期排序、范围查询(比如查某个月的数据)都依赖类型本身的比较规则,字符串月份格式虽然也能排序,但一旦格式不统一就会出问题。
  • type字段用CHECK约束限定只能是income或expense,从数据库层面挡住非法数据。应用层再怎么校验也不如数据库兜底来得稳。
  • trans_date和category_id建了索引,因为月度统计和分类聚合是最频繁的查询,索引能把扫描范围缩小一个量级。
  • budgets表里加了UNIQUE(category_id, month),意思是同一个分类同一个月份只能有一条预算记录,重复写入直接冲突,省得在代码里判断“是否存在”。

2.3 数据库初始化与预置数据

首次运行系统时,数据库文件还不存在,这时需要执行初始化脚本。初始化做了三件事:连接数据库、执行建表SQL、写入预置分类数据。预置分类不能太粗,也不能太细,我按实际记账体验整理了一套:

  • 支出分类:餐饮、交通、居住、购物、娱乐、医疗、教育、其他
  • 收入分类:工资、奖金、理财收益、其他

初始化函数的写法:

import sqlite3 DB_PATH = "finance.db" def init_db(): with sqlite3.connect(DB_PATH) as conn: conn.executescript(DDL_SQL) categories = [ ("餐饮", "expense"), ("交通", "expense"), ("居住", "expense"), ("购物", "expense"), ("娱乐", "expense"), ("医疗", "expense"), ("教育", "expense"), ("其他支出", "expense"), ("工资", "income"), ("奖金", "income"), ("理财收益", "income"), ("其他收入", "income"), ] for name, ctype in categories: conn.execute( "INSERT OR IGNORE INTO categories(name, type) VALUES(?, ?)", (name, ctype) )

用INSERT OR IGNORE而不是普通INSERT,是为了让初始化脚本可重复执行,多跑几次也不会报重复数据错误。using with sqlite3.connect 这种写法,事务会自动提交,遇到异常自动回滚,简单可靠。数据库初始化脚本放到db.py里,入口main.py启动时调用一次init_db()即可。

3. 实操过程与核心代码实现

3.1 环境准备:从Python安装到依赖配置

拿到源码后想跑起来,第一步是准备Python环境。推荐用3.8以上版本,我开发时用的是Python 3.10。安装过程其实没啥技术含量,重点是用虚拟环境隔离依赖,避免和系统Python环境互相污染。

python -m venv venv source venv/bin/activate # Windows下执行 venv\Scripts\activate pip install matplotlib pandas

如果你在国内的环境,pip下载速度可能很慢,可以临时指定国内镜像源:

pip install matplotlib pandas -i https://pypi.tuna.tsinghua.edu.cn/simple

matplotlib用来画统计图表,pandas是数据处理工具,这两个是除了标准库之外仅有的外部依赖。Tkinter是标准库自带的,不用单独装,但在部分Linux发行版上需要手动安装python3-tk包,否则import tkinter会报错。Windows和macOS的官方Python安装包一般都自带。

依赖装好后,执行python db.py初始化数据库,然后python main.py启动界面。整个项目不依赖任何外部服务,本机就能跑,这也是个人财务管理系统比服务端项目更适合练手的原因。

3.2 核心代码:数据库增删改查的落地写法

数据访问层的代码是整个系统的核心。我做这套系统时最重要的一条编码经验就是:一律用参数化查询,不要用字符串拼接SQL。虽然本地单机应用被注入攻击的风险不大,但字符串拼接SQL还有一个隐性坑——金额、日期这类字段值里如果带了引号或特殊字符,拼接出来的SQL就会语法错误,程序直接崩溃。

录入一笔收支记录的函数:

def add_transaction(category_id, trans_type, amount, note, trans_date): with sqlite3.connect(DB_PATH) as conn: conn.execute( "INSERT INTO transactions(category_id, type, amount, note, trans_date) " "VALUES(?, ?, ?, ?, ?)", (category_id, trans_type, amount, note, trans_date) )

查询某个月份的汇总数据:

def monthly_summary(year, month): start_date = f"{year}-{month:02d}-01" if month == 12: end_date = f"{year + 1}-01-01" else: end_date = f"{year}-{month + 1:02d}-01" with sqlite3.connect(DB_PATH) as conn: row = conn.execute( "SELECT " "COALESCE(SUM(CASE WHEN type='income' THEN amount END), 0) AS income, " "COALESCE(SUM(CASE WHEN type='expense' THEN amount END), 0) AS expense " "FROM transactions WHERE trans_date >= ? AND trans_date < ?", (start_date, end_date) ).fetchone() return {"income": row[0], "expense": row[1], "balance": row[0] - row[1]}

这里有一个容易忽略的点:查询月份区间用的是左闭右开,即>= start_date和< end_date。这样处理的原因是下个月1日的0点整点不好算,但< next_month_01能天然把本月最后一天包含进来,逻辑最干净。COALESCE包裹SUM是为了防止没有记录时返回NULL,导致后续计算报错。

编辑和删除记录同理,删除除了删transactions表里的记录,还要考虑是否有统计缓存要重建——这个项目里没有缓存,所以直接删即可。增删改查合起来就是数据层最基本的四个函数:add、update、delete、query,业务层调用这四个函数,界面也调用这四个函数,整体结构非常清晰。

3.3 统计与可视化:用matplotlib把账目画出来

统计是个人财务管理系统最出效果的部分。前面monthly_summary函数已经算好了数据,可视化就是把结构化的数字变成图表。我用了matplotlib,先看月度收支趋势的折线图:

def draw_trend(data): import matplotlib.pyplot as plt months = [d["month"] for d in data] incomes = [d["income"] for d in data] expenses = [d["expense"] for d in data] plt.figure(figsize=(9, 5)) plt.plot(months, incomes, marker="o", label="收入") plt.plot(months, expenses, marker="o", label="支出") plt.title("月度收支趋势") plt.xlabel("月份") plt.ylabel("金额") plt.legend() plt.xticks(rotation=45) plt.tight_layout() plt.savefig("trend.png")

很多人在这一步踩坑:matplotlib默认字体不支持中文,plt.title("月度收支趋势")执行后图上全是方框。解决办法是在绘图前设置字体:

plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"] plt.rcParams["axes.unicode_minus"] = False

第二行axes.unicode_minus设置的负号显示问题。如果你的系统装了中文字体,设置完后中文就能正常渲染。Linux服务器上一般没有微软雅黑和黑体,可以用plt.rcParams["font.sans-serif"] = ["WenQuanYi Zen Hei"]或者先fc-list :lang=zh查看可用字体。

分类占比饼图的代码更简单,核心是算出各类支出占比后传入plt.pie,强调一块是否超预算可以加参数explode突显。画完图直接保存成PNG文件,界面里再用Label加载图片展示,这样避免Tkinter和matplotlib的嵌入机制纠缠,实现起来最稳。

3.4 主程序与界面逻辑

界面层我用Tkinter做了个单窗口布局:顶部是录入表单,中间是条目列表,底部是统计按钮和导出按钮。表单用了StringVar绑定输入框内容,分类用ttk.Combobox下拉选择,金额输入框只允许输入数字和一个小数点,通过validate回调实现:

def validate_amount(text): if text == "": return True try: float(text) return True except ValueError: return False

保存按钮的回调函数负责收集表单数据、调用transaction_dao.add_transaction,成功后清空表单并刷新下面的列表。刷新列表其实就是重新查询数据库,再次显示在Treeview控件里。这个“表单录入→保存→刷新列表”的模式,是绝大多数桌面录入工具的基本交互范式。

界面代码里需要特别注意的一点是:不要在UI线程里做耗时操作。本项目的SQLite查询都是毫秒级,不会卡界面,但如果以后接了远程数据库或大数据量报表,就要考虑用线程把查询放到后台,否则点击按钮后整个窗口会无响应几秒钟。

4. 常见问题与排查技巧实录

4.1 SQLite数据库打不开、表不存在

跑源码时最常见的报错是sqlite3.OperationalError: no such table: transactions。出现这个报错基本可以断定是没执行初始化,或者程序连错了数据库文件。SQLite连接时会自动创建空的.db文件,所以如果db.py里的DB_PATH和查询函数里的DB_PATH不一致,就会出现一个文件里建了表、另一个文件里查数据的情况。

排查思路分三步:

  1. 先确认代码里所有DB_PATH指向同一个路径,最好在db.py里定义一个全局常量,其他文件统一from db import DB_PATH。
  2. 运行一次python db.py,确认finance.db文件生成且大小不是0字节。
  3. 用sqllite命令行或数据库工具打开finance.db,执行.tables看表是否存在。

另外要注意:不要把建表SQL写在业务查询函数里反复执行。虽然IF NOT EXISTS能防重复,但每次查询前都执行一次DDL是浪费。初始化只做一次就够了。

4.2 中文乱码与图表不显示中文

中文问题分两种。第一种是控制台和CSV导出的乱码,主要是编码问题。写CSV文件时我建议用utf-8-sig编码,而不是默认的utf-8:

with open("data.csv", "w", encoding="utf-8-sig") as f: ...

用utf-8-sig会在文件头写入BOM标记,Excel打开后中文不会乱码;直接utf-8编码在Windows下的Excel里经常显示成乱码。第二种是matplotlib图表里的中文方框,解决办法就是前面提到的rcParams字体设置,核心是要确保当前系统里存在对应字体。

4.3 金额精度问题:用float还是Decimal

记账系统的金额计算对精度要求很高。直接用float存在二进制浮点的坑:0.1 + 0.2的结果并不是0.3,而是0.30000000000000004。月度汇总里涉及大量金额相加,浮点累加会积累误差,长期用下来账目对不上。

我的处理办法是:金额在录入时用Decimal做校验,从数据库读取出来后在Python侧统一转成Decimal再计算。SQLite本身把金额存成REAL类型,读取后转Decimal会有精度损失风险,所以更稳妥的做法是金额字段在数据库里存整数分,比如100.50元存成10050,展示时再除以100。这样彻底绕开浮点问题。如果你只是个人记账,数据量不大,用Decimal在应用层计算也能接受,但存整数分的方案更干净。

4.4 PyInstaller打包成exe的注意事项

很多读者拿到源码后想打包成exe发给朋友用。PyInstaller是常用工具,但有几个坑:

pip install pyinstaller pyinstaller -F -w main.py

-F参数把所有依赖打进单个exe,-w参数表示不显示控制台窗口。打包过程本身不难,难的是数据库文件的处理。如果你把finance.db当数据文件塞进exe,程序第一次运行会初始化它,但由于exe运行时的工作目录和数据库路径不一致,用户记账的数据会写到临时目录里,下次打开又生成一个新的临时目录,造成“数据丢失”的错觉。

我的建议是:打包时不捆绑数据库文件,让程序在exe所在目录的旁边创建finance.db。做法是在代码里根据sys.executable的目录动态计算DB_PATH,而不是写死相对路径。说实话,打包只是锦上添花,个人工具自己用Python跑就行,追求稳定优先。

最后分享一点个人体会

这套系统我自己实际用了三个多月,最大的感觉是“自动生成图表”让记账变成了一件有反馈的事。月初录几笔,月末打开折线图看一眼,这个月的消费趋势和类别占比一目了然,比自己在记账软件里翻汇总页直观得多。拿到源码的朋友,我建议不要急着改界面,先把数据库表结构读懂,再用transaction_dao.py里那几个增删改查函数试着手动插入一条假记录,然后跑一次月度汇总。这个过程走完,整个系统的数据流就通了,接下来加功能、改样式都是水到渠成的事。

后续如果想继续扩展,可以加个预算预警:budgets表里已经预留了monthly_limit字段,录入支出时顺便查一下当前分类本月累计消费是否超过预算,超过就在界面上给个提示。这个功能改动量不大,但会让系统从“单纯记账”升级成“主动帮你盯预算”,实用价值会高一个台阶。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 9:09:13

MySQL 5.7升级8.0:从准备到踩坑排查的完整指南

1. 升级之前先搞清楚&#xff1a;你的MySQL到底该不该升、能升到哪 干MySQL这块的同行应该都有感触&#xff1a;系统跑得好好的&#xff0c;最怕听见"升级"两个字。生产环境动数据库&#xff0c;搞不好就是通宵加背锅。但有些情况你躲不过——官方停止维护、安全漏洞…

作者头像 李华
网站建设 2026/10/2 9:08:33

基于Spring Boot的医院医疗仪器管理系统开发实战

设备科最怕的不是仪器突然坏了&#xff0c;而是坏的时候翻不到这台设备的购买日期、维保记录和上次检修报告。我最早接触这个需求时&#xff0c;对方还在用Excel管理全院几千台医疗设备&#xff0c;维修单靠纸质流转&#xff0c;保养提醒完全取决于设备科老师傅的记忆力。后来我…

作者头像 李华
网站建设 2026/10/2 9:07:26

小米手机反复重启?从启动模式到电池健康度的完整排查指南

我这台红米K40用了两年半&#xff0c;某天视频刷着刷着突然黑屏&#xff0c;原以为是系统抽风&#xff0c;就没在意。结果第二天&#xff0c;手机开始隔几分钟就重启一次&#xff0c;有时卡在Mi字标半天进不去&#xff0c;有时刚解锁进桌面又黑屏&#xff0c;重启之后页面全都要…

作者头像 李华
网站建设 2026/10/2 9:07:23

Python批量注册系统实战:绕过风控与验证码的工程化方案

1. 项目概述&#xff1a;这不是“点几下就注册成功”的玩具脚本&#xff0c;而是一套能扛住真实业务压力的批量注册系统“Python批量注册脚本开发详细”——这八个字背后藏着太多被轻描淡写的现实。很多人搜“python批量注册”&#xff0c;点开就是三五行requests.post()发个表…

作者头像 李华
网站建设 2026/10/2 9:06:33

单元测试六大陷阱与Vue实战:从稳定维护到LLM辅助生成新玩法

单元测试这件事&#xff0c;圈子里讨论了很多年&#xff0c;但真正能把它做好的团队并不多。很多项目一开始信誓旦旦“以后所有核心逻辑都要覆盖测试”&#xff0c;结果跑了几个月之后&#xff0c;测试套件变成了一堆改需求就爆、跑起来就红、没人敢动的历史包袱。我见过不少团…

作者头像 李华
网站建设 2026/10/2 9:05:15

ChromeDriver与Chrome版本对齐实战:win64环境Selenium自动化避坑指南

简介&#xff1a;本资源面向Web自动化测试开发者与Selenium学习者&#xff0c;提供Windows 64位系统下ChromeDriver与Chrome浏览器的配套组合&#xff0c;解决版本不匹配导致的驱动兼容问题。压缩包共84个文件&#xff0c;约150.08MB&#xff0c;包含chromedriver.exe驱动主程序…

作者头像 李华