最近做了一个小脚本,能把微信支付记录的 XLSX 导出到苹果日历,起因是我妈想要一个记账的东西。她不太会用复杂的软件,就想要个能一眼看清每天花了多少钱的工具。我以前是用一个本地部署的软件来管理账目的,但那个软件的界面实在太过丑陋,满屏都是花花绿绿的配色和生硬的排版,一股浓烈的AI生成味道,用起来毫无愉悦感。寻思着既然自己天天要用,不如索性重写一个,做成自己用得顺手的模样。一开始是完全没考虑过把账单放进日历里的,最初的设想很简单,就是直接导出一个 CSV 文件,方便自己在 Excel 里做分类统计和月度汇总一类的工作。

但后来实际用下来发现 CSV 的效果并不理想,主要是每次查看都得专门打开表格软件,还得手动按日期排序筛选,操作起来略显繁琐。等等,说到了日期,我忽然想起来那些老一辈人,都是习惯直接在纸质日历上写字的——今天买菜花了多少钱、明天人情往来出了多少份子,密密麻麻地记在日历格子里,翻看起来特别直观。于是我就想,能不能在电子日历上也实现类似的效果呢?苹果的 ICS 文件格式支持你预定一个提醒事项,能够自动放到系统自带的日历里面,每天打开手机就能看到当天的收支摘要,非常方便。

我还考虑到,如果全部信息(包括店名、收了多少钱、支付了多少钱)都一股脑儿导进去的话,那阅读起来还是太过辛苦,信息过载反而失去了快速浏览的意义。所以我特意设计成了仅导入支出和收入这两部分核心数据,精简掉冗余细节,只保留每天的总账目。以下是实现这一功能的完整代码,整体逻辑清晰,包含文件读取、数据解析、ICS 生成和命令行交互等模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#!/usr/bin/env python3
"""
微信支付账单 XLSX → Apple ICS 日历转换脚本
将每天的收支汇总为日历事件,导入到 macOS/iOS 日历中显示。
"""

import argparse
import datetime
import uuid
from collections import defaultdict
from pathlib import Path

try:
import openpyxl
except ImportError:
print("缺少 openpyxl 库,请运行: pip install openpyxl")
exit(1)


def parse_xlsx(path: str) -> str:
"""读取 XLSX 文件,返回 (昵称, 收入总笔数/金额, 支出总笔数/金额, 按日期汇总的收支记录)"""
wb = openpyxl.load_workbook(path, read_only=True, data_only=True)
ws = wb.active

rows = list(ws.iter_rows(values_only=True))

# 解析表头信息(前 18 行)
nick_name = None
for r in rows[:6]:
if r[0] and isinstance(r[0], str) and r[0].startswith("微信昵称"):
nick_name = r[0].replace("微信昵称:", "").replace("[", "").replace("]", "")

# 第 18 行是列标题(0-indexed = 17)
# 列: 交易时间, 交易类型, 交易对方, 商品, 收/支, 金额(元), ...
header_row = rows[17]
col_idx = {
"time": 0,
"type": 1,
"income_expense": 4,
"amount": 5,
}

daily: dict[str, dict] = defaultdict(lambda: {"income": 0.0, "expense": 0.0, "count_income": 0, "count_expense": 0})

for row in rows[18:]:
dt = row[col_idx["time"]]
io = row[col_idx["income_expense"]]
amt = row[col_idx["amount"]]

if dt is None:
continue
if not isinstance(dt, datetime.datetime):
continue

# 只取日期
date_str = dt.strftime("%Y-%m-%d")

if io and isinstance(io, str):
try:
amount = float(amt) if amt is not None else 0.0
except (ValueError, TypeError):
amount = 0.0

if io == "收入":
daily[date_str]["income"] += amount
daily[date_str]["count_income"] += 1
elif io == "支出":
daily[date_str]["expense"] += amount
daily[date_str]["count_expense"] += 1
# 中性交易跳过

wb.close()
return nick_name, daily


def escape_ics(text: str) -> str:
"""转义 ICS 文本字段的特殊字符"""
text = text.replace("\\", "\\\\")
text = text.replace(";", "\\;")
text = text.replace(",", "\\,")
text = text.replace("\n", "\\n")
return text


