Apple 数据整理套件

把 Mac 上苹果自带应用里的数据批量读出来,整理成能查、能对账、能存档的报告 —— 日历、提醒、备忘、通讯录、照片、短信、邮件、Safari、通话在内共 16 类数据源。读取一律零写入,删除永远由你亲手确认。

macOS 本机运行 只读优先 数据不出本机 需要「完全磁盘访问权限」
套件本体是命令行工具,但它的「导出件解析」那一半不需要读你的本机数据库 —— 所以本页把这半边搬进了浏览器:页面下半部分是真能跑的解析器, 把健康 export.zip、照片 .csv、Numbers 导出的 .csv 拖进去就出结果。 文件不上传,解析全在你这台设备里完成。没有导出件也能点「用示例数据看看」。

1 · 它替你解决什么

「上次和这个人联系是什么时候?」——三个 App 都答不上来 通话记录在电话里,短信在信息里,iMessage 又是另一套。每个 App 只能往下翻、不能按人检索,更不会把三条线拼到一起。套件的 touchpoints 把通讯录、通话、短信/iMessage 按同一个人合并成一条时间线,支持按姓名、单位、号码模糊查,按日期区间截取。
「信息 App 里翻不到两年前那条消息」——因为一半以上的正文根本不在你以为的地方 新版 macOS 把消息正文写进 attributedBody 这个二进制字段,不再写 text 列。任何只查 text 的工具会静默漏掉绝大部分历史(本机实测漏约 87%,且越老的年份漏得越干净)。套件自带解码器,把这部分正文完整还原出来再入库。
「日历散在好几本里,想合并又怕把订阅日历搞坏」 先清点每本日历可写不可写、各有多少事件,导出 .ics 全量备份,生成合并计划让你过目,批准后才执行,执行完再跑一次核对。订阅日历、节假日、生日日历一律只读,不参与合并。
「照片上万张,想分类、想找出含敏感信息的截图、想清重复」 先出库画像,再生成分类计划;文字识别走 macOS 本机的 Vision 引擎(不联网、不送云端),把含身份证号、银行卡号一类内容的照片标出来;重复项导出成清单供你核对。要真删,是你在照片 App 里手动按 ⌘⌫ —— 脚本只负责打标签、进待删相册。
「健康 App 攒了几年数据,想看长期趋势,可它只肯给你看最近一小段」 健康 App 能导出 export.zip,里面是一份几百 MB 的 export.xml。这份文件谁都能拿到,可谁都打不开 —— 双击它文本编辑器会卡死。套件流式解析它、按月聚合、出图。这部分正是本页下半部分在浏览器里跑的东西。

2 · 能拿到什么 —— 16 个数据域

套件按 16 个顶层数据域组织;其中日历、提醒、备忘、通讯录同属一个 PIM 域,下表把它们拆开列,所以行数比 16 多。表头可点击排序。「读写」列写的是这个域实际具备的能力:只读= 代码里没有任何写回路径;可写= 有写入/删除动作,但一律走「计划 → 你审 → 执行」三段式。

