
这个工具解决什么问题
换日历、把公司日历并进手机、或者干脆想把几年攒下来的日程归拢成一份存档,办的都是同一件事:从不同地方导出 .ics,再想办法合到一起。麻烦在合完那一步——同一场会往往在两份文件里各有一条记录,甚至更多:手机日历导一次、公司 Exchange 导一次、同事发来的邀请又是一份。日历软件看到它们,只要有两条记录内容一样就各显示一条,于是日程列表里「周例会」「客户拜访」都成双成对地出现,一屏翻下去全是重复。手动清理的代价很实在:几百条日程里挑出重复的,只能靠眼睛一条条比时间、比标题,删错了还可能把真正的日程一起删掉。更隐蔽的是同一场会改过时间的情况——旧的那条还躺在另一份文件里,导入后你会同时看到 14:00 和 14:30 两个版本,到底哪个是最终安排,得回头翻聊天记录。
- 原来的做法:把 .ics 一份份导入日历软件,然后在日程列表里一条条比对、手动删重复;或者干脆只留一份、放弃另一份里独有的日程。重复的会改过时间时,还得凭记忆判断哪条是新的。
- 用这个工具:把几份 .ics 一起拖进窗口,点一次「开始合并」,程序按「UID + 内容指纹」两级判重,同一场会只留一条,改过时间的那种自动保留 SEQUENCE 更新的版本;结果表里原文照列,灰字是重复项、橙字是被新版替换的旧版本,导出一份 .ics 就能直接导入手机。
- 省下来的:三份演示日历(含 8 条个人日程、10 条工作日程、5 条节假日)一共 23 条,一轮合并去掉了 4 条重复,保留 19 条,整个过程不到一秒。换成手工比对,这几百次「看时间、看标题、判断是不是同一场」的工作量,是半小时起步的活。
技术实现
这个工具的技术难点不在界面,而在「怎么把 .ics 读懂并且不读错」。.ics 是纯文本,但它是 RFC 5545 规定的格式,有三层容易被忽略的规则:单行超过 75 字节必须折断、续行以空格开头;属性名后面可以带参数,参数值可以用引号包住并包含冒号分号;属性值里的换行、逗号、分号、反斜杠都是转义写法。任何一层处理错,都会得到「能打开但内容全乱」的结果——这是最坏的一种失败,因为用户看不出来。所以本工具没有用「按行 split 冒号」这种省事写法,而是老老实实写了一个解析器:先展开折行,再按「引号外」的规则找名值分隔点,解析成组件树,最后把事件原样重新拼回去。零第三方依赖是刻意的选择:整个工具只用标准库,编译出来的 exe 不需要用户装任何运行库,也不会因为某个第三方包的版本变化突然坏掉。
- 折行按「字节」而不是按「字符」:RFC 5545 要求每条物理行不超过 75 个八位组(字节),续行以一个空格开头。中文在 UTF-8 里是 3 字节,于是「按字符数折到 75」对中文等于折到 225 字节——看似正常,实际已经违规,严格的客户端会拒绝解析。反过来的坑更致命:如果按字节硬切,折点很容易落在一个汉字的中间(UTF-8 的后续字节以 0b10 开头),切开后两半都解不出原字符。本工具的做法是先 encode 成字节,切完再从切点往回退,跳过所有属于「后续字节」的位置,保证折点一定落在字符边界上;自测里对折行结果逐行断言「单独解码不出现替换字符」。
- 两级去重:先认 UID,再认内容:同一份导出被加了两遍,UID 是一样的,按 UID 判重就够了。但现实里同一个会议在手机日历和公司 Exchange 里是两条独立记录,UID 完全不同——只按 UID 永远认不出它们是一回事。所以第二级用「内容指纹」:开始时间相同 + 标题相同(去掉所有空白、不分大小写)+ 地点不冲突 + 时长相差不超过 30 分钟。后两条是防止误伤:同一时刻两个不同部门各开一个都叫「周会」的会,地点不同就不该合并;把 30 分钟的站会和 3 小时的评审合并更是灾难。界面上给了三档强度,想保守就只比 UID。
- 同一场会的新旧两版:按 SEQUENCE 决定留谁:会议改时间是最常见的场景:原定 14:00,后来改成 14:30 并换了会议室。两份导出里都留着记录,如果只是「先到的留下、后到的丢掉」,很可能留下的正是作废的那条。iCalendar 用 SEQUENCE 表达修订次数——每改一次加一。本工具在判为重复之后,再比一次 (SEQUENCE, LAST-MODIFIED/DTSTAMP):更新的那版胜出,旧的那条标成橙色「已被新版替换」。演示数据里就放了这样一对:14:00 的初版与 14:30 的修订版,合并后只留 14:30 那条。
- 不做字段级改写,属性原样搬:合并工具最容易犯的错是「顺手规范化」:把 RRULE 展开成一条条实例、把 TZID 换成 UTC、把 DESCRIPTION 重新排版。这些改写每一条都是信息损失,而且用户根本不知道自己的日程被改过。本工具只做一个判断——这条事件要不要留;留下来的事件连同它的 RRULE、VALARM(提醒)、X- 私有扩展、ATTENDEE 一起原样重新拼回去,属性顺序都不动。这样导入后,手机上的提醒时间、重复规则、参与人跟原来完全一致。
- 时区定义按 TZID 去重,不猜偏移:每份 .ics 都会自带一份 VTIMEZONE,把两三份合起来就会出现同名的时区定义重复。去重时以 TZID 为键只留第一份;万一某份文件没写 TZID(不合规但确实存在),就退化成用整块内容当键,宁可多留一份也不把两份不同的定义错当一份。这里刻意**不做时区换算**:Windows 上的 Python 默认不带 tzdata,按 Asia/Shanghai 这类名字换算会直接抛异常,为了这个让用户装包不值得。比对用的是「墙上时间」,同一台机器导出的日历标记方式通常一致,够用且不会算错。
- 后台线程 + 队列:界面不假死:读文件、解析、比对都放在工作线程里跑,线程只往 queue.Queue 里放进度和结果,主线程每 80 毫秒取一次并刷新控件。tkinter 不是线程安全的,从工作线程里直接调 after 或改控件,在解释器没进入 mainloop 时会抛 RuntimeError,进入了也只是碰运气——这个坑在同类工具上是通病。文件读不出来(被占用、不是文本)时按文件隔离:坏掉的那份记一条提示,其余照常合并。
技术栈:Python 3.8+、tkinter 窗体、标准库零依赖、RFC 5545 解析、纯本地离线
已知局限:不做时区换算:不同时区写法(比如一份写 Asia/Shanghai 本地时间、另一份写 UTC 带 Z)之间不会被认成同一条,也不会互相换算,输出保持原样。不展开 RRULE 实例——重复日程按「整条规则」参与去重,不会变成一条条具体的会。只处理 VEVENT,VTODO(待办)与 VJOURNAL 原样搬过去但不参与去重。不支持 vCalendar 1.0(老的 .vcs)与 CSV 格式,需要先转成 .ics。单文件超过几万条日程时解析会明显变慢,此时请分批合并。
同类软件对照
下面几款都是公开可查的同类软件,定位各不相同。这里只说明各自的定位,以及本工具在其中的位置,方便你判断哪一类更契合自己的用法。
| 软件 | 它的定位 | 本工具的定位 |
|---|---|---|
| Google 日历(导入功能) | 把 .ics 文件并进 Google 账号里的某个日历,之后在网页或 App 里统一看 | 本工具的定位是「导入之前先把几份文件合成一份干净的」,产出的仍是一个本地 .ics 文件,交给哪个日历软件都可以 |
| Microsoft Outlook(导入/导出向导) | 把 .ics 导入 Outlook 的日历文件夹,或把 Outlook 日历导出成 .ics | 本工具不绑定任何客户端,只处理文件本身;去重发生在导入之前,重复项不会先进到账号里再手工清理 |
| Thunderbird(日历组件) | 可以同时订阅、显示多个本地或远程日历,多个日历并排显示在一起 | 本工具把多个来源收成一个文件,重点是「同一场会只留一条」,而不是并排展示多个日历 |
| 在线日历合并类网站 | 把文件上传到网页,在服务器端合并后下载结果 | 本工具全程在本机运行,日程文件不出电脑;对含客户姓名、会议地点、家庭安排的日历更合适 |
以上仅作定位对照,各软件的可用性、授权方式与功能范围请以其官方页面为准。
操作教程
- 把 .ics 文件加进来
点「添加文件…」可多选,点「添加文件夹…」会把该目录(含子文件夹)里所有 .ics 一次性收进来。也可以把文件或文件夹直接拖到程序图标上打开。重复的路径会自动忽略。
预期结果:左侧列表按加入顺序出现文件名,日程列暂时显示「—」,状态是「待合并」。 - 看每个文件能读出多少条
这一步不是必须的,但很有用:如果某个文件显示「解析失败」,说明它不是有效的 .ics(常见于改了扩展名的文本文件,或者网页另存下来的 HTML)。
预期结果:每个文件后面的日程列填上真实条数,比如 8、10、5。 - 选去重强度
默认「同 UID + 内容指纹」——先用 UID 认同一份导出里的重复,再用内容认出不同软件生成的同一条日程。只想绝对保守(绝不误合并任何两条)就选「只比对 UID」;两份日历来自不同软件、UID 肯定不一样时,选「只比对内容」。
预期结果:下拉框即时生效,下一次合并按新策略执行。 - 填日历名称,决定要不要合并时区
日历名称会写进 X-WR-CALNAME,多数日历软件导入后拿它当这个日历的名字显示,默认「合并后的日历」。下面两个开关:「合并重复时区」默认开(几份文件各自带的同名时区只留一份),「丢掉已取消的日程」默认关(勾上会把 STATUS:CANCELLED 的日程直接剔掉)。
预期结果:这两项只影响输出文件的内容,不影响源文件。 - 点「开始合并」
程序逐份读取、解析、比对,进度条按文件数推进。演示的三份日历在这台机器上不到一秒。
预期结果:右侧表格按开始时间列出全部日程:黑字「保留」,灰字「重复·同 UID」或「重复·内容相同」,橙色「已被新版替换」。左下表里每个文件标出重复几条。 - 读下面那行统计
统计栏是这次合并的结论,格式像「23 条 → 19 条保留 · 去掉 4 条重复(同 UID 1 / 内容相同 2 / 旧版本 1)· 时区 2 → 1 · 时间跨度 2026-10-21 ~ 2027-02-06」。
预期结果:三类重复各有多少条一目了然;数字对不上时,多半是选错了去重强度。 - 导出并导入手机
点「导出 .ics…」,选保存位置(默认用日历名称当文件名)。导出成功会弹窗告诉你文件路径、条数与体积。把这个文件用邮件、微信或网盘发到手机,点开即可导入系统日历。
预期结果:得到一个 .ics 文件;再次导入手机后,日程列表里不再成双成对。原文件一份都没有被改动。
常见问题
技术实现与后续维护
整个程序分三层:解析层(折行展开、属性解析、组件树)、判定层(事件抽取、内容指纹、版本比较)、输出层(按字节折行、拼回 VCALENDAR),界面层只负责收集文件与选项、把结果画到表格上。三层之间没有反向依赖,所以内核可以完全脱离界面测试——内联在工程里的自测有 127 项断言,全部不碰窗口。后续维护最需要盯两处:一是折行函数,任何改动都要跑一遍「中文长标题」用例,折点切在汉字中间是这里最容易复发的错误;二是内容指纹的四个条件,放宽任何一个之前先想清楚会不会把两场不同的会合成一条,那比漏掉重复更糟。
运行环境:
运行环境:Windows 10/11(64 位),无需安装 Python、无需任何运行库。程序已打包成单个可执行文件,解压后双击即可运行,不写注册表、不装服务、不需要联网授权。
可扩展方向:
- 加「按时间段筛选」:只合并某一年或某个月的日程,便于分年度归档
- 加「导出前预览差异」:把「保留下来的条目」与「被去掉的条目」导出成两份清单,便于核对
- 加「规则级去重」:把 RRULE 相同的重复日程识别为同一个系列,即使 DTSTART 被整体平移过
- 加「批量改提醒」:合并时统一调整 VALARM 的提前量,比如全部改成提前 15 分钟
- 加「按日历分别着色」:输出时给来自不同源文件的日程打上不同 CATEGORIES,导入后一眼看出出处
本工具免费提供使用,但不对外分发源码。如果你需要在本工具基础上做功能调整、对接内部系统或长期维护,可以联系我们做二次开发,源码与工程文档可在定制合作中一并交付。
需要其它功能?可以定制
这个工具只做一件事:手机、电脑、公司日历各导出一份 .ics,合到一起后同一场会在列表里冒出两三条,几百条日程只能一条条手工删。如果你的场景还需要别的处理,可以联系我们做二次开发,按你的实际数据格式和流程定制。
常见的定制需求:
- 公司内部部署:把 Exchange 导出的 .ics 与部门共享日历定期合并成一份,供行政统一下发
- 教育场景:把课程表 .ics 与校历合并,去掉重复的调课记录后再发给学生
- 把去重规则固定成公司自己的口径(比如「标题相同即视为同一条」),做成不给用户改的内部工具
- 加一个命令行可调用的版本,纳入已有的归档脚本(本工具本身保持图形界面)
- 联系方式:邮箱/微信见站内「联系我们」页面,或在本文下方留言
- 定制说明:请附上你的数据样例(脱敏即可)和期望结果,方便快速评估工作量
- 交付形式:定制项目可交付可执行程序,按需一并交付源码与工程文档,支持长期维护
使用说明与免责声明
- 本工具在本地运行,不联网、不上传任何数据到服务器。
- 涉及文件改写的功能,请在使用前备份原始文件。
- 软件完全免费,可自由使用与转发,请勿用于倒卖。
- 因误操作造成的数据问题请自行承担。

