Python简易记账软件设计:从SQLite数据模型到统计可视化全解析

发布时间:2026/10/25 11:25:58
Python简易记账软件设计:从SQLite数据模型到统计可视化全解析 简介这份资源是一套基于Python开发的简易记账软件设计源码面向Python学习者、前端开发人员以及需要日常收支管理的个人和小型企业。软件采用Python处理后端业务逻辑结合HTML/CSS构建界面并由JavaScript增强交互实现账目记录、查询与管理等功能。压缩包共37个文件大小约848KB其中包含20个Python脚本作为核心业务模块5个JavaScript脚本和4个CSS、4个HTML文件共同完成前端展示与动态交互另含辅助配置与说明文件整体结构清晰。源码按controller、dao、service等分层组织体现了控制层、数据访问层与业务逻辑层分离的设计思想便于阅读和二次开发。目前已有718人学习下载适合希望参考完整项目管理方式、了解Python结合前端技术实现实际应用的开发者。1. 一张 Python 记账源码到底能解决什么先看清它的边界微信、支付宝、银行卡三个 App 的账单各自为政月底想对一笔账要来回切换这时候“基于 Python 的简易记账软件设计源码”就派上用场了。它的核心并不复杂把每一笔收支变成一行结构化数据再配上增删改查、汇总和图表。标题里“简易”两个字很重要——它意味着不依赖外部服务、不部署数据库服务器、不改业务逻辑就能跑起来数据全部留在本机一个文件里。适合刚入门 Python、想拿完整源码练手的学生也适合习惯本地记账、不想把账目交给第三方 App 的用户。接下来这篇笔记会从数据模型、主流程、统计分析一路说到实测踩坑照着复现一遍你会拿到一个真正能记账、能查询、能出图的小系统。2. 先定数据模型简易记账为什么优先选 SQLite而不是 CSV 文本2.1 三种存储方案的取舍CSV 方便打开SQLite 更适合当正经账本很多初学源码喜欢把账目存在二维列表或字典里程序一退出数据就没了稍微进阶一点的写法是存 CSV用 Excel 能直接打开看起来非常“透明”。但记账软件一旦运行超过一个月CSV 的短板就暴露出来了按日期区间查询需要遍历全部行做月度汇总要把所有数据读进内存再分组修改一条记录要先整文件读入、替换、再整文件写回。数据量到几千行时这个开销还不明显到几万行就会感觉到每次保存都变慢。更麻烦的是多进程或多次运行时共享写同一个 CSV 会有数据覆盖风险。我一般会把存储方案压缩成一张表来选型。存储方式适合场景主要缺点纯文本/二维列表学习演示、几十条数据无索引、无并发、无法按条件高效查询JSON配置类数据、结构嵌套较多只能整体加载不适合聚合统计CSV需要频繁用 Excel 手工查看没有事务、并发写有风险、修改麻烦SQLite本地账本、需要查询统计需要会一点 SQL但对记账足够了SQLite 本质上是一个单文件的嵌入式关系型数据库Python 标准库自带 sqlite3 模块不需要安装额外服务。记账软件这个场景最大的需求就是“按日期过滤、按分类汇总”这恰好是 SQL 的强项一条 SELECT 加 GROUP BY 就能完成不需要在 Python 里手写分组逻辑。对于单人使用的记账工具SQLite 在可靠性、可移植性和查询能力之间是最平衡的选择。2.2 核心表结构与建表语句把账目拆成时间、分类、金额、备注四件事设计数据模型时不要一上来就建立五张表做外键关联。简易记账的核心是流水一张 transactions 表足够承载主要业务再配一张 categories 表管分类两张表足矣。transactions 表我习惯存放这些字段id 主键、trans_date 交易日期、category 分类名、amount 金额、note 备注、is_deleted 软删除标记、created_at 创建时间。其中 is_deleted 不是神经过敏而是为了给后面“误删后悔药”留后路这一坑在第 5 章会专门展开。CREATE TABLE IF NOT EXISTS transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, trans_date TEXT NOT NULL, category TEXT NOT NULL, amount REAL NOT NULL, note TEXT DEFAULT , is_deleted INTEGER NOT NULL DEFAULT 0, created_at TEXT DEFAULT (datetime(now, localtime)) ); CREATE TABLE IF NOT EXISTS categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, kind TEXT NOT NULL CHECK (kind IN (expense, income)) ); INSERT OR IGNORE INTO categories(name, kind) VALUES (餐饮, expense), (交通, expense), (购物, expense), (工资, income), (其他, expense);这段 SQL 里有两个细节需要注意trans_date 用 TEXT 存而不是 DATETIME是因为记账软件的日期比较永远走 ISO 格式字符串比如 2025-01-15字符串比较早、中、晚天然有序不涉及时区问题amount 用 REAL 存储会带来浮点尾差这是第 5 章的重点真正生产级账本更应该用“分”为单位的整数存储源码阶段先保留 REAL 便于理解后续迁移可以加一个转换函数。初始化数据库的 Python 代码可以这样写import sqlite3 DB_PATH ledger.db def get_conn(): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row return conn def init_db(): conn get_conn() conn.executescript( CREATE TABLE IF NOT EXISTS transactions (...); CREATE TABLE IF NOT EXISTS categories (...); INSERT OR IGNORE INTO categories(name, kind) VALUES ...; ) conn.commit() conn.close()逻辑说明get_conn 里设置了 row_factory 为 sqlite3.Row让查询结果可以按字段名访问而不是只能按下标取可读性大幅提升。init_db 用 executescript 一次性执行多条 SQL 建表和种子数据插入。INSERT OR IGNORE 依赖 categories 表 name 字段的唯一索引保证每次启动程序重复执行初始化脚本都不会产生重复分类。数据库文件 ledger.db 会生成在当前工作目录下建议在项目根目录运行脚本避免路径错乱找不到账本文件。3. 把记账主流程跑通增删改查与每日汇总的最小实现3.1 录入和查询先把一笔支出安静地写进库里记账软件最核心的两个操作是写和读。录入时要注意“先校验、后落库”尤其是日期和金额的合法性。日期格式统一为 YYYY-MM-DD金额要能转成浮点数且大于 0。校验放在录入函数入口处的好处是后面不管是命令行调用、GUI 调用还是未来写 Web 接口所有入口共享同一套校验逻辑不会因为入口变多而出现脏数据。def add_transaction(trans_date, category, amount, note): conn get_conn() try: conn.execute( INSERT INTO transactions(trans_date, category, amount, note) VALUES(?, ?, ?, ?), (trans_date, category, amount, note) ) conn.commit() except sqlite3.Error as e: conn.rollback() print(f入库失败: {e}) finally: conn.close()参数说明trans_date 必须是字符串格式 YYYY-MM-DDcategory 建议先用 categories 表里的名字如果传入不存在的分类当前设计并不会报错这也是简易版本刻意降低耦合的做法一旦实际使用中发现分类混乱再改成外键约束也不迟amount 用浮点数存在尾差问题见第 5 章。execute 使用了 ? 占位符传参而不是用字符串拼接这是为了防止 SQL 注入也是一名 Python 开发者的基本习惯任何进数据库的字符串都不得直接拼进 SQL。查询函数则需要支持参数控制条数List 场景下通常只取最近 20 笔而不是一次加载全部历史记录。def list_transactions(limit20): conn get_conn() rows conn.execute( SELECT id, trans_date, category, amount, note FROM transactions WHERE is_deleted 0 ORDER BY trans_date DESC, id DESC LIMIT ?, (limit,) ).fetchall() conn.close() return rows逻辑说明排序先按日期倒序同一天的记录再按 id 倒序这样新录入的账目会排在最前面。LIMIT 参数由调用方传入避免数据量变大后一次性拉回全表。is_deleted 0 是软删除过滤条件配合后面 update 和 delete 使用。查询返回的是 sqlite3.Row 对象列表在打印或模板渲染时可以通过 row[amount] 这样的方式取字段。3.2 修改与删除先查再改避免批量误伤改账和删账是最容易出事故的地方萌新经常犯的错误是“先改再说”结果 WHERE 条件写错整列被更新。我的习惯是修改操作强制分两步第一步根据 id 查出原记录确认存在第二步执行 UPDATE。两步操作不用放在一个事务里但代码结构上必须清晰。def update_transaction(txn_id, trans_dateNone, categoryNone, amountNone, noteNone): conn get_conn() row conn.execute( SELECT id FROM transactions WHERE id ? AND is_deleted 0, (txn_id,) ).fetchone() if not row: conn.close() print(f记录 {txn_id} 不存在或已删除) return fields [] values [] if trans_date is not None: fields.append(trans_date ?) values.append(trans_date) if category is not None: fields.append(category ?) values.append(category) if amount is not None: fields.append(amount ?) values.append(amount) if note is not None: fields.append(note ?) values.append(note) if not fields: conn.close() return values.append(txn_id) conn.execute( fUPDATE transactions SET {, .join(fields)} WHERE id ?, values ) conn.commit() conn.close()逻辑说明这里用动态拼接字段的方式就是为了让调用方可以只修改一个字段而不影响其他字段。字段名集合是代码写死的不会因用户数据改变所以不存在 SQL 注入风险。f-string 拼接的是白名单字段名VALUES 仍然走占位符。删除操作不建议物理删除用 is_deleted 标记即可。def soft_delete(txn_id): conn get_conn() conn.execute( UPDATE transactions SET is_deleted 1 WHERE id ?, (txn_id,) ) conn.commit() conn.close()逻辑说明删除实际是 UPDATE 语句改一个标记位。好处是误删后还有恢复的可能也保住了所有历史流水不被物理抹掉。代价是每次查询都要记得加 is_deleted 0 过滤代码里所有 SELECT 都要带上这个条件。想真正“扔掉”数据时可以定期执行 PURGE 语句把 is_deleted 1 的记录删掉。3.3 每日汇总一条 GROUP BY 算出今天花了多少每日汇总放在记账软件里是最高频的操作很多人记完账就想看“今天合计花了多少”。按分类聚合可以很清楚看到钱流进了哪些地方。def daily_summary(day): conn get_conn() rows conn.execute( SELECT category, SUM(amount) AS total, COUNT(*) AS cnt FROM transactions WHERE trans_date ? AND is_deleted 0 GROUP BY category ORDER BY total DESC, (day,) ).fetchall() conn.close() return rows逻辑说明WHERE 先按日期缩窄范围然后 GROUP BY category 做分类汇总SUM(amount) 得到每个分类的总金额COUNT(*) 得到笔数。排序按金额从大到小一眼看到今天最花钱的地方。在 Python 里调用时day 参数格式必须严格是 YYYY-MM-DD否则 SQL 字符串比对根本匹配不上。这一行 GROUP BY 就是 SQLite 相对 CSV 最大的优势换成 CSV 方案要写 20 行以上的 Python 循环才能做到同样的效果。4. 让账目会说话按月统计、分类占比与两张图的画法4.1 用 pandas 做数据层聚合直接从 SQLite 读成 DataFrameSQL 能做的聚合语法比较单调想按月份画趋势、按分类算占比直接在 Python 里处理会更顺手。常见做法是把 SQLite 查出来的结果交给 pandaspandas 的 groupby 和 resample 在时间维度上非常顺手。import pandas as pd def load_tx(): conn get_conn() df pd.read_sql_query( SELECT trans_date, category, amount, note FROM transactions WHERE is_deleted 0, conn ) conn.close() df[trans_date] pd.to_datetime(df[trans_date]) return df def monthly_report(df): df[month] df[trans_date].dt.to_period(M) expense df.groupby(month)[amount].sum() return expense逻辑说明load_tx 一次性读出全部未删除流水转成 DataFrame 后把 trans_date 从文本转成 pandas 时间类型。monthly_report 里用 dt.to_period(M) 把每个日期截断到月份生成 month 列然后按月份求和得到类似 2025-01 3580.50 这样的月度支出序列。pandas 的 read_sql_query 是通用做法但要注意它要求 sqlite3.Connection 对象不能传 DB_PATH 字符串。如果你还没有装 pandas用 python 安装 pandas 的方式执行 pip install pandas 即可这一步几乎不需要费心唯一的坑是 matplotlib 的中文字体下面马上讲。4.2 画月度趋势和分类占比中文字体是第一个坑matplotlib 默认字体不支持中文画出来的图往往是方块。解决方式是在绘图前设置字体参数同时关掉 Unicode 负号否则纵轴的负号也显示异常。Windows 系统通常可用 SimHeiLinux 上用 WenQuanYi Micro Hei 或 Noto Sans CJK SC。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, WenQuanYi Micro Hei, Arial Unicode MS] plt.rcParams[axes.unicode_minus] False def draw_monthly_trend(df): monthly monthly_report(df) monthly.plot(kindline, markero, title月度支出趋势) plt.ylabel(金额) plt.xlabel(月份) plt.tight_layout() plt.savefig(monthly_trend.png, dpi150) plt.show()逻辑说明monthly_report 返回的是 pandas SeriesSeries 自带 plot 方法kindline 指定折线图markero 让每个月份数据点显示为圆点方便看具体位置。savefig 的 dpi 参数控制清晰度150 足够在屏幕上展示。分类占比更适合用饼图需要先过滤出支出类数据再画。def draw_category_pie(df, monthNone): data df.copy() if month: data data[data[trans_date].dt.to_period(M) month] category_sum data.groupby(category)[amount].sum() category_sum.plot(kindpie, autopct%.1f%%, figsize(8, 8)) plt.title(分类支出占比) plt.ylabel() plt.tight_layout() plt.savefig(category_pie.png, dpi150) plt.show()参数说明autopct 控制饼图上百分比的小数位数%.1f%% 表示保留一位小数并带上百分号figsize(8, 8) 让饼图保持正圆否则容易变形。month 参数如果传入 pandas「Period」对象而不是字符串需要调用方保持一致最简单的用法是传入类似 pd.Period(2025-01, M)。这两个绘图函数组合起来就完成了“月度总额怎么看”和“钱花在哪类”两个核心分析诉求。5. 避坑记账源码接入真实数据时的 4 个常见坑5.1 日期格式不统一月度统计串了行现象账本里既有 2025-01-15又有 2025/01/15还有 20250115月度汇总结果出现同一个月份被拆成多行甚至 2025-1-15 排在 2025-01-02 之前。原因录入时没有统一日期格式字符串排序在“1”和“01”上产生不同结果SQLite 不会替你纠正格式不规范的数据。解决在写入前强制做一次解析再写回。from datetime import datetime def normalize_date(value): return datetime.strptime(value, %Y-%m-%d).date().isoformat()参数说明strptime 要求输入严格匹配 %Y-%m-%d比如 2025-01-15。如果报错说明调用方传入了错误格式应当让异常抛出来而不是静默处理。我见过一些源码用 replace(/, -) 来“补救”结果 2025/1/5 变成 2025-1-5依然不是规范格式。正确做法是强制所有入口统一按 YYYY-MM-DD 字符串传入在 GUI 或命令行入口层就限制用户输入格式。5.2 金额出现 0.00000001 尾差对账对不上现象显示上明明是 0.19 0.01 0.20程序里 SUM 的结果却是 0.19999999999999998打印出来带着一长串尾数。原因REAL 是 IEEE 754 双精度浮点数无法精确表示十进制的 0.1 和 0.19 这类分数。SQLite 的 SUM 基于浮点计算尾差会在聚合过程中积累。解决最稳的方案是账本内部金额全部用整数“分”存储。建表时定义 amount_cents INTEGER录入时把元换算成分为单位展示和统计时再除以 100 转回元。def yuan_to_cents(amount): return int(round(amount * 100)) def cents_to_yuan(cents): return cents / 100逻辑说明round 先对浮点数做四舍五入再转 int避免出现 19.999999 转成 19 的情况。统计时对 amount_cents 做 SUM得到整数再统一转回元尾差问题彻底消失。如果旧数据已经用 amount REAL 存了迁移脚本可以用 SQL 内联函数近似修正UPDATE transactions SET amount_cents CAST(ROUND(amount * 100) AS INTEGER)但历史脏数据仍建议人工抽查几笔对账。5.3 导出 CSV 用 Excel 打开中文全乱现象用 pandas 或 csv 模块导出的 .csv 文件用记事本打开正常Excel 直接打开却是乱码。原因Excel 默认以 GBK 编码解析 CSV而 Python 写入文件默认或代码中指定的 utf-8 编码两边编码不一致。解决导出 CSV 时用 utf-8-sig 编码它会在文件开头写入 BOM字节序标记Excel 识别 BOM 后自动按 UTF-8 解析。df.to_csv(export.csv, indexFalse, encodingutf-8-sig)参数说明indexFalse 防止把 DataFrame 索引写成第一列无意义的数字encodingutf-8-sig 是唯一关键点。顺带一提如果这个 CSV 是给其他 Linux 服务用utf-8-sig 不会影响解析BOM 在 Python 打开时会自动忽略所以这个参数几乎是零成本解决兼容问题。5.4 删除账目没有后悔药误删直接消失现象想删一笔重复账结果 WHERE 条件写少了整天的记录全部被删掉。SQLite 默认关闭外键和事务回滚一旦 COMMIT数据就永久没了。原因初版源码直接执行 DELETE FROM没有软删除标记也没有备份机制。解决为 transactions 表增加 is_deleted 字段把所有删除改成 UPDATE 置 1。同时在每次程序退出或每天第一次记账前自动备份一份账本文件到 backup 子目录。import shutil import os from datetime import date def backup_db(): os.makedirs(backup, exist_okTrue) target fbackup/ledger_{date.today().isoformat()}.db shutil.copy2(DB_PATH, target)逻辑说明copy2 保留文件修改时间备份文件名带上日期一天一行备份文件不会互相覆盖。这样即使软删除后想恢复也可以从当天的备份文件里找回记录再把 is_deleted 改回 0。我在源码里见过的翻车情况大多不是程序逻辑写错而是没有留恢复通道记住一句话能恢复的账本才敢放手去删。6. 更进一步的三种做法给源码加上桌面表单、自动备份与数据迁移6.1 用 Tkinter 把命令行入口换成桌面表单简易源码落地后大部分人很快就会不满足于在终端里敲函数而是想要一个能填表单的窗口。Python 自带的 tkinter 不需要额外安装依赖做最简单的记账窗口足够日期输入框、分类下拉框、金额输入框、保存按钮布局用 pack 或 grid 都可以。import tkinter as tk from tkinter import ttk def save_from_gui(date_var, category_var, amount_var, status_label): add_transaction( trans_datedate_var.get(), categorycategory_var.get(), amountfloat(amount_var.get()) ) status_label.config(text已保存)逻辑说明GUI 层只是薄薄一层壳最终调用的仍然是第 3 章里的 add_transaction 函数业务逻辑没有被 GUI 替代。这样做的好处是命令行和窗口共用一套校验与入库逻辑不会出现“窗口能存、命令行不能存”的分叉。想快速验证效果直接 pip install 不存在的依赖不用额外装任何东西tkinter 是 Python 标准库的一部分。如果你习惯用 pandas 和 matplotlib 做统计可以在这个界面里放一个“打开报表”按钮直接调用第 4 章的绘图函数。6.2 备份、数据迁移与对账习惯账本运行半年后数据量会从几十行涨到几千行CSV 或 SQLite 都不至于撑不住但备份必须坚持。我自己的习惯是每周手动执行一次备份命令同时把关键流水导出成 CSV 存档。数据迁移最常见的是 SQLite 到 CSV、以及旧账本到新表结构两者都可以用 pandas 一行完成df_old pd.read_sql_query(SELECT * FROM transactions, conn_old) df_old.to_csv(legacy_backup.csv, indexFalse, encodingutf-8-sig)逻辑说明迁移前第一步永远是先导出一份 CSV 快照这是后悔药的后悔药。新库建好后再用 pandas 的 to_sql 写入或批量 INSERT 导入。金额列如果从元改成“分”这一步也是做数据清洗的最佳时机把 amount 改成 amount_cents 并在 SQL 层校验没有脏数据。对我个人而言记账软件最容易坚持不下去的原因不是功能不够而是数据不可信。我有一段时间每天对账都对不上后来发现是浮点尾差累积把金额改成整数“分”存储之后这个现象再也没出现过。给账本做一次干净迁移比加十个新功能都值钱。希望这套从数据模型到统计可视化再到避坑恢复的流程能帮你少走一些弯路。如果你也是自己动手记过账的人应该明白工具好不好用是其次账目真实、能随时翻出来才能真正坚持下来希望这篇笔记对你有用。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询