你用的图床关停了,于是你写过的每一篇文章里的每一张图,都成了失效链接。又或者,你有一个 Obsidian 库,所有截图都躺在 attachments/ 里,除了你自己的电脑,放到哪儿都显示不出来。
这两件事本质上是同一个问题:几百个 Markdown 文件,里面的图片链接指向了不该指向的地方,而且根本没法靠手工一个个改。
picgo-plugin-pic-migrater 可以替你搞定。它会读取你的 Markdown 文件,把每一张图片重新上传到 PicGo 当前配置的图床,然后改写链接。一个文件、一个目录、一整个库都可以。从 v1.4.0 开始,它还支持 Obsidian 的 ![[wikilink]] 嵌入。
简单版
假设你已经装好了 PicGo-Core,也配置好了新图床,只需要三条命令:
picgo install pic-migrater
picgo set plugin pic-migrater
picgo migrate ./posts/
最后一条命令会递归遍历 ./posts/,迁移其中所有 .md 文件。默认情况下原文件不会被改动——改写后的内容会写到旁边的 post_new.md 里,你可以先 diff 一下两个文件,再决定留哪个。
在 PicGo 桌面端里也能用。行为完全一样,只不过入口换成了插件上的 选择文件 / 选择文件夹 菜单。
它到底做了什么
这不是对文本做一次查找替换。对每个 Markdown 文件,插件会:
- 提取图片链接。 既支持 Markdown 语法(
),也支持原生 HTML(<img src="url" />,v1.3.0 起支持)。 - 获取每一张图片。 远程 URL 会下载到内存里;本地路径则直接从磁盘读取——绝对路径或相对于 Markdown 文件的相对路径都行,像
./my%20image.png这样经过 URL 编码的路径也会先解码再重试。 - 通过 PicGo 上传。 插件自己没有上传器。它把图片交给 PicGo,由 PicGo 发往你已经配置好的地方——PicGo Cloud、GitHub、Amazon S3、腾讯云 COS、七牛、又拍云、SM.MS,或者你安装的任何第三方上传插件。
- 改写链接,换成新的 URL,然后写出文件。
正因为第 3 步,你不需要去查什么「支持的目标图床列表」。只要 PicGo 能传过去,就能迁移过去。
这也意味着本地图片和远程图片的处理方式完全一样。在两个图床之间搬家是最常见的场景,但把本地附件传到图床上用的也是同一条命令——这正是它适合用来处理 Obsidian 库的原因。
先配置
第一次迁移之前,先运行 picgo set plugin pic-migrater,哪怕你全部使用默认值。没有配置的话,插件会拒绝运行:
You should configure this plugin first!
picgo set plugin pic-migrater
在桌面端里对应的是插件的 配置 按钮,提示会以通知的形式弹出,而不是一行日志。
一共有四个配置项。其中两个决定了这次迁移是顺顺利利,还是搞得一团糟。
| 配置项 | 作用 | 默认值 |
|---|---|---|
newFileSuffix | 生成文件的后缀 | _new |
include | 只迁移匹配它的链接 | 空 |
exclude | 跳过匹配它的链接 | 空 |
oldContentWriteToNewFile | 反转被覆盖的是哪个文件 | false |
用默认后缀的话,迁移 2019.md 会生成 2019_new.md。include 为空表示全部迁移;exclude 为空表示什么都不跳过。
include / exclude:只迁移挂掉的那个图床
这是大多数人最需要、却最找不到的配置。
真实的 Markdown 存档从来不会只用一个图床。有的图片在刚挂掉的图床上,有的在一个还好好的 CDN 上,还有 shields.io 的徽章,以及几张本地截图。你想要的是把挂掉的那部分搬走,其余的一概不动。
所以把 include 设成你要逃离的那个图床:
sinaimg.cn
这样只有包含 sinaimg.cn 的 URL 会被迁移。你的 shields.io 徽章原样保留,也不用把几百张本来就没坏的图片再传一遍。
exclude 则反过来,适合「列出要跳过的更方便」的情况。两个都设置的话,一条链接必须匹配 include 并且不匹配 exclude,才会被迁移。
有一点文档里说得不够清楚:这两个值都会被编译成正则表达式,而不是 glob 模式或者普通子串。这很好用——sinaimg\.cn|imgur\.com 一次就能迁移两个图床。但这也意味着 . 和 ? 带有正则含义,所以匹配普通域名时记得把点转义。
安全网:到底覆盖哪个文件
默认情况下,原文件保持不动,迁移后的内容写到一个新文件里:
post.md— 不变,仍然指向旧图床post_new.md— 新文件,新链接
你检查完之后再自己把两者对调。这样很安全,但对 Obsidian 库或 Hexo 站点来说就很别扭了:文件名就是 URL,一整个目录的 _new 文件显然不是你想要的。
oldContentWriteToNewFile(v1.3.0+)就是为此准备的。把它设为 true,两者的角色就对调了:
post.md— 被新链接覆盖,文件名不变post_new.md— 原文件的备份
无论哪种方式,最后都是这两个文件。唯一的区别是哪个文件保留原来的文件名。对于路径很重要的站点或笔记库,第二种布局才是你要的。
不管用哪种方式,后缀都不要留空。newFileSuffix 为空的话,生成的文件名就和源文件一模一样,最后你只剩一个文件:被覆盖掉的原文件,没有任何备份。
迁移 Obsidian 库
如果是往笔记里粘贴新截图,请看用 PicGo 在 Obsidian 里自动上传图片。下面的步骤针对的是库里已经存在的图片的迁移。
这是大家用这个插件最常见的原因,而且有一个前提条件,最好在开始之前就弄对。
笔记库和一个博客文章目录有两点不同。从 v1.4.0 开始,这两点都已经处理好了。
Wikilink 嵌入也会被迁移(v1.4.0+)
Obsidian 默认把图片嵌入写成 ![[Pasted image 20240101.png]]。这种语法没有圆括号,所以 1.3.3 及之前的版本会完全跳过它——迁移照常运行,报告的图片数比你库里实际的少,每个嵌入依旧指向本地文件。唯一的变通办法是先在 Obsidian 里关掉 使用 [[Wiki 链接]],但这对你已经写好的笔记毫无作用。
从 v1.4.0 开始,它们可以直接迁移。嵌入语法没法放远程 URL,所以会被改写成标准的 Markdown:
<!-- 迁移前 -->
![[Pasted image 20240101.png]]
![[diagram.png|300]]
<!-- 迁移后 -->


