最近我把在 Notion 上写了大半年的一整套学习笔记导出成 Markdown,打算归档到本地,再挑一部分发到个人博客。导出当下一切正常,可等我把 .md 传上博客、又过了一阵子回头看,图片却陆续开始裂:有的刚发就裂,有的放了一段时间才变空白。这篇把我排查和迁移的完整过程记下来,核心是先搞懂 Notion 导出的图片到底以什么形式存在,再决定怎么搬,跟具体用哪个工具关系不大。
一、现象:图片不是一起裂,而是分两批
- 第一批,传上博客当天就裂:本地编辑器里明明能看到图,一部署全是占位符。
- 第二批,刚导出时正常,过段时间陆续变空白。
同样是“裂图”,触发时机不同,根因也完全不同,得分别看。
二、Notion 导出的图片,到底是什么
在页面右上角 ••• → Export,格式选 Markdown & CSV,会得到一个 zip。解压后图片只有两种存在形式。
| 形式 | 在 .md 里的样子 | 问题 |
|---|---|---|
| 本地文件 + 相对路径 | 、 | 图片只在导出文件夹里;只把 .md 拷走、或传到博客,相对路径就找不到文件 |
| 远程签名 URL | https://prod-files-secure.s3.us-west-2.amazonaws.com/...?X-Amz-Algorithm=...&X-Amz-Date=...&X-Amz-Expires=... | 带签名参数的临时授权地址,不是为长期外链设计的;签名时效一过、或换个环境,就可能打不开 |
两个关键点:
- 相对路径那批,图就在你解压出来的文件夹里,只是 .md 与图片的相对位置一旦被打破就裂——这解释了“本地能看、传上去就裂”。
- 签名 URL 那批,图不在本地、在 Notion 的对象存储上,链接里带着
X-Amz-Date、X-Amz-Expires等参数,本质是一张“限时门票”——这解释了“刚导出正常、过段时间才裂”。
三、三步确认你手里是哪种
- 解压 zip 看目录:有没有一堆
image.png、image 1.png。有,就说明至少一部分是本地相对路径。 - 打开 .md 看图片链接开头:是
http(s)://,还是文件名 / 相对路径。 - 对 http 开头的,复制一条在终端测一下:
curl -I "粘贴一条图片链接"
返回 200 只能说明此刻有效;重点看 URL 里有没有 X-Amz-Expires 这类签名参数——有,就别把它当长期地址。
我自己这批是“混合”状态:一部分图被打进了本地文件夹,另一部分仍是远程签名链接,所以两批现象都撞上了。
四、迁移思路与取舍
| 方案 | 怎么做 | 适合 |
|---|---|---|
| 图片留本地、随笔记走 | 保留完整导出文件夹,用 Obsidian 等按相对路径管理 | 只在本机 / 同步盘内归档,不外发 |
| 手动逐张另存再替换 | 一张张截图、上传、改链接 | 三五篇短文 |
| 本地图片批量上传 + 脚本替换 | 多选上传本地 image.png,拿到映射后正则替换 | 图都在本地、要发博客 |
| 远程 URL 批量抓取转存 + 替换 | 把签名 URL 收集成列表,交给服务端逐条抓取,拿回新链接 | 图全是签名链接、数量多 |
一旦要外发到博客、公众号或长期归档,目标都一样:把图片从“相对路径 / 限时签名地址”,搬到一个你能控制的稳定外链上,再统一替换 .md 里的链接。
五、我固定下来的迁移流程
- 解压、固定目录:把整个导出文件夹解压到一个工作目录,别急着只拎出 .md。如果笔记很多,建议先挑一篇结构完整、图片类型齐全的跑通整条流程,确认无误再批量,避免一次性把几十篇都替换错。
- 抽出全部图片链接并分类:
import re, pathlib
md = pathlib.Path("note.md").read_text(encoding="utf-8")
imgs = re.findall(r'!\[.*?\]\((.*?)\)', md)
remote = [u for u in imgs if u.startswith("http")]
local = [u for u in imgs if not u.startswith("http")]
print("远程:", len(remote), "本地:", len(local))
- 分别转存:远程那批签名 URL,我没有一张张手动存,而是把列表交给图床小镇的 URL 批量上传,由服务端逐条抓取转存,拿回一组新链接;本地那堆 image.png,则在网页端直接多选、拖拽上传,一次性传完。
- 处理同名坑:Notion 导出很容易出现一堆都叫 image.png 的图。这里要特别小心——如果上传后按“同名覆盖”处理,后传的会把前传的顶掉。我选的存放方式是多级目录加随机路径,每张图各占独立路径,同名也互不影响。反过来,“同名覆盖、链接不变”更适合以后某张图要反复改版的场景,而不是迁移这种重名场景。顺带一提,导出文件名里还常带空格(在链接里表现为
%20)甚至中文,个别博客平台对这类 URL 编码处理不一致,迁移时我会顺手让最终链接都落在英文与随机路径上,省得后面再踩编码的坑。 - 按映射统一替换:准备好“旧链接 → 新链接”的对照表,批量替换并写回:
mapping = {
"image.png": "https://你的图床/aaa/image.png",
"https://prod-files.../x.png?X-Amz-...": "https://你的图床/bbb/x.png",
}
for old, new in mapping.items():
md = md.replace(f"({old})", f"({new})")
pathlib.Path("note.md").write_text(md, encoding="utf-8")
远程签名 URL 很长且含特殊参数,建议直接用“整条原始链接”做 key,不要截断。 6. 全新环境验证:用无痕窗口打开最终页面,确认所有图片都是新链接、能正常加载。
迁完之后,这些图就和 Notion 的签名时效、导出文件夹的相对位置彻底解耦了,.md 发到哪都能显示。至于迁到博客后图片该怎么按目录组织,我之前在一篇静态博客图片资产的整理里写过,这里不重复:juejin.cn/post/768898… 。
六、迁移自检清单
- 解压后确认图片是本地相对路径、远程签名 URL,还是两者混合
- 远程链接检查过是否带
X-Amz-Expires等签名参数 - 同名 image.png 用了独立目录 / 随机路径,没有被互相覆盖
- .md 里所有图片链接都替换成了新的稳定外链
- 在无痕窗口、全新环境回读,图片全部正常
- 原始导出文件夹先留存,确认无误后再清理
写在最后
整个过程真正花时间的不是上传,而是先分清图片以什么形式存在,再批量处理重名和替换。我自己这批图最后统一转存、托管在我在用的图床上,图省事而已,同类能做批量转存和外链的服务都可以,关键是把图片链接的控制权拿回自己手里。官网:imgbed.cn 。