家庭记账软件哪个好?Python实战从入门到精通
刚复制来的代码在本地跑不通,报错信息满屏飘,这种崩溃感我懂。很多新手卡在环境配置和逻辑报错上,以为是自己笨,其实多半是忽略了底层细节。想要真正掌握家庭记账软件哪个好这个选型问题,光看评测没用,得动手写一个。今天这篇教程带你从零搭建一个轻量级记账系统,从入门到精通,把代码跑通、逻辑理清,让你不再被各种Bug劝退。
项目目标与痛点分析
咱们先明确要解决什么。市面上记账App琳琅满目,但很多要么广告多,要么数据导出麻烦,要么隐私存疑。对于程序员来说,自建一个本地化、数据完全掌控的记账工具,不仅是为了省钱,更是为了练手。
核心痛点其实就三个:数据持久化、分类统计、可视化展示。
很多初学者写的记账脚本,重启电脑数据就没了,因为只存在内存里。这就像你修路没打地基,看着还行,一压就塌。我们要做的,是一个基于SQLite的轻量级应用,数据存在本地文件里,安全、快速、零依赖。
为什么选SQLite?因为它遵循RFC 规范中关于数据交换与存储的最佳实践思路,虽然SQLite本身是库,但其ACID事务特性符合工业级数据一致性要求。更重要的是,它单文件部署,不需要启动服务器,适合个人工具。
我们的目标很简单:
- 增删改查:记录每一笔开销,支持修改和删除。
- 分类统计:按月、按类别统计支出。
- 命令行交互:不用写前端,直接在终端输入指令即可操作。
这个目标看似简单,但涉及数据库连接管理、SQL注入防护、异常处理等核心技能。这也是从入门到精通必经的关卡。
目录结构与依赖安装
别一上来就写代码,工程化思维很重要。一个混乱的目录结构会让后期维护变成噩梦。
我们采用最简洁的结构:
family_accounting/
├── main.py # 入口文件
├── db_manager.py # 数据库操作模块
├── models.py # 数据模型定义
└── utils.py # 工具函数
db_manager.py 负责所有与SQLite打交道的逻辑,models.py 定义数据结构,utils.py 处理日期格式化、货币转换等杂活。main.py 则是用户交互界面。
安装依赖很简单,我们只用标准库,不需要pip安装第三方包。Python 3.8+ 自带 sqlite3 模块,这保证了最大兼容性。
如果你用的是Windows,确保环境变量配好;如果是Mac/Linux,直接运行即可。记住,环境一致是代码跑通的前提。很多“复制来的代码跑不通”,就是因为你在Py2环境跑Py3代码,或者路径分隔符不同。
核心代码实现
1. 数据库初始化与模型定义
先看 models.py,我们用简单的类来封装数据,避免裸字典传递。
# models.py
from dataclasses import dataclass
from datetime import datetime@dataclass
class Transaction:amount: floatcategory: strnote: strdate: datetimedef __post_init__(self):# 确保日期格式统一if isinstance(self.date, str):self.date = datetime.strptime(self.date, "%Y-%m-%d")
使用 dataclass 可以让代码更简洁,类型提示也更清晰。这是Python 3.7+ 的特性,推荐新手掌握。
接下来是核心中的核心:db_manager.py。这里我们要解决两个关键问题:连接管理和SQL安全。
# db_manager.py
import sqlite3
from models import Transaction
from datetime import datetimeclass DBManager:def __init__(self, db_name="accounts.db"):self.db_name = db_nameself.conn = Noneself._create_tables()def _connect(self):"""建立数据库连接,确保资源释放"""if not self.conn:self.conn = sqlite3.connect(self.db_name)self.conn.row_factory = sqlite3.Row # 让查询结果像字典一样访问def _create_tables(self):"""初始化表结构"""self._connect()cursor = self.conn.cursor()cursor.execute('''CREATE TABLE IF NOT EXISTS transactions (id INTEGER PRIMARY KEY AUTOINCREMENT,amount REAL NOT NULL,category TEXT NOT NULL,note TEXT,date TEXT NOT NULL,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')self.conn.commit()def add_transaction(self, txn: Transaction):"""插入新交易记录注意:使用参数化查询防止SQL注入"""self._connect()cursor = self.conn.cursor()# 关键:使用 ? 占位符,绝对不要拼接字符串cursor.execute("INSERT INTO transactions (amount, category, note, date) VALUES (?, ?, ?, ?)",(txn.amount, txn.category, txn.note, txn.date.strftime("%Y-%m-%d")))self.conn.commit()return cursor.lastrowiddef get_monthly_summary(self, year: int, month: int):"""获取月度统计返回:{category: total_amount}"""self._connect()cursor = self.conn.cursor()# 使用 LIKE 进行模糊匹配日期date_prefix = f"{year}-{month:02d}"cursor.execute('''SELECT category, SUM(amount) as total FROM transactions WHERE date LIKE ?GROUP BY category''', (f"{date_prefix}%",))return {row['category']: row['total'] for row in cursor.fetchall()}def close(self):"""关闭数据库连接"""if self.conn:self.conn.close()
逐行讲解关键点:
row_factory = sqlite3.Row:这行代码至关重要。默认情况下,查询结果返回的是元组(value1, value2),你需要知道索引位置才能取值。设置后,你可以用row['category']访问,代码可读性提升十倍。- 参数化查询
?:这是安全底线。如果你写成cursor.execute(f"INSERT ... VALUES ({amount}...)"),一旦用户输入包含'字符,就会报错甚至被注入。务必养成使用占位符的习惯。 date LIKE ?:SQLite的日期函数不如MySQL丰富,用LIKE匹配年月前缀是高效且通用的做法。
2. 主程序交互逻辑
main.py 负责接收用户输入并调用上述方法。
# main.py
import sys
from db_manager import DBManager
from models import Transaction
from datetime import datetimedef main():db = DBManager()print("=== 家庭记账助手 ===")print("1. 记一笔")print("2. 月度统计")print("3. 退出")try:while True:choice = input("\n请选择操作: ").strip()if choice == '1':amount = float(input("金额: "))category = input("类别 (如:餐饮,交通): ").strip()note = input("备注 (可选): ").strip()# 默认今天,可手动输入日期date_input = input("日期 (YYYY-MM-DD, 回车默认今天): ").strip()if date_input:date = datetime.strptime(date_input, "%Y-%m-%d")else:date = datetime.now()txn = Transaction(amount, category, note, date)db.add_transaction(txn)print("✅ 记录成功!")elif choice == '2':year = int(input("年份: "))month = int(input("月份: "))summary = db.get_monthly_summary(year, month)if summary:print(f"\n--- {year}年{month}月 支出统计 ---")total = 0for cat, amt in sorted(summary.items(), key=lambda x: x[1], reverse=True):print(f"{cat}: ¥{amt:.2f}")total += amtprint(f"总计: ¥{total:.2f}")else:print("本月无记录")elif choice == '3':print("再见!")breakelse:print("无效输入,请重试")except KeyboardInterrupt:print("\n\n程序中断")finally:db.close()if __name__ == "__main__":main()
运行与测试
代码写完,别急着觉得完事了。测试是区分新手和熟手的关键。
基础功能测试: 运行
python main.py,选择1,输入金额100,类别“餐饮”。再输入一笔50元“交通”。然后选择2,查看本月统计。你应该看到“餐饮: ¥100.00”和“交通: ¥50.00”。异常测试:
- 输入非数字金额:应该捕获
ValueError异常(当前代码未处理,建议你在float(input(...))外加 try-except)。 - 输入非法日期:如
2023-13-45,strptime会抛出异常,需提示用户格式错误。
- 输入非数字金额:应该捕获
数据持久化测试: 关闭程序,重新运行,查询之前的记录。如果数据还在,说明SQLite配置正确。
常见坑点:
- 权限问题:在某些Linux系统或macOS上,直接在项目根目录创建
accounts.db可能因权限不足失败。建议指定绝对路径或确保目录可写。 - 编码问题:如果备注中包含中文,确保终端编码为UTF-8。Python 3 默认UTF-8,通常无碍,但Windows CMD用户需注意。
优化扩展与进阶技巧
当基础功能跑通后,如何让它更“精通”?
1. 增加数据校验
在 utils.py 中加入验证函数:
# utils.py
def validate_amount(amount: float) -> bool:return amount > 0def validate_date(date_str: str) -> bool:try:datetime.strptime(date_str, "%Y-%m-%d")return Trueexcept ValueError:return False
在 main.py 中调用,无效输入直接拒绝,提升用户体验。
2. 导出CSV
很多用户需要把数据导给Excel分析。添加一个 export_csv 方法:
# 在 db_manager.py 中
import csvdef export_csv(self, filename="export.csv"):self._connect()cursor = self.conn.cursor()cursor.execute("SELECT * FROM transactions ORDER BY date DESC")rows = cursor.fetchall()with open(filename, 'w', newline='', encoding='utf-8') as f:writer = csv.writer(f)writer.writerow(["ID", "Amount", "Category", "Note", "Date"])for row in rows:writer.writerow([row['id'], row['amount'], row['category'], row['note'], row['date']])print(f"已导出到 {filename}")
3. 性能优化
如果数据量超过万级,每次查询都打开连接会慢。可以改用连接池或保持长连接。对于个人工具,SQLite的默认配置已足够快,瓶颈通常在I/O,而非CPU。
4. 安全性增强
虽然本地应用风险低,但养成好习惯很重要。如果未来扩展到Web版,务必使用HTTPS,并遵循RFC 2818等安全传输规范。即使本地,也建议对敏感备注进行加密存储,使用Python的 cryptography 库。
小结
回到最初的问题:家庭记账软件哪个好?
对于程序员而言,自己写的就是好的。因为它完全契合你的使用习惯,数据掌握在自己手里,而且在这个过程中,你锻炼了数据库设计、异常处理、工程化思维等核心能力。
从入门到精通,不在于你用了多炫的框架,而在于你能否把一个简单的问题做扎实、做安全、做可维护。这个记账小项目,看似简单,实则涵盖了CRUD、参数化查询、数据验证、文件导出等高频场景。
代码不是抄来的,是调出来的。当你亲手修好每一个Bug,理解每一行代码的作用时,你才真正具备了从0到1构建产品的能力。
你在项目里踩过这个坑吗?比如SQLite并发写入冲突,或者日期时区处理出错?评论区聊聊,看看大家的解决方案。