有三点值得了解:
- 只动图片。 Obsidian 用
![[...]]嵌入任何文件,笔记也包括在内。像![[Some note#heading]]这样的笔记嵌入,以及普通的[[Some note]]链接,都会原封不动。这点比听起来更重要——如果你的库里有讲 Obsidian 的笔记,这种语法会出现在正文和代码块里,把它改写掉就错了。 - 显示参数会被处理。
![[image.png|300]]和![[image.png|alt|300]]都会解析成image.png。尺寸设置会随着嵌入语法一起去掉。 - 短名称按 Obsidian 的方式解析。
![[image.png]]里没有路径——Obsidian 是通过搜索整个库来找到文件的。插件也一样:先尝试显式的相对路径,再搜索笔记所在的目录树。所以默认的attachments/布局开箱即用。
用 picgo install pic-migrater 升级(或者在桌面端插件列表里点 更新)。迁移笔记库之前,确认你用的是 1.4.0 或更新的版本。
使用相对路径
在 设置 → 文件与链接 中,把 内部链接类型 设为 基于当前笔记的相对路径。插件会以笔记所在的目录为基准解析相对图片路径,所以基于库根目录的路径,对子目录里的笔记全都会失效。
然后迁移整个库
本地附件和远程 URL 的处理方式完全一样,所以一条命令就能把满是 attachments/ 的库整个搬到图床上:
picgo migrate ~/Documents/MyVault/
对于笔记库,请先把 oldContentWriteToNewFile 设为 true。Obsidian 是按文件名来定位笔记的,库里到处是 note_new.md 会让你的内部链接全部失效;你希望迁移后的内容保留原文件名,让备份带上后缀。
运行之前要知道的四件事
这些来自实际行为,而不是 README。
先 commit。 迁移会改写磁盘上的文件,开启 oldContentWriteToNewFile: true 时改写的就是你的原文件。事先 git commit 一下,或者干脆把目录复制一份,任何意外都能一行命令撤销。
旧图片必须还能访问。 远程图片的迁移方式是下载再重新上传。如果旧图床已经彻底下线,或者用防盗链拦截了普通请求,那就没东西可下载,这些链接会保持原样。所以要在图床挂掉之前迁移,而不是之后。一旦某个服务宣布了关停日期,那就是你的窗口期。
每张图片单独占一行。 链接提取按每行一张图片来处理。如果在同一行里并排放两张 Markdown 图片,只有最后一张会被识别。大多数 Markdown 本来就是一行一张图,所以很少遇到这个问题,但如果你用了并排图片,最好检查一下。引用式链接(![alt][ref])同样不会被识别——必须写成行内链接。
最后看一眼统计数字。 运行结束时会输出一份汇总:
Success: 128 pics, Fail: 3 pics
对于老存档来说,有失败很正常:源图床上已经 404 的图片、几年前就挪过位置的路径。这些链接会继续指向原来的地方,所以情况不会变得更糟。这个数字只是告诉你需要抽查多少。
在 Node.js 里调用
如果要写脚本,比如跨多个仓库迁移,或者作为构建中的一个步骤,可以直接调用 API(v1.2.3+):
const { PicGo } = require('picgo')
const PluginMigrater = require('picgo-plugin-pic-migrater')
const picgo = new PicGo()
picgo.setConfig({
'picgo-plugin-pic-migrater': {
newFileSuffix: '_new',
include: '',
exclude: ''
}
})
const plugin = picgo.use(PluginMigrater)
const result = await plugin.migrateFiles(['/path/to/post.md'])
// { total: 12, success: 12 }
setConfig 也算作配置了插件,所以能通过「先配置」的检查。
即使你想让 include 和 exclude 为空,也要显式地写上。在 1.4.0 之前的版本里,如果完全不写这两个 key,过滤器会退回到一个什么都匹配不上的占位值,结果运行报告零张图片、一个文件都没改——看起来和「迁移了但什么都没找到」一模一样。这个问题已在 v1.4.0 修复,但显式传入仍然是更清晰的习惯。
版本要求
插件会调用 PicGo 的内部接口,所以版本需要对得上:
| 插件 | PicGo 桌面端 | PicGo-Core |
|---|---|---|
> 1.2.2 | 2.3.0+ | 1.5.0-alpha.1+ |
<= 1.2.2 | 2.0.2 ~ 2.2.0 | 1.4.0 ~ 1.5.0 |
当前版本是 1.4.0,所以只要是较新的 PicGo,都适用第一行。Obsidian 的 wikilink 嵌入则特别要求 1.4.0 或更新版本。
在较老的桌面端版本上,插件会一开始就告诉你,而不是迁移到一半才失败。
常见问题
问:我的图床能用吗?
答:只要 PicGo 能上传到它,就能用。插件自己不上传任何东西——它把图片交给 PicGo,由 PicGo 使用你配置好的图床,第三方上传插件也包括在内。
问:它会覆盖我原来的 Markdown 文件吗?
答:默认不会。它会用你配置的后缀写一个新文件(post_new.md),原文件保持不动。把 oldContentWriteToNewFile 设为 true 可以反过来——原文件名保存新链接,备份带上后缀。
问:能把本地图片迁移到图床吗?
答:能。相对路径和绝对路径的本地图片,处理方式和远程 URL 完全一样,所以一个满是本地附件的笔记目录,和一个满是失效外链的博客,用的是同一条命令。
问:Obsidian 能用吗?
答:能。请使用 v1.4.0 或更新版本,它可以直接迁移 Obsidian 默认的 ![[image.png]] 嵌入——更早的版本只识别标准的  链接,会悄无声息地跳过所有嵌入。在 设置 → 文件与链接 里把 内部链接类型 设为 基于当前笔记的相对路径,并开启 oldContentWriteToNewFile,让你的笔记保留原文件名。
问:它会改写我的 [[笔记链接]] 或笔记嵌入吗?
答:不会。只有带图片扩展名的目标才会被迁移,所以 ![[Some note#heading]] 和普通的 [[Some note]] 都会原封不动——包括这些语法出现在讲 Obsidian 本身的笔记正文或代码块里的情况。
问:能只迁移某一个图床上的图片吗?
答:这正是 include 的用途。把它设为那个图床的域名,文件里其他所有链接都不会被动。
问:它能处理 HTML 的 <img> 标签吗,还是只支持 Markdown 语法?
答:能,v1.3.0 起支持。 和 <img src="url" /> 都会被识别并改写。
问:迁移失败的图片会怎样?
答:它们的链接保持不变,所以不会比之前更糟。最后的汇总会报告成功和失败各多少张。
问:命令行和桌面端都能用吗?
答:都能。命令行里用 picgo migrate <files...>,桌面端里用插件上的 选择文件 / 选择文件夹 菜单。
PicList 用户可以在 npm 上找到这个插件的单独分支 picgo-plugin-pic-migrater-piclist。两个应用之间的插件兼容关系,可以看 PicGo 和 PicList 对比。
试试看
picgo install pic-migrater
picgo set plugin pic-migrater
picgo migrate ./posts/
插件基于 MIT 协议,在 GitHub 上开源。如果它漏掉了你文件里的某种链接写法,欢迎附上示例提个 issue。现在大部分提取规则就是这么来的,v1.4.0 的 wikilink 支持也是。
最初的想法来自 @Moyf 写的一个 Python 脚本。修复几百个失效图片链接的办法一直都是同一个——只是它不该再是每个人都要自己写一遍的脚本了。
迁移愉快!