背景介绍
日常支出管理是理财的基础,但手动记录和统计既繁琐又容易出错。开发一个轻量化的支出统计工具,不仅能帮助我们高效管理支出,还能通过实践巩固文件操作、数据统计、可视化的编程技能。本文将带你从需求分析到代码实现,打造一个支持「记录支出、统计报表、可视化图表」的个人支出管理工具。
思路分析
该工具的核心需求可拆解为三个模块:
1. 支出记录:通过命令行收集「日期、类别、金额、备注」,持久化到CSV文件(简单易读,适合本地存储)。
2. 统计报表:按「类别」或「时间段」分组,计算支出总额和笔数,控制台输出直观结果。
3. 可视化图表:用Matplotlib生成「类别占比饼图」或「月度支出柱状图」,将数据转化为更直观的图形。
技术选型上,Python的csv库处理文件,datetime处理日期,matplotlib实现可视化,命令行交互通过input完成,无需复杂框架,适合新手实践。
代码实现
下面是完整的代码实现,包含详细注释:
import csv
import os
from datetime import datetime
import matplotlib.pyplot as plt
# 支出数据文件路径(本地CSV)
EXPENSES_FILE = 'expenses.csv'
def add_expense():
"""添加支出记录到CSV文件"""
# 输入日期并验证格式
date = input("日期(格式:YYYY-MM-DD):")
try:
datetime.strptime(date, '%Y-%m-%d')
except ValueError:
print("日期格式错误,请使用YYYY-MM-DD格式(例如:2023-10-01)!")
return
# 输入类别并验证(限制为餐饮/交通/购物/娱乐)
category = input("支出类别(餐饮/交通/购物/娱乐):")
valid_categories = ['餐饮', '交通', '购物', '娱乐']
if category not in valid_categories:
print(f"支出类别必须为{valid_categories}之一!")
return
# 输入金额并验证为数字
try:
amount = float(input("金额(数字,例如:10.5):"))
except ValueError:
print("金额必须是有效的数字!")
return
# 输入备注(可选,直接回车则为空)
remark = input("备注(可选,直接回车则为空):") or ""
# 写入CSV文件(追加模式,自动创建表头)
fieldnames = ['日期', '类别', '金额', '备注']
file_exists = os.path.exists(EXPENSES_FILE)
with open(EXPENSES_FILE, 'a', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
if not file_exists:
writer.writeheader() # 文件不存在时写入表头
writer.writerow({
'日期': date,
'类别': category,
'金额': amount,
'备注': remark
})
print(f"支出记录已成功添加到 {EXPENSES_FILE} 文件中。")
def report_expense():
"""生成支出统计报表(控制台输出+可视化图表)"""
report_type = input("请选择报表类型(category:按类别统计 / time:按时间统计):")
start_date_str = input("请输入统计起始日期(YYYY-MM-DD):")
end_date_str = input("请输入统计结束日期(YYYY-MM-DD):")
# 验证日期格式
try:
start_date = datetime.strptime(start_date_str, '%Y-%m-%d')
end_date = datetime.strptime(end_date_str, '%Y-%m-%d')
except ValueError:
print("日期格式错误,请使用YYYY-MM-DD格式!")
return
# 读取支出数据(筛选日期范围内的记录)
expenses = []
if os.path.exists(EXPENSES_FILE):
with open(EXPENSES_FILE, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
row_date = datetime.strptime(row['日期'], '%Y-%m-%d')
if start_date <= row_date <= end_date:
expenses.append({
'日期': row_date,
'类别': row['类别'],
'金额': float(row['金额']),
'备注': row['备注']
})
else:
print(f"错误:{EXPENSES_FILE} 文件不存在,无支出记录!")
return
if not expenses:
print("该时间段内无支出记录!")
return
if report_type == 'category':
# 按类别统计:金额总和、笔数
category_stats = {}
total_amount = 0.0
total_count = 0
for exp in expenses:
category = exp['类别']
amount = exp['金额']
if category not in category_stats:
category_stats[category] = {'amount': 0.0, 'count': 0}
category_stats[category]['amount'] += amount
category_stats[category]['count'] += 1
total_amount += amount
total_count += 1
# 输出统计报表
print(f"\n===== 支出类别统计({start_date_str} ~ {end_date_str}) ======")
for cat, stats in category_stats.items():
print(f"{cat}:{stats['amount']:.2f}元(共{stats['count']}笔)")
print(f"总计:{total_amount:.2f}元(共{total_count}笔)")
# 生成类别占比饼图
categories = list(category_stats.keys())
amounts = [stats['amount'] for stats in category_stats.values()]
generate_pie_chart(categories, amounts, start_date_str, end_date_str, 'category')
elif report_type == 'time':
# 按月份统计(年月格式:YYYY-MM)
month_stats = {}
for exp in expenses:
month_key = exp['日期'].strftime('%Y-%m') # 提取年月作为分组键
if month_key not in month_stats:
month_stats[month_key] = {'amount': 0.0, 'count': 0}
month_stats[month_key]['amount'] += exp['金额']
month_stats[month_key]['count'] += 1
# 输出统计报表(按月份排序)
print(f"\n===== 支出时间统计({start_date_str} ~ {end_date_str}) ======")
total_amount = 0.0
total_count = 0
for month, stats in sorted(month_stats.items()):
print(f"{month}:{stats['amount']:.2f}元(共{stats['count']}笔)")
total_amount += stats['amount']
total_count += stats['count']
print(f"总计:{total_amount:.2f}元(共{total_count}笔)")
else:
print("报表类型错误,可选:category(按类别) / time(按时间)")
def generate_pie_chart(categories, amounts, start_date, end_date, chart_type):
"""生成饼图(类别占比或其他类型)"""
plt.figure(figsize=(8, 6))
plt.pie(
amounts,
labels=categories,
autopct='%1.1f%%', # 显示百分比(保留1位小数)
startangle=140, # 饼图起始角度(避免标签重叠)
textprops={'fontsize': 10}
)
plt.axis('equal') # 保证饼图为正圆形
title = f'支出{chart_type}占比({start_date} ~ {end_date})'
plt.title(title, fontsize=12)
plt.tight_layout() # 调整布局,避免标签被截断
# 生成带时间戳的文件名(避免重复)
filename = f"{chart_type}_pie_{start_date.replace('-','')}_{end_date.replace('-','')}.png"
plt.savefig(filename)
print(f"图表已生成:{filename}")
plt.close() # 关闭图表,释放内存
def generate_bar_chart(months, amounts, start_date, end_date):
"""生成月度支出柱状图"""
plt.figure(figsize=(10, 6))
x = range(len(months))
plt.bar(x, amounts, width=0.6, color='#1f77b4')
# 设置x轴标签(月份),旋转45度避免重叠
plt.xticks(x, months, rotation=45, ha='right', fontsize=9)
plt.xlabel('月份', fontsize=10)
plt.ylabel('支出金额(元)', fontsize=10)
title = f'月度支出统计({start_date} ~ {end_date})'
plt.title(title, fontsize=12)
plt.tight_layout()
# 生成带时间戳的文件名
filename = f"monthly_bar_{start_date.replace('-','')}_{end_date.replace('-','')}.png"
plt.savefig(filename)
print(f"图表已生成:{filename}")
plt.close()
def generate_chart():
"""生成可视化图表(月度柱状图或类别饼图)"""
chart_type = input("请选择图表类型(monthly:月度柱状图 / category:类别饼图):")
start_date_str = input("请输入统计起始日期(YYYY-MM-DD):")
end_date_str = input("请输入统计结束日期(YYYY-MM-DD):")
# 验证日期格式
try:
start_date = datetime.strptime(start_date_str, '%Y-%m-%d')
end_date = datetime.strptime(end_date_str, '%Y-%m-%d')
except ValueError:
print("日期格式错误,请使用YYYY-MM-DD格式!")
return
# 读取支出数据
expenses = []
if os.path.exists(EXPENSES_FILE):
with open(EXPENSES_FILE, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
row_date = datetime.strptime(row['日期'], '%Y-%m-%d')
if start_date <= row_date <= end_date:
expenses.append({
'日期': row_date,
'类别': row['类别'],
'金额': float(row['金额']),
'备注': row['备注']
})
else:
print(f"错误:{EXPENSES_FILE} 文件不存在,无支出记录!")
return
if not expenses:
print("该时间段内无支出记录!")
return
if chart_type == 'monthly':
# 按月统计金额
month_stats = {}
for exp in expenses:
month_key = exp['日期'].strftime('%Y-%m')
if month_key not in month_stats:
month_stats[month_key] = 0.0
month_stats[month_key] += exp['金额']
# 按月份排序
months = sorted(month_stats.keys())
amounts = [month_stats[month] for month in months]
generate_bar_chart(months, amounts, start_date_str, end_date_str)
elif chart_type == 'category':
# 按类别统计金额
category_stats = {}
for exp in expenses:
category = exp['类别']
if category not in category_stats:
category_stats[category] = 0.0
category_stats[category] += exp['金额']
categories = list(category_stats.keys())
amounts = list(category_stats.values())
generate_pie_chart(categories, amounts, start_date_str, end_date_str, 'category')
else:
print("图表类型错误,可选:monthly(月度柱状图) / category(类别饼图)")
def main():
"""主函数:命令行交互入口"""
print("欢迎使用个人支出统计与可视化工具!")
while True:
choice = input("\n请选择操作(add:添加记录 / report:统计报表 / chart:生成图表 / exit:退出):")
if choice == 'add':
add_expense()
elif choice == 'report':
report_expense()
elif choice == 'chart':
generate_chart()
elif choice == 'exit':
print("感谢使用,祝您支出合理,生活愉快!")
break
else:
print("无效操作,请重新选择!")
if __name__ == "__main__":
main()
关键逻辑解析
- 文件持久化:使用
csv.DictWriter以追加模式写入数据,自动处理表头(文件不存在时创建),确保数据格式统一。 - 数据统计:
- 按类别统计时,用字典
category_stats累加金额和笔数,时间复杂度O(n)。 - 按时间统计时,提取「年月」作为分组键(如
2023-10),确保数据按月份有序输出。
- 按类别统计时,用字典
- 可视化细节:
- 饼图通过
autopct='%1.1f%%'显示百分比,startangle调整起始角度避免标签重叠。 - 柱状图通过
rotation=45旋转x轴标签,tight_layout()避免图表元素被截断。
- 饼图通过
总结
本工具通过「文件操作+数据统计+可视化」的组合,实现了从支出记录到图表分析的全流程。核心难点在于日期格式验证(确保输入合法性)、数据分组统计(字典高效累加)和Matplotlib图表美化(布局、标签优化)。
通过这个项目,你不仅能掌握Python基础库的使用,还能理解「数据采集→处理→可视化」的完整链路。若需扩展,可增加「支出预算预警」「多用户支持」等功能,进一步提升工具实用性。
赶快运行代码,开始记录你的支出,看看钱都花去哪儿了吧!