├── .gitignore ├── 自动记账2.0_可视化.xlsx ├── 自动记账2.0_源数据.xlsx ├── README.md ├── KeepAccounts_v2.0.py └── AddTag.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.xlsx 2 | -------------------------------------------------------------------------------- /自动记账2.0_可视化.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benature/bill/HEAD/自动记账2.0_可视化.xlsx -------------------------------------------------------------------------------- /自动记账2.0_源数据.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benature/bill/HEAD/自动记账2.0_源数据.xlsx -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 原仓库使用了 macOS 不支持的 `msvcrt` ([#1](https://github.com/MickLife/KeepAccounts_v2.0/issues/1)),本仓库对此作出修改,可以在 macOS 系统上运行。 2 | 3 | --- 4 | # KeepAccounts_v2.0 5 | KeepAccounts.exe和其配套表格能够实现微信、支付宝官方导出账单的读取合并,为每笔帐标记类型,并按月份和类型生成可视化图表。再也不用消费一笔记一笔,每月仅需10分钟,记好所有的帐。 6 | 7 | 原作者: MickLife 8 | 9 | **相比较原作者内容,我添加了新文件addtag.py,可以方便的在控制台中添加标签了,不用再excel一个一个点选,速度更快,使用更加方便** 10 | 11 | Bilibili: https://space.bilibili.com/38626658 12 | 13 | Github: https://github.com/MickLife/KeepAccounts_v2.0 14 | 15 | 程序和表格下载链接:https://pan.baidu.com/s/1trgfNS6RuXJwy_NWVSo74Q 提取码:84d3 16 | 17 | ### v2.0更新内容 18 | 19 | 1. 利用python脚本编写程序,自动合并微信、支付宝账单,节省了操作时间。 20 | 2. 更新记账分类方法,使记账有助于改善你的消费习惯 21 | 3. 更新Excel明细页和可视化页,增加数据透视表和数据透视图。 22 | *** 23 | # 如何使用 24 | 25 | ### 第一步 下载账单 26 | 27 | **微信账单** 28 | 29 | 1. 进入手机版微信,选择 “我”,进入用户中心界面,然后点击 “支付” 选项; 30 | 2. 点击 “钱包”,进入钱包界面后,点击右上角的 “账单” 按钮; 31 | 3. 点击右上角“常见问题”,点击“下载账单”->“用于个人对账”; 32 | 4. 自定义账单时间,然后点击 “下一步”; 33 | 5. 填写要导出的邮箱(微信会把账单发送到你填写的邮箱),点击 “下一步”; 34 | 6. 输入支付密码,提示申请已提交,微信官方会给你发送一条消息,里面有账单的解压码; 35 | 8. 前往你的邮箱下载得到压缩包,用解压码解压得到 .csv 格式微信账单,导出成功。 36 | 37 | **支付宝账单** 38 | 1. 电脑浏览器中打开支付宝官网 https://www.alipay.com/ 39 | 2. 点击右上角“客户服务”->“自助服务”; 40 | 3. 在“交易服务”中点击“交易记录”一项; 41 | 4. 扫码登录; 42 | 5. 选择交易时间,并选择下载 excel 格式,得到 .zip 压缩包(其实是 .csv 格式,这是一种更轻便的文本格式); 43 | 6. 解压压缩包得到 .csv 格式的支付宝账单,导出成功。 44 | 45 | **备注:** 46 | 商家用户请勿从商家中心导出,否则数据格式不同无法使用本程序导入账单。请按以上步骤或切换至个人版页面导出。 47 | 48 | ### 第二步 运行程序合并账单 49 | 1. 将 KeepAccounts_v2.0.zip 解压,推荐解压至 D:\Program Files\; 50 | 2. 运行 KeepAccounts_v2.0 目录下的 **KeepAccounts.exe**; 51 | 3. 根据提示,依次选择微信 csv 账单、支付宝 csv 账单和账本文件(自动记账2.0_源数据.xlsx); 52 | 4. 程序会自动将微信和支付宝账单合并到你选择的账本文件。 53 | 5. 运行成功后按任意键退出。 54 | 55 | **备注:** 56 | * 程序会将账单中大部分中性支出、收入(如提现、退款)删除。 57 | * 小部分中性支出、收入会被程序识别,并在逻辑 2 标注 0,乘后金额会显示 0。 58 | * 由于算法的编写由个人完成,不能做到识别所有情况,如果一些中性支出、收入没能自动识别,请手动在源数据表格中将乘后金额改为 0 即可。 59 | 60 | ### 第三步 补充数据、标记类别 61 | 1. 打开“自动记账2.0_源数据.xlsx”; 62 | 2. 打开“明细”sheet页,在最后一行追加其他收入和支出数据(如现金、银行卡、校园卡、余额宝等消费情况); 63 | 3. 在最后两列的下拉列表中选择类别; 64 | 4. 填写时注意,“月份、乘后金额、类别标记1、类别标记2”为必填项,其他可视情况填写。 65 | 5. 追加数据后一定要保存 66 | 67 | ### 第四步 查看可视化图表 68 | 1. 打开“自动记账2.0_可视化.xlsx”前,最好不要关闭源数据表格; 69 | 2. 打开“自动记账2.0_可视化.xlsx”;(如果提示各种安全警告和更新链接询问,请点击“允许更新、启用内容”之类的选项) 70 | 3. **如果你是第一次打开这个表格,需要更新数据源连接属性。** 71 | 更新步骤: 72 | 73 | a. 请选择任意数据透视表中的任意一个单元格,点击“数据透视表工具-分析”选项卡,点击“更新数据源”处的下拉菜单,点击“连接属性” 74 | 75 | b. 在“连接属性”对话框中,点击“定义”选项卡 76 | 77 | c. 点击连接文件路径右侧的“浏览”,定位到表格文件的路径,选择“自动记账2.0_数据源.xlsx”文件,点击确定 78 | 79 | d. 在选择表格的弹窗中选择“明细$”,点击确定; 80 | 81 | e. 点击确定,看到数据自动更新。 82 | 83 | 4. 查看可视化图表,退出时记得保存。 84 | 85 | **备注:** 86 | 所有数据透视表、数据透视图中的筛选按钮均可点击,可以根据需求自定义。 87 | 88 | *** 89 | 90 | # Q&A 91 | 92 | #### 如何自定义消费类型? 93 | 1. 在“自动记账2.0_源数据.xlsx”文件的“消费类型2.0”sheet页修改类别; 94 | 2. 消费类别会同步出现在明细页的下拉列表、可视化的数据透视图和透视表中; 95 | 3. 第二行编辑后需在“公式”选项卡 - “名称管理器”中同步修改,否则二级下拉列表将失效。 96 | 97 | 备注: 98 | * 类别名称中勿包含空格、划线、标点符号等特殊字符,会导致bug 99 | * 如果不清楚背后的原理,请在B2:O12区域内编辑,不要新增行列 100 | * 请勿修改明细页的数据有效性公式,因为不使用INDIRECT公式改用直接引用会导致bug,下拉列表消失。 101 | * 如果修改后出现问题,请自行检索关键词,学习有关知识:数据有效性、二级下拉、INDIRECT函数、名称管理器。 102 | 103 | #### 打开可视化表格,数据没有更新怎么办? 104 | 答:第一次打开这个表格,需要更新数据源连接属性。后续打开时不必每次这样操作。如果你已经更新过连接属性,但数据仍没有更新,请右键数据透视表的任意单元格,点击“更新”。如果这样还是不行,请在数据透视表工具-分析选项卡中,点击刷新下面的小三角,点击“全部刷新”。 105 | 106 | #### 追加其他明细内容需要填写所有项吗? 107 | 答:“月份、乘后金额、类别标记1、类别标记2”为必填项,其他可视情况填写。 108 | 109 | #### 每月导入前需要删除上个月的明细吗? 110 | 答:不需要。程序会直接在明细页最后一行后附加新的数据。 111 | 112 | #### 第二年可以接着导入吗? 113 | 答:不可以,暂时还不支持筛选年份,因为不想增加工作量ㄒ_ㄒ。第二年就把表格copy一份,数据清空当作新表来记录吧!如果你有好的表格设计想法,欢迎私信与我交流呀。 114 | 115 | #### 怎么反馈bug或改进意见? 116 | 答:欢迎在B站私信 MickLife 反馈,一起携手改变世界! 117 | 118 | *** 119 | 附:Excel自动记账v1.0链接: 【Mick小课堂3】Excel自动化个人记账方案 表格分享 120 | https://www.bilibili.com/video/BV145411Y7Bj 121 | -------------------------------------------------------------------------------- /KeepAccounts_v2.0.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # version: 2.0 3 | # StartTime: 2021/1/6 12:30 4 | # Finished: 2021/1/7 20:30 5 | # Author: MickLife 6 | # B站: https://space.bilibili.com/38626658 7 | 8 | import pandas as pd 9 | import openpyxl 10 | import tkinter.filedialog 11 | import datetime 12 | 13 | 14 | def strip_in_data(data): # 把列名中和数据中首尾的空格都去掉。 15 | data = data.rename(columns={column_name: column_name.strip() for column_name in data.columns}) 16 | data = data.applymap(lambda x: x.strip().strip('¥') if isinstance(x, str) else x) 17 | return data 18 | 19 | 20 | def read_data_wx(path): # 获取微信数据 21 | d_wx = pd.read_csv(path, header=16, skipfooter=0, encoding='utf-8') # 数据获取,微信 22 | d_wx = d_wx.iloc[:, [0, 4, 7, 1, 2, 3, 5]] # 按顺序提取所需列 23 | d_wx = strip_in_data(d_wx) # 去除列名与数值中的空格。 24 | d_wx.iloc[:, 0] = d_wx.iloc[:, 0].astype('datetime64') # 数据类型更改 25 | d_wx.iloc[:, 6] = d_wx.iloc[:, 6].astype('float64') # 数据类型更改 26 | d_wx = d_wx.drop(d_wx[d_wx['收/支'] == '/'].index) # 删除'收/支'为'/'的行 27 | d_wx.rename(columns={'当前状态': '支付状态', '交易类型': '类型', '金额(元)': '金额'}, inplace=True) # 修改列名称 28 | d_wx.insert(1, '来源', "微信", allow_duplicates=True) # 添加微信来源标识 29 | len1 = len(d_wx) 30 | print("成功读取 " + str(len1) + " 条「微信」账单数据\n") 31 | return d_wx 32 | 33 | 34 | def read_data_zfb(path): # 获取支付宝数据 35 | d_zfb = pd.read_csv(path, header=4, skipfooter=7, encoding='gbk') # 数据获取,支付宝 36 | d_zfb = d_zfb.iloc[:, [2, 10, 11, 6, 7, 8, 9]] # 按顺序提取所需列 37 | d_zfb = strip_in_data(d_zfb) # 去除列名与数值中的空格。 38 | d_zfb.iloc[:, 0] = d_zfb.iloc[:, 0].astype('datetime64') # 数据类型更改 39 | d_zfb.iloc[:, 6] = d_zfb.iloc[:, 6].astype('float64') # 数据类型更改 40 | d_zfb = d_zfb.drop(d_zfb[d_zfb['收/支'] == ''].index) # 删除'收/支'为空的行 41 | d_zfb.rename(columns={'交易创建时间': '交易时间', '交易状态': '支付状态', '商品名称': '商品', '金额(元)': '金额'}, inplace=True) # 修改列名称 42 | d_zfb.insert(1, '来源', "支付宝", allow_duplicates=True) # 添加支付宝来源标识 43 | len2 = len(d_zfb) 44 | print("成功读取 " + str(len2) + " 条「支付宝」账单数据\n") 45 | return d_zfb 46 | 47 | 48 | def add_cols(data): # 为数据增加列 49 | # 逻辑1,取值-1 or 1。-1表示支出,1表示收入。 50 | data.insert(8, '逻辑1', -1, allow_duplicates=True) # 插入列,默认值为-1 51 | for index in range(len(data.iloc[:, 2])): # 遍历第3列的值,判断为收入,则改'逻辑1'为1 52 | if data.iloc[index, 2] == '收入': 53 | data.iloc[index, 8] = 1 54 | # 逻辑2,取值0 or 1。1表示计入,0表示不计入。 55 | data.insert(9, '逻辑2', 1, allow_duplicates=True) # 插入列,默认值为1 56 | for index in range(len(data.iloc[:, 3])): # 遍历第4列的值,判断为资金流动,则改'逻辑2'为0 57 | col3 = data.iloc[index, 3] 58 | if (col3 == '提现已到账') or (col3 == '已全额退款') or (col3 == '已退款') or (col3 == '退款成功') or (col3 == '还款成功') or ( 59 | col3 == '交易关闭'): 60 | data.iloc[index, 9] = 0 61 | # 月份 62 | data.insert(1, '月份', 0, allow_duplicates=True) # 插入列,默认值为0 63 | for index in range(len(data.iloc[:, 0])): 64 | time = data.iloc[index, 0] 65 | data.iloc[index, 1] = time.month # 访问月份属性的值,赋给这月份列 66 | # 乘后金额 67 | data.insert(11, '乘后金额', 0, allow_duplicates=True) # 插入列,默认值为0 68 | for index in range(len(data.iloc[:, 8])): 69 | money = data.iloc[index, 8] * data.iloc[index, 9] * data.iloc[index, 10] 70 | data.iloc[index, 11] = money 71 | return data 72 | 73 | 74 | if __name__ == '__main__': 75 | 76 | # 路径设置 77 | print('提示:请在弹窗中选择要导入的【微信】账单文件\n') 78 | path_wx = tkinter.filedialog.askopenfilename(title='选择要导入的微信账单:', filetypes=[('所有文件', '.*'), ('csv文件', '.csv')]) 79 | if path_wx == '': # 判断是否只导入了微信或支付宝账单中的一个 80 | cancel_wx = 1 81 | else: 82 | cancel_wx = 0 83 | 84 | print('提示:请在弹窗中选择要导入的【支付宝】账单文件\n') 85 | path_zfb = tkinter.filedialog.askopenfilename(title='选择要导入的支付宝账单:', filetypes=[('所有文件', '.*'), ('csv文件', '.csv')]) 86 | if path_zfb == '': # 判断是否只导入了微信或支付宝账单中的一个 87 | cancel_zfb = 1 88 | else: 89 | cancel_zfb = 0 90 | 91 | while cancel_zfb == 1 and cancel_wx == 1: 92 | print('\n您没有选择任何一个账单!') 93 | exit(1) 94 | 95 | path_account = tkinter.filedialog.askopenfilename(title='选择要导出的目标账本表格:', 96 | filetypes=[('所有文件', '.*'), ('Excel表格', '.xlsx')]) 97 | while path_account == '': # 判断是否选择了账本 98 | print('\n年轻人,不选账本怎么记账?') 99 | exit(1) 100 | 101 | path_write = path_account 102 | 103 | # 判断是否只导入了微信或支付宝账单中的一个 104 | if cancel_wx == 1: 105 | data_wx = pd.DataFrame() 106 | else: 107 | data_wx = read_data_wx(path_wx) # 读数据 108 | if cancel_zfb == 1: 109 | data_zfb = pd.DataFrame() 110 | else: 111 | data_zfb = read_data_zfb(path_zfb) # 读数据 112 | 113 | data_merge = pd.concat([data_wx, data_zfb], axis=0) # 上下拼接合并表格 114 | data_merge = add_cols(data_merge) # 新增 逻辑、月份、乘后金额 3列 115 | print("已自动计算乘后金额和交易月份,已合并数据") 116 | merge_list = data_merge.values.tolist() # 格式转换,DataFrame->List 117 | 118 | workbook = openpyxl.load_workbook(path_account) # openpyxl读取账本文件 119 | sheet = workbook['明细'] 120 | maxrow = sheet.max_row # 获取最大行 121 | print('\n「明细」 sheet 页已有 ' + str(maxrow) + ' 行数据,将在末尾写入数据') 122 | for row in merge_list: 123 | sheet.append(row) # openpyxl写文件 124 | 125 | # 在最后1行写上导入时间,作为分割线 126 | now = datetime.datetime.now() 127 | now = '👆导入时间:' + str(now.strftime('%Y-%m-%d %H:%M:%S')) 128 | break_lines = [now, '-', '-', '-', '-', '-', '-', '-', '-', '-', '-', '-'] 129 | sheet.append(break_lines) 130 | 131 | workbook.save(path_write) # 保存 132 | print("\n成功将数据写入到 " + path_write) 133 | print("\n运行成功!write successfully!") 134 | exit(1) 135 | -------------------------------------------------------------------------------- /AddTag.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | @author: Zeng LH 4 | @contact: 893843891@qq.com 5 | @software: pycharm 6 | @file: AddTag.py 7 | @time: 2021/2/13 0013 11:56 8 | @desc: 作者的版本只能一个一个手动添加,还是希望在python里面输入,稍微方便一点 9 | """ 10 | import sys 11 | import openpyxl 12 | import pandas as pd 13 | import tkinter.filedialog 14 | 15 | 16 | def find_tag(input_tag, tags): 17 | alpha = input_tag[:1] 18 | digit = input_tag[1:] 19 | row = 0 # 写死,只能是第二行 20 | column = ord(alpha) - ord('a') + 1 21 | # print(tags.iloc[row, column]) 22 | # print(tags.iloc[row + int(digit) + 1, column]) 23 | return tags.iloc[row, column], tags.iloc[row + int(digit) + 1, column] 24 | 25 | 26 | def read_data(path, auto=False): 27 | data = pd.read_excel(path, sheet_name="明细", engine='openpyxl', keep_default_na=False) 28 | tags = pd.read_excel(path, sheet_name="消费类型2.0", engine='openpyxl', keep_default_na=False) 29 | if auto: # 自动处理 30 | for row in data.index.values: 31 | if data.iloc[row, 12] == '' or data.iloc[row, 13] == '': 32 | a = data.iloc[row, 6] 33 | # b = data.iloc[row, 7] 34 | # price = data.iloc[row, 8] 35 | if "哈啰" in a or "滴滴出行" in a: 36 | data.iloc[row, 12], data.iloc[row, 13] = find_tag("h2", tags) 37 | elif "西南交通大学" in a or "饿了么" in a or "西南科技大学" in a: 38 | data.iloc[row, 12], data.iloc[row, 13] = find_tag("a1", tags) 39 | elif "龙泉山鲜果园" in a: 40 | data.iloc[row, 12], data.iloc[row, 13] = find_tag("a4", tags) 41 | elif "红旗连锁" in a or "友宝" in a: 42 | data.iloc[row, 12], data.iloc[row, 13] = find_tag("a5", tags) 43 | elif "成都金控数据服务有限公司" in a or "绵州通" in a or "地铁" in a: 44 | data.iloc[row, 12], data.iloc[row, 13] = find_tag("h1", tags) 45 | elif "中国铁路" in a: 46 | data.iloc[row, 12], data.iloc[row, 13] = find_tag("h3", tags) 47 | elif "舞东风" in a: 48 | data.iloc[row, 12], data.iloc[row, 13] = find_tag("a5", tags) 49 | elif "天然气" in a or "中国联通" in a or "中国移动" in a or "话费" in a: 50 | data.iloc[row, 12], data.iloc[row, 13] = find_tag("d2", tags) 51 | elif "dengyueyue" in a: 52 | data.iloc[row, 12], data.iloc[row, 13] = find_tag("g8", tags) 53 | print("正在保存!") 54 | pd.DataFrame(data).to_excel("output.xlsx", sheet_name='明细', index=False, header=True) 55 | print("处理完毕!") 56 | sys.exit(0) 57 | 58 | else: 59 | try: 60 | with open("progress", "r", encoding="utf-8") as f: 61 | start = int(f.read()) 62 | start_with_progress = input("是否从之前进度 %s 开始?(Y/N)" % start).lower() 63 | if start_with_progress != 'y': 64 | start = 0 65 | except: 66 | start = 0 67 | temp = dict({}) 68 | # 先遍历之前数据获取tag 69 | for row in range(0, start): 70 | a = data.iloc[row, 6] 71 | b = data.iloc[row, 7] 72 | if a != '' and b != '': 73 | if a not in temp: 74 | temp[a] = dict({}) 75 | temp[a][b] = [data.iloc[row, 12], data.iloc[row, 13]] 76 | elif b not in temp[a]: 77 | temp[a][b] = [data.iloc[row, 12], data.iloc[row, 13]] 78 | # 再从上次的地方继续 79 | for row in range(start, data.shape[0]): 80 | a = data.iloc[row, 6] 81 | b = data.iloc[row, 7] 82 | if data.iloc[row, 12] == '' or data.iloc[row, 13] == '': 83 | print("\n第%s行数据,平台:%s,商品:%s,金额:%s元" % (row, a, b, data.iloc[row, 11])) 84 | try: 85 | while True: 86 | text = "请输入标签,输入exit或者按下CTRL+C退出: " 87 | skip = True 88 | tag = "" 89 | if a in temp and b in temp[a]: 90 | tag = temp[a][b] 91 | text = "曾经有过相同数据,是否写入?回车写入%s:" % tag 92 | skip = False 93 | 94 | input_tag = input(text).lower() # 只支持第一个为字母,后面跟着数字的形式。 95 | if input_tag == '': 96 | if skip: 97 | print("跳过数据") 98 | else: 99 | data.iloc[row, 12], data.iloc[row, 13] = tag 100 | break 101 | elif input_tag == 'exit': 102 | save_and_close(data, row) 103 | elif input_tag[:1].isalpha() and input_tag[1:].isdigit(): 104 | first, second = find_tag(input_tag, tags) 105 | if first == '' or second == '': 106 | print("输入有误,请重新输入") 107 | continue 108 | data.iloc[row, 12], data.iloc[row, 13] = first, second 109 | temp[a] = dict({}) 110 | temp[a][b] = [first, second] 111 | print(first, second + "已写入") 112 | break 113 | else: 114 | print("输入错误,请重新输入") 115 | except KeyboardInterrupt: 116 | save_and_close(data, row) 117 | else: 118 | if a not in temp: 119 | temp[a] = dict({}) 120 | temp[a][b] = [data.iloc[row, 12], data.iloc[row, 13]] 121 | elif b not in temp[a]: 122 | temp[a][b] = [data.iloc[row, 12], data.iloc[row, 13]] 123 | 124 | 125 | def save_and_close(data, row): 126 | print("正在保存文件,请稍候") 127 | pd.DataFrame(data).to_excel("output.xlsx", sheet_name='明细', index=False, header=True) 128 | with open("progress", "w", encoding="utf-8") as f: 129 | f.write(str(row)) 130 | print("再见!") 131 | sys.exit(0) 132 | 133 | 134 | if __name__ == '__main__': 135 | # 路径设置 136 | print('提示:请在弹窗中选择要标记的源文件\n') 137 | origin_file_path = tkinter.filedialog.askopenfilename(title='选择要处理的源数据文件:', 138 | filetypes=[('所有文件', '.*'), ('Excel表格', '.xlsx')]) 139 | if origin_file_path != '': 140 | read_data(origin_file_path, False) 141 | --------------------------------------------------------------------------------