def build_ics(nick_name: str, daily: dict) -> str:
"""生成 ICS 日历内容。每天一个全天事件,标题包含当天收支汇总。"""
now = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8)))
now_utc = now.astimezone(datetime.timezone.utc)
timestamp = now_utc.strftime("%Y%m%dT%H%M%SZ")

lines = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//xlsx2ics//微信账单日历//CN",
"CALSCALE:GREGORIAN",
"METHOD:PUBLISH",
"X-WR-CALNAME:支出",
f"X-WR-CALDESC:{escape_ics(nick_name or '我的')}的微信支付每日收支。",
]

if daily:
first_date = min(daily.keys())
last_date = max(daily.keys())
lines.append(f"X-WR-TIMEZONE:Asia/Shanghai")

for date_str in sorted(daily.keys()):
d = daily[date_str]
income = d["income"]
expense = d["expense"]
net = income - expense

# 构建事件标题
parts = []
if income > 0:
parts.append(f"收入 ¥{income:.2f}")
if expense > 0:
parts.append(f"支出 ¥{expense:.2f}")
if income > 0 and expense > 0:
if net >= 0:
parts.append(f"净收入 ¥{net:.2f}")
else:
parts.append(f"净支出 ¥{abs(net):.2f}")
summary = " | ".join(parts)
if not summary:
continue

# 日内详情
detail_lines = [
f"日期: {date_str}",
]
if income > 0:
detail_lines.append(f"收入: ¥{income:.2f} ({d['count_income']}笔)")
if expense > 0:
detail_lines.append(f"支出: ¥{expense:.2f} ({d['count_expense']}笔)")
detail = "\\n".join(detail_lines)

uid = str(uuid.uuid4())
date_compact = date_str.replace("-", "")

lines.extend([
"BEGIN:VEVENT",
f"UID:{uid}",
f"DTSTART;VALUE=DATE:{date_compact}",
f"DTEND;VALUE=DATE:{date_compact}",
f"SUMMARY:{escape_ics(summary)}",
f"DESCRIPTION:{escape_ics(detail)}",
f"DTSTAMP:{timestamp}",
"TRANSP:TRANSPARENT",
"END:VEVENT",
])

lines.append("END:VCALENDAR")
return "\r\n".join(lines) + "\r\n"


def main():
parser = argparse.ArgumentParser(description="微信支付 XLSX 账单 → ICS 日历")
parser.add_argument("xlsx", help="微信支付账单 XLSX 文件路径")
parser.add_argument("-o", "--output", help="输出 ICS 文件路径(默认: 输入文件名.ics)")
args = parser.parse_args()

xlsx_path = Path(args.xlsx)
if not xlsx_path.exists():
print(f"❌ 文件不存在: {xlsx_path}")
exit(1)

print(f"📖 读取: {xlsx_path}")
nick_name, daily = parse_xlsx(str(xlsx_path))

if not daily:
print("❌ 未找到有效的交易记录")
exit(1)

total_income = sum(v["income"] for v in daily.values())
total_expense = sum(v["expense"] for v in daily.values())
total_net = total_income - total_expense
print(f"👤 昵称: {nick_name or '未知'}")
print(f"📅 日期范围: {min(daily.keys())} ~ {max(daily.keys())} ({len(daily)}天)")
print(f"💰 总收入: ¥{total_income:.2f}")
print(f"💸 总支出: ¥{total_expense:.2f}")
print(f"📊 净收支: ¥{total_net:.2f}")

ics_content = build_ics(nick_name, daily)

output_path = args.output or xlsx_path.with_suffix(".ics")
with open(output_path, "w", encoding="utf-8") as f:
f.write(ics_content)

print(f"✅ 已导出: {output_path}")
print(f" {len(daily)} 个日历事件")
print()
print("📌 双击 .ics 文件即可导入到 macOS/iOS 日历。")


if __name__ == "__main__":
main()

这个是项目地址,如果你对这个玩具感兴趣,可以下载来玩一下。项目后续我打算继续维护,因为我正在用着,每天导出账单、双击导入日历已经成为我日常记账的习惯流程了。我觉得它本身已经很完美了,因为它真的很简单,整个设计理念就是极简实用——没有花哨的图表分析,没有复杂的账目分类,就像是你在纸质日历上写字那样纯粹直接,打开日历就能看到每天的收入支出概况,一目了然,干净利落。