简介:这套基于Python的财务记账系统源码,面向Python初学者、GUI开发爱好者以及需要日常账务管理的个人或小型工作室,提供一套完整可运行的桌面应用方案。系统使用Tkinter构建图形用户界面,实现支出、收入、借款、还款、记事与标签分类等常用功能;数据层面采用SQLite保存历史记录,并通过openpyxl支持将记录导出为Excel表格,便于统计分析和定期备份。资源包共包含17个文件,其中9个Python脚本按职责划分,覆盖主界面、数据库创建与初始化、工具函数、日志处理及Excel导出模块;另有6张界面截图,直观展示搜索、新增标签、修改删除记录以及备份与恢复等操作场景,配合1份目录结构说明和1个Markdown文档,帮助快速上手。压缩包整体约876KB,轻量易部署。目前已有334人学习,既适合用来练习Python桌面开发,也可以在此基础上扩展成个人或家庭财务工具。
1. 拿到一份 Python 财务记账系统源码,先别急着跑起来
财务记账系统大概是 Python 实战项目里最「看着简单、做着坑多」的一类。你搜到这份基于Python的财务记账系统.zip,解压后大概率面对的是一个包含models.py、main.py、ui.py之类文件的工程,甚至可能连requirements.txt都没有。很多从 Java 课程设计转过来的同学会习惯性地想:这不就是个增删改查吗?结果一跑,要么No module named 'tkinter',要么 SQLite 文件路径写死导致权限报错,要么中文字体在界面上全部变成方块。
这份笔记的价值不在于替你把代码逐行朗读一遍——我没见过作者原稿,也没法假装自己跑过这份源码。我会按一线工程师拿到陌生 Python 项目的通用路径:先拆目录、再定数据流、然后本地跑通、最后改造成自己能维护的样子。关键词是 Python、源码、财务记账系统,但真正决定你能不能把这个项目用起来的是 SQLite 的事务处理、日期字符串的排序坑、以及 UI 层和业务层到底该不该耦合。
适合谁看?刚做完 Python 基础语法想找一个完整项目练手的学生,以及想给家里或小团队做个本地记账工具、但不想用现成 App 的从业者。这篇文不会教你写一个生产级 ERP,它只解决一个问题:让你拿到这份源码后,能在半天内跑起来、看懂它在干什么,并且敢动手改。先把话放这儿:这类系统的核心难点从来不是增删改查,而是账目的不可丢失性和统计口径的一致性,全文都会围绕这两点展开。
2. 拆解源码结构:记账系统里最容易被忽略的三层职责
2.1 为什么说「单文件记账脚本」是埋雷的写法
你随便在搜索引擎里找「Python 财务记账系统源码」,会看到大量单文件解决方案:一个main.py塞下所有函数,用列表套字典当数据库,程序一关数据就蒸发。这种写法作为语法练习题没问题,但凡是叫「系统」的东西,至少要扛得住一次异常退出。这份标题所说的源码,如果作者是认真做的,目录里应该能看到模型层、业务层、展示层的大致区分;如果只有孤零零一个.py,那你更需要知道怎么把它拆开。
我拿到这类源码的第一步永远是先看数据持久化方案。常见的梯队是:JSON 文件存数据(入门级)、SQLite 单文件数据库(实用级)、MySQL/PostgreSQL(课程设计凑分级别)。基于Python的财务记账系统这个命名风格,八成落在 SQLite 上,因为它既不需要安装额外服务,又能支撑事务回滚。判断标准很简单:源码里有没有import sqlite3或者from sqlalchemy import ...。这决定了你后续所有操作——JSON 方案你只需要关心读写时机,SQLite 方案你必须理解连接生命周期。
接下来是 UI 方案。Tkinter 是 Python 自带 GUI 库,零依赖,但丑且控件行为诡异;PyQt5/PySide6 好看但打包体积直接奔 60MB 起步;还有一部分源码会用streamlit做成网页版。这批源码如果叫「财务记账系统」而不是「记账本」,作者很可能选了 Tkinter 或 PyQt5,因为课程设计和毕设偏爱桌面应用。这个选择直接影响你测试时的交互方式——Tkinter 程序没法和 pytest 好好玩耍,你只能模拟用户点击。
最后看业务层。老练的记账系统会把收入、支出、分类、账单四张表拆开,用外键关联;粗糙的写法是把类型塞进一个category字段,用字符串匹配。这两种写法在功能上没区别,但后续你要加「按月统计」「预算预警」时,后者会让你改到怀疑人生。我会在下一节用一个最小目录结构做参照,让你能快速对照自己手里那份源码属于哪种水平。
2.2 典型源码目录和核心文件职责
假设你解压后看到类似这样的结构:
finance_book/ ├── main.py # 程序入口,负责启动 UI 或命令行交互 ├── database.py # SQLite 连接、建表、事务封装 ├── models.py # 记账数据的数据类(Record, Category) ├── services.py # 业务逻辑:增删改查、统计、报表 ├── views/ │ ├── __init__.py │ ├── app_window.py # 主窗口 │ └── dialogs.py # 新增/编辑账单对话框 ├── data/ │ └── finance.db # SQLite 数据库文件(首次运行自动生成) ├── requirements.txt # 第三方依赖,可能为空 └── README.md # 可能缺失或写得很敷衍main.py是启动入口,常见套路是:
def main(): app = FinanceApp() app.mainloop() if __name__ == "__main__": main()database.py是这个项目的命根子,里面应该有一个get_connection()函数和建表语句。抄作业时你重点关注三件事:连接是否用了with上下文管理器、写操作是否开了事务、表结构里日期字段是TEXT还是DATE类型。前两点决定数据会不会丢,第三点决定你之后做月度汇总时要不要和字符串格式化搏斗。
services.py负责把你的操作翻译成 SQL。这里有个隐藏考点:查询语句用的是参数化查询还是字符串拼接。下面这种写法在任何源码评审里都会被骂:
# 高危写法:SQL 注入 + 引号转义地狱 cursor.execute(f"SELECT * FROM records WHERE category='{category}'")正确写法是:
cursor.execute( "SELECT * FROM records WHERE category = ?", (category,) )?占位符让 sqlite3 模块替你处理转义,既防注入又避免字段值里带单引号时程序崩掉。记账系统虽然是自己用,但养成参数化习惯成本极低。
2.3 理解数据流:一笔账从点击到落盘经历什么
现在把视角拉高,看一次「新增收入」操作背后发生了什么。用户在主界面点「记一笔」→ UI 层弹出对话框收集金额、分类、备注 → 点确定后 UI 调用services.add_record(amount, category, note, timestamp)→ 这个函数先做参数校验(金额是否为正数、分类是否合法)→ 然后调用database.insert_record(...)→ 数据库模块BEGIN事务、执行 INSERT、COMMIT→ 最后 UI 层刷新表格并提示成功。
这条链路里最容易出问题的是第 2 步到第 3 步的交接。很多入门源码会在 UI 层直接写 SQL,表面看省了一个文件,实际上把用户点击和数据操作焊死在一起。将来你想加一个「自动导入微信账单」的功能,会发现根本没法绕过界面层去调数据。所以我拿到源码后第一件事就是画这条数据流,如果标注出来全是ui -> sqlite的直箭头,说明作者把业务层省了。这不是不能用,而是扩展性差。你现在的选择只有两个:忍着用,或者自己动手把业务逻辑抽出来——动手方案我放在第 4 章。
数据流另一处关键点是界面刷新时机。正常的记账系统在每次增删改之后要重新查询列表;笨一点的实现会直接把整个表格清空重建,导致用户刚选中某一行,操作完焦点就丢了。更隐蔽的坑是删除记录后统计面板没刷新——账删了,总额还显示旧值,用户会以为自己算错了。这些点在你验收源码时用三次操作就能试出来:记一笔 → 改分类 → 删掉它,看统计数字是否跟着变。
3. 本地跑通的最小路径:环境、依赖、首次启动
3.1 环境准备:Python 版本和虚拟环境是两件不同的事
先解决最基础的「跑不起来」。我刚入行时也干过「直接把 zip 解压到 C 盘根目录然后双击 main.py」的事,通常以一堆红色报错收场。正确姿势分四步。第一步,确认 Python 版本。这份源码如果用了 f-string(f"{amount:.2f}")或类型注解,最低要求是 3.6+;如果用了match语句,就得 3.10+。打开命令行执行:
python --version我建议直接在项目目录建虚拟环境,不污染全局 Python。Windows 用户注意,如果你电脑上装了多个 Python,用py -3.10指定版本:
cd path/to/finance_book python -m venv venvWindows 激活命令是venv\Scripts\activate,macOS/Linux 是source venv/bin/activate。激活后命令行前缀会多一个(venv),这就是当前环境生效的标志。这一步的核心价值在于:你后续pip install装的东西只属于这个项目,不会把系统 Python 搞得乱七八糟。我见过太多人因为全局环境里某个包版本冲突,把能跑的代码改得面目全非。
第二步,安装依赖。有requirements.txt直接:
pip install -r requirements.txt没有也别慌,先看看源码里 import 了哪些第三方库。Tkinter 是标准库,不需要装;PyQt5 用pip install PyQt5;如果涉及图表(报表页面的饼图),八成要pip install matplotlib。装完之后跑一句验证:
python -c "import main"不报错说明依赖齐了,报ModuleNotFoundError: xxx就补装哪个。
3.2 首次启动:三步走完「不崩溃」到「数据可见」
依赖装完,别直接双击。先在终端里跑,因为 Tkinter 程序的报错只有终端能看到,双击时窗口一闪而过你啥也抓不到。启动命令:
python main.py如果顺利,一个窗口弹出来,大概率带一个表格和几个按钮。此时你只完成了「能启动」,还没验证「能记账」。我一般按三步验收。
第一步,记一笔「测试收入」。金额填 100,分类选「工资」,点保存。表格里应该出现一行,底部统计(如果有的话)显示收入 100。第二步,记一笔支出。金额填 20.5,分类选「餐饮」,保存后看剩余总额是不是 79.5。第三步,关掉程序重新打开。如果刚才记的两笔还在,说明持久化生效了;如果数据消失,说明这是个纯内存记账本,只能当 Demo 看。
这里有个细节你要注意:记账时金额输入框用的是文本控件,意味着用户可能输入abc或12.3.4。靠谱的源码会在保存前做校验,不靠谱的就把字符串直接塞给float(),然后程序炸个ValueError给你看。所以测试时故意输一个非法值,看它是弹出友好提示还是直接崩溃——这个行为直接决定你愿不愿意把它给家里人用。
3.3 数据库文件位置:隐藏的权限坑
SQLite 的坑大多集中在「文件放哪」。源码里如果写死相对路径data/finance.db,那么你在项目根目录启动没问题,但如果在别的目录执行python /path/to/main.py,程序可能会在错误的地方创建数据库,或者干脆因为目录不存在报OperationalError: unable to open database file。
检查database.py里连接字符串是怎么拼的。稳健写法是:
BASE_DIR = Path(__file__).resolve().parent DB_PATH = BASE_DIR / "data" / "finance.db"这样无论你从哪个目录启动,数据库都固定在项目目录的data文件夹下。如果是写死的相对路径,建议你自己改成这个写法,改动量三行,收益是以后再也不用担心「我的数据去哪了」问题。
依赖问题也顺手说说:这项目如果用了 PyQt5,在部分 Linux 发行版上还需要装libxcb-cursor0之类的系统库,否则启动报could not load the Qt platform plugin。Windows 和 macOS 一般没这个事,但 Ubuntu 用户看到这个报错别慌,sudo apt install libxcb-cursor0就能解决。
4. 数据库设计与数据安全:让这个系统值得托付真实账目
4.1 表结构核心逻辑:流水表与分类表的关联设计
你如果打开的是一份正经源码,数据库里应该至少有两张表:categories和records。categories存分类名称和类型(收入/支出),records存每一笔账的金额、分类 ID、备注、时间。
-- 分类表 CREATE TABLE categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, kind TEXT NOT NULL CHECK(kind IN ('income', 'expense')) ); -- 流水表 CREATE TABLE records ( id INTEGER PRIMARY KEY AUTOINCREMENT, category_id INTEGER NOT NULL, amount REAL NOT NULL CHECK(amount > 0), note TEXT, created_at TEXT NOT NULL, FOREIGN KEY (category_id) REFERENCES categories(id) );amount REAL是这里最值得讨论的设计决策。Python 的float在表示 0.1 时有精度误差(二进制浮点的老问题),反映到数据库里就是:你记一笔 0.1 元的收入,查出来可能是0.100000000000000005。记账系统里金额动辄成百上千,误差会累积。教科书的答案是DECIMAL(10,2),SQLite 没有这个类型,但可以存整数(单位分):
amount_cents INTEGER NOT NULL CHECK(amount_cents > 0)展示时再除以 100。这份源码如果用了REAL,不是不能用,只是你心里要有数:做月累计时用SUM(amount)可能出现123.45000000000002这种尾巴,格式化"{:.2f}".format()就能掩盖。我的建议是,如果源码用了 REAL,你在自己的改动里至少统一用round(value, 2)处理所有从数据库读出来的金额,避免统计加总时出现诡异小数。
created_at用 TEXT 而非时间戳,这是为了让人类能直接读懂备份数据。格式建议统一YYYY-MM-DD HH:MM:SS,原因在下一节讲排序坑时说明。
4.2 一个必须复制的基操:事务回滚
账目数据最怕「写一半崩了」的中间态。比如你删除一条流水:先清了records里的行,再更新categories的计数(假设有冗余计数这回事),两步之间断电了,数据库处于不一致状态。SQLite 的解决方案是事务,代码长这样:
def delete_record(conn, record_id): try: with conn: # 关键:with 块自动提交/回滚 conn.execute("DELETE FROM records WHERE id = ?", (record_id,)) # 如果还有其他关联操作,放在这里 except sqlite3.Error as e: print(f"删除失败,已回滚: {e}")with conn的语义是:块内所有操作成功才 COMMIT,有异常就 ROLLBACK。这是 Python sqlite3 模块最优雅的事务写法,比你手动调BEGIN/COMMIT少踩一大半坑。注意:只有conn = sqlite3.connect(...)默认开着isolation_level时with conn才有效;如果你哪行代码写了isolation_level=None(自动提交模式),with conn就不会开事务了,删除操作变成逐条自动提交,中间失败只能哭。
原本没这层保护的话,建议你动手加上,改动量只有把conn.execute(...)换成with conn:嵌套。这是一个能直接让系统可靠性上一个台阶的最小改动。
4.3 数据库备份:没有后悔药的系统不叫记账
写到这里,我必须强调一个用户几乎不会在源码里见到的功能:导出备份。记账软件有个天然矛盾——你越用它,里面的数据越值钱,一旦文件损坏,损失越大。SQLite 单文件特性天然适合备份:关掉程序后,把finance.db复制一份改名存起来就是完整备份。
如果你愿意加个小功能,在services.py里写一个导出函数:
import shutil from datetime import datetime def backup_database(db_path, backup_dir="backups"): timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") target = Path(backup_dir) / f"finance_{timestamp}.db" target.parent.mkdir(exist_ok=True) shutil.copy2(db_path, target) return target做成定时任务也行、每次退出时调一下也行。这个功能成本极低,但它是「记账系统」和「玩具」之间的一条分界线。当你某天误删了一批记录,或者杀毒软件把 db 文件隔离了,你会回来感谢这一节。
5. Python 财务记账系统常见的 5 个翻车现场与排查流程
5.1 现象:界面文字全是方块/问号,尤其 Windows 上
原因:Tkinter 默认字体TkDefaultFont对中文支持不稳定,部分 Windows 环境会显示成□或乱码。PyQt5 一般没这个问题,因为它用系统字体渲染。
解决:如果是 Tkinter,在创建窗口后显式设置字体:
import tkinter as tk from tkinter import font root = tk.Tk() default_font = font.nametofont("TkDefaultFont") default_font.configure(family="Microsoft YaHei", size=10) root.option_add("*Font", default_font)注意Microsoft YaHei是 Windows 字体,Linux 上要换成WenQuanYi Micro Hei,macOS 用PingFang SC。这个改动必须在任何 UI 控件创建之前执行。
5.2 现象:sqlite3.OperationalError: table records already exists
原因:database.py里每次启动都执行CREATE TABLE,没有加IF NOT EXISTS。第一次运行建表成功,第二次程序启动时又执行同一条语句,SQLite 直接报错。
解决:把所有建表语句改成CREATE TABLE IF NOT EXISTS。这既是常规做法,也是排查此类报错的唯一标准修复。顺带检查是否在main.py里对init_db()做了幂等处理——多次调用不该产生副作用。
5.3 现象:按月份筛选统计,结果排序错乱
原因:created_at存储格式不统一。比如有的记录是2024-01-05,有的是2024/1/5,还有带时间的2024-01-05 10:30。SQLite 的ORDER BY对 TEXT 按字典序排,2024/1/5会排到2024-01-05后面,月份截取substr(created_at, 1, 7)也拿不出统一样式。
解决:写一个迁移脚本,把所有历史日期统一成YYYY-MM-DD HH:MM:SS格式。这一步如果源码里没有,自己写个循环处理现有数据即可。代码思路:
from datetime import datetime def normalize_date(raw: str) -> str: # 容忍 2024/1/5 12:00:00 等多种格式 for fmt in ("%Y/%m/%d %H:%M:%S", "%Y/%m/%d", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d"): try: return datetime.strptime(raw, fmt).strftime("%Y-%m-%d %H:%M:%S") except ValueError: continue raise ValueError(f"无法解析日期: {raw}")跑一遍后,所有筛选和排序行为都会恢复正常。这里面有个隐藏状况:源码里datetime.now()在不同 Python 版本返回的str格式一致,但如果你从微信记账导出 CSV 再导入,格式就可能五花八门。这种问题上过当之后,我在所有涉及日期存储的代码里只认一种格式,多余的靠解析函数兜底。
5.4 现象:关闭程序后重新打开,之前记的账全没了
原因:数据从没写进磁盘。要么是数据库文件被程序放在临时目录(比如每次启动tempfile新路径),要么是 JSON 方案只在退出时写文件且中间崩了没写成功。还有一种是:程序能跑但每次都在内存建库,data/finance.db从未生成过。
解决:先查项目目录里有没有finance.db。没有的话,打印get_connection()里的完整路径看看指向哪里。如果是临时目录,改成固定路径(参照 3.3 的Path(__file__).resolve().parent方案)。如果能看到 db 文件但数据还是丢,检查是不是INSERT语句在内存连接上执行、但展示时读的是另一个文件连接——这个问题常出现在main.py里多个sqlite3.connect()调用各管各的,连接没有复用同一个 DB 路径。
5.5 现象:点击「添加」按钮后程序卡死,CPU 飙高
原因:UI 线程里执行了耗时操作,比如把整个数据库读入内存排序、或者在主线程里做耗时的文件扫描。Tkinter 是单线程模型,主线程忙的时候就无法响应鼠标事件,表现就是窗口无响应。
解决:小型记账系统数据量不大,遇到这种情况先检查是不是循环里写了while True而没有退出条件,其次看是不是查询没有加LIMIT把几十万行全部拉回来了。如果是跨数据源导入大量流水,正确做法是把导入逻辑放到后台线程,用queue传结果给 UI 刷新。但这不是你现在最紧迫的事——先把明显傻的操作找出来。
这些坑我当年一个个都踩过,尤其是日期格式那道,改完第二天拿上个月数据一跑,统计终于对上了。如果你是刚接触源码的初学者,5.2 和 5.4 是最可能劝退你的拦路虎,定位思路完全一样:先看报错信息,再追数据库文件状态。
6. 让系统变成你自己的:一个月的账目验证与报表扩展
当你能稳定记账、备份、恢复之后,这个系统才刚进入可用阶段。我最后分享一个具体技巧和一个验证习惯。
技巧是给系统加「月度收支汇总」视图。看源码里有没有现成的GROUP BY substr(created_at, 1, 7)查询,如果没有,自己写一个:
def monthly_summary(conn, year=None): where = "" params = () if year: where = "WHERE substr(created_at, 1, 4) = ?" params = (str(year),) sql = f""" SELECT substr(created_at, 1, 7) AS month, SUM(CASE WHEN c.kind = 'income' THEN r.amount ELSE 0 END) AS income, SUM(CASE WHEN c.kind = 'expense' THEN r.amount ELSE 0 END) AS expense, SUM(CASE WHEN c.kind = 'income' THEN r.amount ELSE -r.amount END) AS net FROM records r JOIN categories c ON r.category_id = c.id {where} GROUP BY month ORDER BY month DESC """ return conn.execute(sql, params).fetchall()这段代码解决了「这月到底花了多少」的核心问题。注意JOIN categories不是装饰——没有分类表关联,你根本分不清一笔钱是流入还是流出。CASE WHEN把收入支出分列展示,比混在一列里直观得多。
验证习惯是跑一次「对账」:拿这个系统记账一个月,月底把每个分类的合计和实际支付记录(微信账单、支付宝账单)做比对。误差超过 5 块钱就查原因——多数是金额精度或漏记。这个习惯能帮你发现代码逻辑里的小数位问题,也会让你对「哪笔账记没记」产生肌肉记忆。我自己的经验是连续对账三个月后,代码里所有隐藏的取整误差基本都被逼出来了。
最后说个离别建议:把这份源码里你改过的所有地方,用# My change:注释标出来。一个月后你要升级功能,打开文件对着注释就能快速回忆起当初的意图,免得在代码海洋里捞自己丢的针。如果你打算走 Python 开发这条路,把记账系统认真维护一年以上,你会在它身上学到比任何网课都多的实战经验——数据一致性、异常处理、UI 响应性、备份策略,这些东西是课本不教但真实项目每天都在考的事。希望帮到你。
本文还有配套的精品资源,点击获取