读懂 ChatGPT 数据导出:对话是一棵树,不是一个列表
向 ChatGPT 申请你的数据,你会拿到一个 .zip,其中真正有用的只有一份 conversations.json。以为打开会看到消息列表,结果看到的是别的东西:一个叫 mapping 的对象,里面全是以标识符为键的条目,每一条指向一个父节点和一组子节点。消息确实在里面。你记忆中的那段对话,是图里的一条路径。
这个页面把那条路径还原出来,并写成 Markdown。把压缩包或解压出来的 JSON 拖进来,解析在这个浏览器标签页里完成。
为什么它被存成图
因为界面允许你改写历史。编辑一条早先的提问,线程就从那里分叉。重新生成一次回答,你就得到一个可以用小箭头翻页的备选。两个版本都还在,界面只是显示其中一个。
扁平数组表达不了这个。所以导出把每一条分支都存下来,而你真正看到的那份文字稿,要从没有父节点的那个节点出发、一路往下走,每一步在子节点之间做选择。与界面一致的约定是跟着最后一个子节点走,因为重新生成的回答会追加在它所替代的那条之后。这个工具就是这么做的,所以输出读起来是你当时那段对话,而不是所有尝试交织在一起。
遍历时有一个环路防护。在格式良好的导出上它永远不该触发,但解析器陷入死循环比截断文字稿更糟。
你从未看到过的内容
导出里包含界面隐藏起来的材料,把它们照搬出来,你得到的文字稿和你对这段对话的记忆对不上:
- 系统消息。每段对话都以一段你没写过、也没看见过的指令开场。它在文件里是一条正经的消息。
- 工具流量。网页搜索、代码执行、图像生成各自留下自己的请求节点和结果节点,收件方不是用户。这些是一次回答背后的机械装置,不是对话的一部分。
- 被显式隐藏的节点。有些消息带着「不在对话中显示」的标记。导出在这一点上是诚实的,这个标记会被尊重。
三者都会被过滤掉,剩下用户和助手的轮次。
消息内容并不总是字符串
content 字段这些年一直在长。纯文本的一轮带着一个字符串数组 parts。有些消息类型改用 text 字段。多模态的轮次把对象放进 parts 数组,一个元素一项,上传的图片和随之而来的问题是两个独立条目。代码解释器的结果又是另一种形状。
解析器读字符串形式、字符串数组形式和对象数组形式,从每一种里把文本取出来。非文本的部分不贡献任何东西,既不产生占位符也不报错,也就是说一段带图片的对话会以「图片周围的那些话」的形式出来。
为什么 Markdown 才是对的目标
助手本来就在写 Markdown。它的回答里有围栏代码块、标题、项目符号和表格,这些标记在导出里就是一个个字符。转成纯文本,它们会原地留下来,变成字面上的反引号和井号——格式既没有被渲染,也没有被去掉,就那么搁浅在那里。
保留 Markdown,意味着代码块还是代码块。把结果粘进任何笔记应用、静态站点生成器、文档工具或 wiki,它就会正常渲染。说话人名字加粗,每段对话按它的标题生成一个标题,于是一份跨年的导出变成一份可以导航的文档,而不是一片没有区分的文字。
导出文件很大
常用两三年下来,conversations.json 会有几十甚至几百兆字节,装着几千段对话。这对文本编辑器是个麻烦——它会试图给整份文件做语法高亮——对任何「先美化再显示」的工具也是。
这里只解析一次,在内存里完成,只渲染可读的输出。每段对话在自己的标题下成为一节,节与节之间有分隔,于是你可以直接在结果里搜一句半记得的话,而不是靠滚动。
怎么申请导出
在 ChatGPT 里:设置,然后数据管理,再点导出数据。压缩包会发到你的邮箱,通常几分钟内到,是一个会过期的下载链接。这个 zip 里还有一个 HTML 查看器和各种媒体文件;这里只需要 JSON,直接把整个压缩包拖进来也完全没问题。
没有任何东西被上传
你的 ChatGPT 历史是一份关于你在做什么、在担心什么、在向谁求助的详细记录。对大多数人来说,它是自己拥有的最能暴露自己的文件之一。解析跑在这个标签页里的 JavaScript 中,没有上传这一步,所以没有服务器收到它,事后也没有东西需要删除。
Claude 的导出有自己的页面:Claude 导出转 Markdown;通用聊天记录导出页则处理所有已支持的平台。