把 Mac 上苹果自带应用里的数据批量读出来,整理成能查、能对账、能存档的报告 —— 日历、提醒、备忘、通讯录、照片、短信、邮件、Safari、通话在内共 16 类数据源。读取一律零写入,删除永远由你亲手确认。
touchpoints 把通讯录、通话、短信/iMessage 按同一个人合并成一条时间线,支持按姓名、单位、号码模糊查,按日期区间截取。
attributedBody 这个二进制字段,不再写 text 列。任何只查 text 的工具会静默漏掉绝大部分历史(本机实测漏约 87%,且越老的年份漏得越干净)。套件自带解码器,把这部分正文完整还原出来再入库。
.ics 全量备份,生成合并计划让你过目,批准后才执行,执行完再跑一次核对。订阅日历、节假日、生日日历一律只读,不参与合并。
export.zip,里面是一份几百 MB 的 export.xml。这份文件谁都能拿到,可谁都打不开 —— 双击它文本编辑器会卡死。套件流式解析它、按月聚合、出图。这部分正是本页下半部分在浏览器里跑的东西。
| 数据源 | 你能拿到什么 | 读写 | 状态 |
|---|---|---|---|
| 日历 | 日历清点(含可写/只读判定)· 全量 .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 也不爆内存) | 离线解析 | 需你先导出 · 本页可试 |
套件的大半读的是你这台 Mac 上的本地数据库 —— 信息、通话记录、日历、照片图库这些文件躺在你自己的硬盘里,服务器上根本不存在这些数据。这一半做不成网页:要读本机数据,程序就得在本机跑;要让它在浏览器里跑,就得先把你的短信、通话、照片全部上传到别人的服务器 —— 那恰恰是这个套件从设计上就拒绝做的事。
但另一半可以。健康 App 的 export.zip、照片 App 导出的 .csv、Numbers 导出的 .csv —— 这些是你手上已经有的文件,解析它们不需要任何系统权限,只需要一个能解 ZIP、能读 XML/CSV 的程序。浏览器这两样都会:DecompressionStream('deflate-raw') 是标准 API,ZIP 的中央目录格式二十行代码读得完。
所以下面这个解析器不是演示动画,是真跑:你拖进去的文件被读进内存、解压、解析、聚合、画图,全程没有一次网络请求(本页没有任何上传接口,也没有引用任何外部脚本或字体)。关掉页面,什么都不剩。
| 路径 | 原始大小 | 压缩后 | 压缩方式 |
|---|
| 字段 | 非空 | 不同取值 | 类型 | 示例值 |
|---|
全部在项目目录内运行。依赖用 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 到标准输出,给渲染器用
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
零写入的快照读法。苹果这些私有数据库多数带 WAL 日志,而「看起来只读」的三种常见写法都有坑:immutable=1 语义上直接跳过 WAL,会静默少读最近若干天的记录(通话记录上实测漏过整整四天,图书库更是直接读成空的);裸 mode=ro 连接和 .backup() / VACUUM INTO 虽然数据全,但只读连接要映射共享内存文件写读标记 —— 实测源库的 -shm 文件内容字节被改了。套件的做法是把数据库和它的 WAL 一起复制到临时目录,只打开副本,源库全程只被 read()。判断「到底有没有写」一律比对 sha256,不比修改时间:上述几种写法改了内容,修改时间却纹丝不动(内存映射的脏页不会立刻回写 mtime),用 mtime 当判据得到的「没写入」是假绿灯。这套读法收敛成套件内唯一一份实现,十来个脚本共用,禁止各域自己再手搓一遍。
正文解码与自检。iMessage 正文存在 attributedBody 里,是 NeXTSTEP typedstream 归档格式,得手工按变长长度前缀解出 UTF-8 载荷。正确性怎么保证?数据库里恰好有一批消息 text 和 attributedBody 同时存在 —— 那就是现成的基准真值,逐条比对全等才算通过,改动解码器必须重跑这个自检。时间戳口径同样是雷区:苹果 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 手写,一行第三方代码都没有。
.ics,改提醒前先出 JSON 备份。fetch、没有表单 action、没有任何外部脚本或字体。解析结果只存在于当前标签页的内存里,刷新即清空;「导出 CSV」是浏览器本地生成的文件,也不经过任何服务器。Record 都没有,都会明说是哪一种,而不是给你一张空表。