你是不是也有这样的烦恼:记账 APP 广告多、隐私不放心,或者功能太复杂懒得用?
今天做一个轻量级个人记账工具。用 Python + SQLite 实现记录、查询、统计一条龙,还能自动生成月度收支饼图和趋势折线图。
项目背景
记账的核心需求其实就三个:记一笔、查一下、看个趋势。市面上很多 APP 把这些简单的事情搞得很复杂。
自己做的好处是:
- 数据完全本地存储,不用上传服务器
- 想加什么功能随时改
- 顺便练手 Python 和数据库
技术选型
| 组件 | 选择 | 理由 |
|---|---|---|
| 数据库 | SQLite3 | Python 内置,零配置 |
| 数据存储 | CSV 导出 | 方便导入 Excel 做深度分析 |
| 可视化 | Matplotlib | 经典库,出图效果好 |
| 命令行交互 | input() + 循环 | 简单直接,不需要 Web 框架 |
实现步骤
第一步:创建记账数据库
"""
finance_db.py - 数据库操作模块
负责建表、增删改查
"""
import sqlite3
from datetime import datetime
DB_PATH = "finance.db"
def get_connection():
"""获取数据库连接"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row # 让结果可以用列名访问
return conn
def init_db():
"""初始化数据库表结构"""
conn = get_connection()
cursor = conn.cursor()
# 分类表
cursor.execute("""
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
type TEXT CHECK(type IN ('收入', '支出')) NOT NULL
)
""")
# 交易记录表
cursor.execute("""
CREATE TABLE IF NOT EXISTS transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER,
amount REAL NOT NULL,
type TEXT CHECK(type IN ('收入', '支出')) NOT NULL,
note TEXT,
date TEXT NOT NULL,
FOREIGN KEY (category_id) REFERENCES categories(id)
)
""")
# 插入默认分类
default_categories = [
("工资", "收入"), ("奖金", "收入"), ("理财收益", "收入"),
("餐饮", "支出"), ("交通", "支出"), ("购物", "支出"),
("住房", "支出"), ("娱乐", "支出"), ("医疗", "支出"),
("其他", "支出"), ("其他收入", "收入")
]
for name, cat_type in default_categories:
cursor.execute(
"INSERT OR IGNORE INTO categories (name, type) VALUES (?, ?)",
(name, cat_type)
)
conn.commit()
conn.close()
print("✅ 数据库初始化完成")
def add_transaction(category_name, amount, transaction_type, note=""):
"""添加一笔交易记录"""
conn = get_connection()
cursor = conn.cursor()
# 查找分类 ID
cursor.execute(
"SELECT id FROM categories WHERE name = ? AND type = ?",
(category_name, transaction_type)
)
row = cursor.fetchone()
if not row:
print(f"❌ 分类 '{category_name}' 不存在")
conn.close()
return False
date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cursor.execute(
"INSERT INTO transactions (category_id, amount, type, note, date) VALUES (?, ?, ?, ?, ?)",
(row["id"], amount, transaction_type, note, date)
)
conn.commit()
conn.close()
print(f"✅ 已记录: {transaction_type} {category_name} ¥{amount:.2f}")
return True
def get_monthly_summary(year, month):
"""获取某月的收支汇总"""
conn = get_connection()
cursor = conn.cursor()
month_str = f"{year}-{month:02d}"
# 总支出
cursor.execute("""
SELECT c.name as category, SUM(t.amount) as total
FROM transactions t
JOIN categories c ON t.category_id = c.id
WHERE t.type = '支出' AND t.date LIKE ?
GROUP BY c.name
ORDER BY total DESC
""", (f"{month_str}%",))
expense_by_category = cursor.fetchall()
# 总收入
cursor.execute("""
SELECT c.name as category, SUM(t.amount) as total
FROM transactions t
JOIN categories c ON t.category_id = c.id
WHERE t.type = '收入' AND t.date LIKE ?
GROUP BY c.name
ORDER BY total DESC
""", (f"{month_str}%",))
income_by_category = cursor.fetchall()
# 本月总支出和总收入
cursor.execute("""
SELECT
COALESCE(SUM(CASE WHEN type='支出' THEN amount ELSE 0 END), 0) as total_expense,
COALESCE(SUM(CASE WHEN type='收入' THEN amount ELSE 0 END), 0) as total_income
FROM transactions
WHERE date LIKE ?
""", (f"{month_str}%",))
summary = cursor.fetchone()
conn.close()
return {
"expenses": [(dict(r)["category"], dict(r)["total"]) for r in expense_by_category],
"incomes": [(dict(r)["category"], dict(r)["total"]) for r in income_by_category],
"total_expense": summary["total_expense"],
"total_income": summary["total_income"],
}
def get_daily_transactions(limit=20):
"""获取最近的交易记录"""
conn = get_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT t.*, c.name as category_name, c.type as category_type
FROM transactions t
JOIN categories c ON t.category_id = c.id
ORDER BY t.date DESC
LIMIT ?
""", (limit,))
rows = cursor.fetchall()
conn.close()
return [dict(r) for r in rows]
第二步:创建可视化模块
"""
charts.py - 统计图表生成模块
"""
import matplotlib.pyplot as plt
import matplotlib
matplotlib.rcParams["font.sans-serif"] = ["SimHei", "Arial Unicode MS", "DejaVu Sans"]
matplotlib.rcParams["axes.unicode_minus"] = False
from finance_db import get_monthly_summary, init_db
def plot_expense_pie(year, month):
"""绘制月度支出饼图"""
data = get_monthly_summary(year, month)
if not data["expenses"]:
print(f"⚠️ {year}年{month}月没有支出记录")
return
categories, amounts = zip(*data["expenses"])
fig, ax = plt.subplots(figsize=(10, 6))
wedges, texts, autotexts = ax.pie(
amounts, labels=categories, autopct="%1.1f%%",
startangle=90, colors=plt.cm.Pastel1(range(len(categories)))
)
ax.set_title(f"{year}年{month}月 支出分布", fontsize=16)
plt.tight_layout()
plt.savefig(f"expense_{year}_{month:02d}.png", dpi=150)
print(f"📊 支出饼图已保存为 expense_{year}_{month:02d}.png")
plt.close()
def plot_income_bar(year, month):
"""绘制月度收入柱状图"""
data = get_monthly_summary(year, month)
if not data["incomes"]:
print(f"⚠️ {year}年{month}月没有收入记录")
return
categories, amounts = zip(*data["incomes"])
fig, ax = plt.subplots(figsize=(10, 5))
bars = ax.bar(categories, amounts, color=["#4CAF50", "#66BB6A", "#81C784", "#A5D6A7"][:len(categories)])
# 在柱子上标注金额
for bar, amount in zip(bars, amounts):
ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 50,
f"¥{amount:.0f}", ha="center", va="bottom", fontsize=10)
ax.set_title(f"{year}年{month}月 收入明细", fontsize=16)
ax.set_ylabel("金额(元)")
plt.tight_layout()
plt.savefig(f"income_{year}_{month:02d}.png", dpi=150)
print(f"📊 收入柱状图已保存为 income_{year}_{month:02d}.png")
plt.close()
def generate_monthly_report(year, month):
"""生成月度统计报告并绘图"""
data = get_monthly_summary(year, month)
print(f"\n{'='*50}")
print(f" {year}年{month}月 财务简报")
print(f"{'='*50}")
print(f" 总收入: ¥{data['total_income']:,.2f}")
print(f" 总支出: ¥{data['total_expense']:,.2f}")
balance = data['total_income'] - data['total_expense']
print(f" 结余: ¥{balance:,.2f}")
print(f"{'='*50}\n")
if data["expenses"]:
print(" 💰 支出明细:")
for cat, amt in data["expenses"]:
print(f" {cat}: ¥{amt:,.2f}")
print()
if data["incomes"]:
print(" 💵 收入明细:")
for cat, amt in data["incomes"]:
print(f" {cat}: ¥{amt:,.2f}")
print()
# 生成图表
if data["expenses"]:
plot_expense_pie(year, month)
if data["incomes"]:
plot_income_bar(year, month)
第三步:创建主程序
"""
main.py - 记账本主程序
"""
from finance_db import init_db, add_transaction, get_daily_transactions
from charts import generate_monthly_report
def show_menu():
"""显示主菜单"""
print("\n" + "="*40)
print(" 📒 我的记账本")
print("="*40)
print(" 1. 添加支出")
print(" 2. 添加收入")
print(" 3. 最近记录")
print(" 4. 月度报告")
print(" 5. 退出")
print("="*40)
def add_expense():
"""添加支出"""
categories = ["餐饮", "交通", "购物", "住房", "娱乐", "医疗", "其他"]
print("\n 可选分类:", ", ".join(categories))
name = input(" 分类: ").strip()
if name not in categories:
print(" ❌ 无效分类")
return
try:
amount = float(input(" 金额: ").strip())
except ValueError:
print(" ❌ 请输入有效数字")
return
note = input(" 备注(回车跳过): ").strip()
add_transaction(name, amount, "支出", note)
def add_income():
"""添加收入"""
categories = ["工资", "奖金", "理财收益", "其他收入"]
print("\n 可选分类:", ", ".join(categories))
name = input(" 分类: ").strip()
if name not in categories:
print(" ❌ 无效分类")
return
try:
amount = float(input(" 金额: ").strip())
except ValueError:
print(" ❌ 请输入有效数字")
return
note = input(" 备注(回车跳过): ").strip()
add_transaction(name, amount, "收入", note)
def show_recent():
"""显示最近交易"""
transactions = get_daily_transactions(15)
if not transactions:
print(" 📭 暂无记录")
return
print("\n {'日期':<12} {'分类':<8} {'类型':<4} {'金额':>10} {'备注'}")
print(" " + "-"*60)
for t in transactions:
print(f" {t['date'][:10]:<12} {t['category_name']:<8} "
f"{t['category_type']:<4} ¥{t['amount']:>9.2f} {t['note'] or ''}")
def main():
# 初始化数据库
init_db()
while True:
show_menu()
choice = input(" 请选择 [1-5]: ").strip()
if choice == "1":
add_expense()
elif choice == "2":
add_income()
elif choice == "3":
show_recent()
elif choice == "4":
try:
year = int(input(" 年份: "))
month = int(input(" 月份: "))
generate_monthly_report(year, month)
except ValueError:
print(" ❌ 请输入有效数字")
elif choice == "5":
print(" 👋 再见!")
break
else:
print(" ❌ 无效选项")
if __name__ == "__main__":
main()
第四步:运行
# 安装依赖
pip install matplotlib
# 运行记账本
python main.py
首次运行会自动创建 finance.db 数据库和默认分类。之后就可以开始记账了。
运行效果
========================================
📒 我的记账本
========================================
1. 添加支出
2. 添加收入
3. 最近记录
4. 月度报告
5. 退出
========================================
请选择 [1-5]: 1
可选分类: 餐饮, 交通, 购物, 住房, 娱乐, 医疗, 其他
分类: 餐饮
金额: 35.5
备注(回车跳过): 午餐
✅ 已记录: 支出 餐饮 ¥35.50
请选择 [1-5]: 4
年份: 2026
月份: 7
==================================================
2026年7月 财务简报
==================================================
总收入: ¥8,000.00
总支出: ¥1,250.50
结余: ¥6,749.50
==================================================
💰 支出明细:
餐饮: ¥850.50
交通: ¥200.00
购物: ¥200.00
📊 支出饼图已保存为 expense_2026_07.png
📊 收入柱状图已保存为 income_2026_07.png
生成的饼图和柱状图会保存在当前目录,可以直接查看或分享。
优化方向
- Web 界面:用 Flask 或 FastAPI 加一个网页端,手机也能记账
- 数据导出:增加 CSV/Excel 导出功能,方便做年度分析
- 预算提醒:给每个分类设置月度预算,超支时弹出警告
- 多账户支持:支持现金、银行卡、信用卡多个账户分别记账
- 定时同步:配合 cron 定期备份数据库到云端
这个记账本虽然简单,但覆盖了 Python 开发中最常用的几个技能:数据库操作、数据可视化、命令行交互。把它跑通一遍,对这些概念的理解会扎实很多。
要不要试试把支出分类再细化一些,比如餐饮下面再分早餐、午餐、晚餐?