
家庭记账软件哪个好?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: float
category: str
note: str
date: datetime
def __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 datetime
class DBManager:
def __init__(self, db_name=accounts.db):
self.db_name = db_name
self.conn = None
self._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.lastrowid
def 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(fINSERT ... 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 datetime
def 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 = 0
for cat, amt in sorted(summary.items(), key=lambda x: x[1], reverse=True):
print(f{cat}: ¥{amt:.2f})
total += amt
print(f总计: ¥{total:.2f})
else:
print(本月无记录)
elif choice == '3':
print(再见!)
break
else:
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 0
def validate_date(date_str: str) - bool:
try:
datetime.strptime(date_str, %Y-%m-%d)
return True
except ValueError:
return False
在 main.py 中调用,无效输入直接拒绝,提升用户体验。
2. 导出CSV
很多用户需要把数据导给Excel分析。添加一个 export_csv 方法:
# 在 db_manager.py 中
import csv
def 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并发写入冲突,或者日期时区处理出错?评论区聊聊,看看大家的解决方案。