数据源你能拿到什么读写状态
日历日历清点(含可写/只读判定)· 全量 .ics 备份 · 合并计划与执行 · 合并后核对 · 导出 .ics · 导入 Google 日历可写在用
提醒事项列表清点 · JSON 全量备份 · 规范化计划 · 列表合并 · 清理 · 校验 · 标记过期项可写在用
备忘录只读快照导出为 Markdown / HTML · 分类清单 · 双向对照(导出↔回读)· 笔记间引用关系图只读在用
通讯录只读导出 · vCard 备份 · 重号/重邮聚类候选 · 分类归桶 · 清理报告;合并与删除是单独的、需显式授权的动作只读为主在用
照片(iCloud 图库)库画像 · 元数据备份 · 分类计划/执行 · 本机 OCR 敏感信息标记 · OCR 文字抽取 · 共享图库清单 · 重复项导出 · 对账 · 抽样分诊 · 标题批量生成计划/执行可写标签在用
短信 / iMessage全历史只读(含 attributedBody 正文还原)· 按联系人与月份聚合 · 分类归桶 · 生成 HTML 报告只读在用
通话记录电话 + FaceTime 全量只读(号码自动打码)· 按联系人 / 国家 / 服务 / 呼入呼出聚合只读在用
Safari浏览历史 + 书签只读导出 · 按域名聚合 · 分类归桶 · HTML 报告只读在用
邮件(Mail.app)本机索引只读:发件人 / 主题 / 时间 · 分类归桶 · 清理计划与批量删除执行(需显式确认)只读 + 受控删除视客户端而定
语音备忘录录音清单(标题、时长、时间)+ 音频文件清单只读保留
图书(Books)书库清单 + 高亮与笔记导出只读保留
音乐曲目与播放列表元数据(走 AppleScript 通路)只读保留
播客订阅列表 + 单集收听进度只读保留
地图收藏地点与搜索历史只读保留
便签(Stickies)便签内容转纯文本摘要只读保留
查找(Find My)设备与物品的本机位置缓存清单只读保留
屏幕使用时间App 使用时长记录(系统保护较严,可能读不到,此时如实返回「跳过」而不是编一份空数据)只读保留
iCloud 云盘文件清单:路径 / 大小 / 修改时间,绝不读文件内容;未下载的占位文件自动排除只读保留
健康解析你从 iPhone 健康 App 导出的 export.zip(流式解析,几百 MB 也不爆内存)离线解析需你先导出 · 本页可试
「保留」= 代码完整可跑、路径与字段陷阱都已实测修正,但这些数据源本身在多数人机器上料很少(比如便签只有两三张、播客只有几个订阅),所以不再持续投入。留着的价值在于:macOS 各版本把这些私有库藏在哪、字段单位是什么,这份知识本身很贵,删掉就得重查一遍。

3 · 哪一半搬得进浏览器,哪一半搬不进

套件的大半读的是你这台 Mac 上的本地数据库 —— 信息、通话记录、日历、照片图库这些文件躺在你自己的硬盘里,服务器上根本不存在这些数据。这一半做不成网页:要读本机数据,程序就得在本机跑;要让它在浏览器里跑,就得先把你的短信、通话、照片全部上传到别人的服务器 —— 那恰恰是这个套件从设计上就拒绝做的事。

但另一半可以。健康 App 的 export.zip、照片 App 导出的 .csv、Numbers 导出的 .csv —— 这些是你手上已经有的文件,解析它们不需要任何系统权限,只需要一个能解 ZIP、能读 XML/CSV 的程序。浏览器这两样都会:DecompressionStream('deflate-raw') 是标准 API,ZIP 的中央目录格式二十行代码读得完。

所以下面这个解析器不是演示动画,是真跑:你拖进去的文件被读进内存、解压、解析、聚合、画图,全程没有一次网络请求(本页没有任何上传接口,也没有引用任何外部脚本或字体)。关掉页面,什么都不剩。

4 · 在浏览器里跑一次:导出件解析器

支持三类投喂:① iPhone 健康 App 的 export.zip(内含 apple_health_export/export.xml)② 直接给 export.xml ③ 任意 .csv(照片导出清单 / Numbers 导出 / 其它表格,自动识别分隔符与 Numbers 的表名前导行)。

把文件拖到这里,或点击选择
.zip · .xml · .csv · .txt —— 不上传,只在本机内存里解析

5 · 命令行怎么用(套件本体)

全部在项目目录内运行。依赖用 uv 统一管理,一个虚拟环境跑全套:

uv sync                      # 建立/更新统一虚拟环境

跨源往来时间线

