惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

罗磊的独立博客
U
Unit 42
N
Netflix TechBlog - Medium
人人都是产品经理
人人都是产品经理
Hugging Face - Blog
Hugging Face - Blog
腾讯CDC
小众软件
小众软件
V
Visual Studio Blog
T
Tailwind CSS Blog
Engineering at Meta
Engineering at Meta
博客园 - 叶小钗
GbyAI
GbyAI
爱范儿
爱范儿
雷峰网
雷峰网
Microsoft Azure Blog
Microsoft Azure Blog
D
DataBreaches.Net
博客园_首页
D
Docker
A
About on SuperTechFans
G
Google Developers Blog
I
InfoQ
T
The Blog of Author Tim Ferriss
V
V2EX
博客园 - Franky

他说

只因一次笔误:一次笔误让我误以为他是女生,好感竟成了困局 表哥家在装修:Win10 金彩色系统,搭配 Minecraft 光线追踪 服务器的长途跋涉:我的世界服务器探索,飞到世界尽头建领地 一场作弊的高考:高考前重返学校,发现同学都在作弊改答案 后来如何了:辍学以后天天在家打游戏,母亲在超市笑着失落 那个外星女友的故事:我在平流层遇险,外星女友却失联两个月 论网恋的危害性:网恋女友身亡又复活,变声骗走老哥巨额钱财 知乎邀请函后续:面对知乎批量邀请,独立博客作者如何选择 被知乎的运营邀请:收到知乎官方邀请,我却选择坚守独立博客 博客宇宙后续:AI 把博客标题解析成两个域名,凌晨修复友链 貌似网站名字错了:博客宇宙友链出错,生造域名站名不显示 还有翻案环节:Dash to Dock 翻译之争,到看开源社区草台班子 吃药缓解一点了:感冒自己买药扛过来,想起当年学校中考跑步 如果有公共功能就好了:想约人打游戏很尴尬,应该有公告功能 暑假娱乐好无聊:信息茧房塌了,博客圈也全是 SEO 垃圾文 有时候运气真够透的:被抖音信息茧房反噬,过时技术推荐真烦 那篇文章的评论者:投影钟博文未发布,陌生访客已摸进局域网 大家要注意身体啊:楼下雪糕让我身体掏空,夏季食品安全需警惕 服务器开荒第三期:我的世界服务器遭遇强盗,黑暗森林生存法则 我和好朋友讨论龙骑:梦回校园重温特摄友情,却终究只是一场梦 记参加文字园活动:一篇有瑕疵的 Git 文章,入选文字园让我心虚 再审视 Windows 设计:从 Win11 的广告沼泽,逃向 UNIX 的清净世界 张雪和张雪峰的一字之差:张雪峰去世背后,就业选择与教育反思 被 A 社吓哭了:Claude Code 隐私检测,让我果断卸载并回归开源 服务器开荒第二期:给邻居送告示牌,扩建地下室发现蜘蛛刷怪笼 朋友邀请我玩服务器:我的世界服务器开荒,冰原遇险草原中安家 楼上装修真是烦死了:被楼上装修噪音追着跑,躲进音乐也逃不掉 为什么我喜欢巴巴爸爸:从漫画到动画,粉色棉花糖的奇遇记 他遇到的伤心事:诚哥的时空穿越奇遇,秘密与校园午后秘密誓言 批量拉黑 B 站评论区账号:评论区太乱?这个脚本帮你批量拉黑
微信账单同步日历:使用 Python 脚本将支付记录导出到苹果日历
梁栋烨 · 2026-07-01 · via 他说

最近做了一个小脚本,能把微信支付记录的 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

"""
微信支付账单 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))


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("]", "")



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()

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