python3 touchpoints.py who 王              # 查人:姓名 / 单位 / 号码任一含关键词
python3 touchpoints.py timeline 张教练      # 出这个人的通话+短信合并时间线
python3 touchpoints.py timeline 10000000000 --since 2026-01-01 --until 2026-06-30
python3 touchpoints.py timeline 某单位 --full     # 展开短信正文(含他人内容,慎用)
python3 touchpoints.py stats                # 三个数据源的覆盖度概况
python3 touchpoints.py export               # 全量 JSON 到标准输出,给渲染器用

这个脚本零落盘 —— 只往标准输出打印,不写任何文件。正文默认只给前 40 个字符。

日历与提醒

cd pim
../.venv/bin/python apple_cli.py cal-inventory           # 清点:各日历事件数 / 可写性
../.venv/bin/python apple_cli.py cal-backup              # 全量备份为 .ics
../.venv/bin/python apple_cli.py cal-merge-plan          # 生成合并计划(不改任何数据)
../.venv/bin/python apple_cli.py cal-merge-apply         # 批准后执行
../.venv/bin/python apple_cli.py cal-verify-merge        # 核对合并结果

../.venv/bin/python apple_cli.py rem-inventory           # 提醒列表清点
../.venv/bin/python apple_cli.py rem-backup              # JSON 全量备份
../.venv/bin/python apple_cli.py rem-cleanup             # 清理
../.venv/bin/python apple_cli.py rem-verify              # 校验

照片

.venv/bin/photocli audit            # 库画像(只读)
.venv/bin/photocli classify-plan    # 生成分类计划
.venv/bin/photocli classify-apply   # 批准后执行
.venv/bin/photocli ocr-scan         # 本机 OCR 扫描,标记含敏感信息的照片
.venv/bin/photocli dedup-export     # 导出重复项清单
.venv/bin/photocli reconcile        # 与上一轮结果对账

只读导出与报告

python3 messages/dump.py            # 短信/iMessage 抽样验证读取通路
python3 safari/dump.py              # Safari 历史 + 书签
python3 callhistory/dump.py         # 通话记录(号码已打码)
python3 _imessage_body.py           # 正文解码器自检(拿真实数据当基准逐条比对)

.venv/bin/python build_index.py     # 渲染套件总索引 HTML
第一次跑之前,需要在「系统设置 → 隐私与安全性 → 完全磁盘访问权限」里给终端(或你运行 Python 的那个程序)授权,否则读不到苹果应用的私有数据库。脚本读不到时会分诊报告:是路径变了,还是权限不够 —— 不会含糊地报个失败了事。

6 · 技术上怎么做的

零写入的快照读法。苹果这些私有数据库多数带 WAL 日志,而「看起来只读」的三种常见写法都有坑:immutable=1 语义上直接跳过 WAL,会静默少读最近若干天的记录(通话记录上实测漏过整整四天,图书库更是直接读成空的);裸 mode=ro 连接和 .backup() / VACUUM INTO 虽然数据全,但只读连接要映射共享内存文件写读标记 —— 实测源库的 -shm 文件内容字节被改了。套件的做法是把数据库和它的 WAL 一起复制到临时目录,只打开副本,源库全程只被 read()。判断「到底有没有写」一律比对 sha256,不比修改时间:上述几种写法改了内容,修改时间却纹丝不动(内存映射的脏页不会立刻回写 mtime),用 mtime 当判据得到的「没写入」是假绿灯。这套读法收敛成套件内唯一一份实现,十来个脚本共用,禁止各域自己再手搓一遍。

正文解码与自检。iMessage 正文存在 attributedBody 里,是 NeXTSTEP typedstream 归档格式,得手工按变长长度前缀解出 UTF-8 载荷。正确性怎么保证?数据库里恰好有一批消息 textattributedBody 同时存在 —— 那就是现成的基准真值,逐条比对全等才算通过,改动解码器必须重跑这个自检。时间戳口径同样是雷区:苹果 Core Data 用的是 2001 年起算的纪元,新版存纳秒旧版存秒(按阈值判),而邮件索引用的是标准 UNIX 秒 —— 混用就是几十年的误差。所有对外输出的时间一律带 UTC 偏移量,因为同一台机器上系统时区和 shell 环境变量可能不一致,不带偏移量的时间串差 15 小时也看不出来。本页的解析器沿用同一条纪律:归月一律取导出件里那串本地日期的 YYYY-MM 前缀,不做时区平移 —— 你在哪个时区打开这个页面,看到的月份都一样。

跨源合并的关键是号码归一。同一个人在三个库里长相完全不同:通讯录存 +86 100 0000 0000,通话记录存 +8610000000000,短信句柄存 10000000000。归一规则是去掉所有非数字、去掉国家码、取末 11 位作为连接键(中国手机号正好 11 位,带区号的座机也落在 11 位内)。不能用更宽松的「末 9 位」之类,那会把不同的人并成一个。另外通讯录在 macOS 上通常有多个数据源文件,只读第一个会漏掉大部分联系人,必须全读再合并。还有个反直觉的细节:判断一通电话有没有接通要看时长,不能看「已接听」字段 —— 那个字段问的是「你接没接」,你主动打出去的时候它恒为 0,拿它判会把所有打通了的去电全标成未接通。

试一下号码归一

下面是归一函数的等价实现,跑在你的浏览器里。左边这几个是合成的示例号码(不是真实号码),它们代表同一个人在三个库里的不同写法 —— 归一后应该得到同一个连接键。可以自己改。

读私有数据库的部分只用 Python 标准库,没有第三方依赖;照片域的文字识别调 macOS 本机的 Vision 引擎;日历和提醒走系统原生的 EventKit 框架而不是 AppleScript 猜。本页的解析器同样零依赖:ZIP 中央目录手工解析 + 浏览器原生 DecompressionStream,SVG 手写,一行第三方代码都没有。

7 · 安全边界

只读优先
16 个数据域里 13 个是纯只读,代码路径上不存在写回。有写入能力的只有三个:PIM(日历合并、提醒规范化)、照片(打关键词、进相册)、邮件(受控批量删除)。备忘和通讯录虽在 PIM 域内,本身也是只读的。
三段式
任何会改数据的操作都是「先出计划 → 你审 → 才执行」,默认空跑。
删除靠人
照片的最后一锤必须你在照片 App 里手动按 ⌘⌫;脚本只打标签、进待删相册。邮件和通讯录的批量删除是需要单独授权的动作,不在日常流程里。
先备份
合并日历前先导出全量 .ics,改提醒前先出 JSON 备份。
不外发
短信正文、邮件、通话号码、照片这类内容落在本地忽略目录里,不进版本库、不上传、不送云端模型。号码在输出时打码,短信正文默认截断。
本页同理
你拖进上面解析器的文件不上传:没有 fetch、没有表单 action、没有任何外部脚本或字体。解析结果只存在于当前标签页的内存里,刷新即清空;「导出 CSV」是浏览器本地生成的文件,也不经过任何服务器。
失败要响
读不到数据源时按原因分诊(路径变了 / 权限不够)并以非零码退出,不静默返回空结果 —— 因为「输出非空」不等于「输出正确」,反过来「输出为空」也不该被当成「没数据」。上面的解析器同样:ZIP 头不对、压缩方式不支持、XML 里一条 Record 都没有,都会明说是哪一种,而不是给你一张空表。
本页所有示例数据(示例健康导出、示例照片清单、号码归一里的号码、人名「张教练 / 李同学」)全部为合成数据,与任何真实的人或设备无关。 真实系统是一套在 macOS 本地运行的命令行工具,读取的数据全程不出本机、不进版本库。 本页为单文件静态页:无外部请求、无上传接口、无统计脚本;你投喂的文件只在本机内存中被解析。