跳到主要内容

自动化文件处理

· 阅读需 7 分钟

汝毋手动修改 30 个文件。
——陈思达

问题开始于我把微信公众平台的文章迁移到这里。把所有文本复制黏贴,再重新排版不算很难,但到了图片环节,事情就不简单了。比如,可以看看这篇文章。(它的本地化版本在这里)。

我想要嵌入第一张图片。一开始,为了节约带宽和 GH pages 服务器的存储空间(我想要对服务器尽可能地好一点,但后来我发现 200 张照片也只占了半个 G 的空间),我计划直接引用 URL,就像这样:

/docs/science/cavalieri.md
export const Figure = ({children, src}) => (
<div style={{textAlign: 'center'}}>
<img src={src} />
<p style={{color: 'gray', fontSize: 'small'}}>{children}</p>
</div>);

...

<Figure src="https://mmbiz.qpic.cn/mmbiz_png/JGibibkelET68EfhySWuOboVia7FJX8ehwIAicTz2be2JDN7HIibwibjrpYPP1bTCr1TrjDicauU0P6BLCgFIibZK42GCQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></Figure>

但结果是这样的:

Oops

看起来微信的图片服务器不喜欢外部用户来偷它的资源。几番努力后,我还是决定在本地存储所有图片。

这时候,我已经把所有图片作为 <Figure> 标签插入在文章中了。剩余的工作包括:

  1. 按照 URL 下载所有图片,再把它们放在对应的文件夹中(虽然所有东西都是由脚本管理的,但我还是想要保留一些结构化的东西);
  2. 把每个图片的引用改为本地 URL。

我想到的第一个东西就是 bash 脚本。可惜我的 bash 知识仅限于调用命令行工具,比如 yarn 或者 python——我不会条件判断,不会循环,也不会定义变量。所以每写一行,我都得去 Stack Overflow 上查 5 分钟。(对 Stack Overflow 和所有创作者们表示一万个感谢!)

下载图片

核心模块可能就一行:

wget --output-document="correct/path/file.png" 'https://mmbiz.qpic.cn/mmbiz_png/JGibibkelET68EfhySWuOboVia7FJX8ehwIAicTz2be2JDN7HIibwibjrpYPP1bTCr1TrjDicauU0P6BLCgFIibZK42GCQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1'

所以有两个问题:

  1. 我如何生成 correct/path/file.png
  2. 我如何定位所有的 URL?

用伪代码写出逻辑:

FILE[] files = enumerateFilesUnderPath("./docs/")
for file in files:
imagePath = "./static/img/" + removeExtension(file.path)
makeDirectory(imagePath)
string[] links = file.findInFile(/"(?<=<Figure src=\").*?(?=\">)"/g)
for link in links:
imageName = makeSomeMeaningfulName(link)
downloadImage(link, imageName, imagePath)

首先,我需要列举 /docs 文件夹中的所有文件。这可以用 find 命令完成。结果被存储在一个列表中。

doc_list=( $(find ./docs -mindepth 2) )

接下来,我们遍历 doc_list,它包含了每个文件的路径。语法很奇怪,长这样:${doc_list[@]},而不是直觉所告诉我们的 ${doc_list}(也就是引用 doc_list 变量本身),而只要用过 JS 或者 Python 的 for-each 循环,就更容易想到后者。

doc_list=( $(find ./docs -mindepth 2) )
for doc in "${doc_list[@]}"
do
# TODO
done

现在有了 doc 的路径,我们需要在 static 文件夹下生成对应的资源文件夹。doc 路径大概长这样:docs/Science/cavalieri.md。为了把扩展名 .md 修掉,我们要用 "${doc%.*} 这个语法,再在前面添加路径 ./static/img/,就得到了正确的路径 ./static/img/docs/Science/cavalieri,把所有图片放在里面。创建文件夹用的命令是 mkdir

doc_list=( $(find ./docs -mindepth 2) )
for doc in "${doc_list[@]}"
do
mkdir -p "./static/img/""${doc%.*}"
# TODO
done

运行一遍这个脚本,我们就能得到正确的文件树,但没有任何内容。

接下来,我们要从文件中提取出 URL。要用正则在文件中搜索,可以用命令 grep。所有的 URL 都被包含在这样的格式中:<Figure src="...">,所以最自然的做法就是用正则前瞻和后瞻。不幸的是,苹果系统中的 grep 不支持 Perl,所以为了用 -p 选项,我需要安装 grep,它提供了 GNU 风格的 ggrep。现在我们就能抓取出所有的链接了。

doc_list=( $(find ./docs -mindepth 2) )
for doc in "${doc_list[@]}"
do
mkdir -p "./static/img/""${doc%.*}"
links=( $(ggrep -o -P "(?<=<Figure src=\").*?(?=\">)" "$doc") )
# TODO
done

为了进一步把每个图片的标识符(就是那串 Base-64 字符串)以及扩展名提取出来,我们要对每个字符串跑 grep

doc_list=( $(find ./docs -mindepth 2) )
for doc in "${doc_list[@]}"
do
mkdir -p "./static/img/""${doc%.*}"
links=( $(ggrep -o -P "(?<=<Figure src=\").*?(?=\">)" "$doc") )
for link in "${links[@]}"
do
name=$(echo "$link" | ggrep -o -P "(?<=(jpg|png)/).*(?=/640)")
ext=$(echo "$link" | ggrep -o -P "(?<=wx_fmt=).*")
# Almost there!
done
done

最后,就只剩 wget 了,它会按照链接下载图片,再保存到给定的路径,包括文件夹、图片名(Base-64 标识符)、图片扩展名。

doc_list=( $(find ./docs -mindepth 2) )
for doc in "${doc_list[@]}"
do
mkdir -p "./static/img/""${doc%.*}"
links=( $(ggrep -o -P "(?<=<Figure src=\").*?(?=\">)" "$doc") )
for link in "${links[@]}"
do
name=$(echo "$link" | ggrep -o -P "(?<=(jpg|png)/).*(?=/640)")
ext=$(echo "$link" | ggrep -o -P "(?<=wx_fmt=).*")
wget --output-document="./static/img/""${doc%.*}""/"$name"."$ext "$link"
done
done

就是这样!运行它,然后看输出喷涌而出吧。

更改 URL

在我们下载图片之后,需要把引用更改到本地 URL。比如,

/docs/science/cavalieri.md
<Figure src="https://mmbiz.qpic.cn/mmbiz_png/JGibibkelET68EfhySWuOboVia7FJX8ehwIAicTz2be2JDN7HIibwibjrpYPP1bTCr1TrjDicauU0P6BLCgFIibZK42GCQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></Figure>

就会变成

/docs/science/cavalieri.md
<Figure src="/zh-Hans/img/docs/Science/cavalieri/JGibibkelET68EfhySWuOboVia7FJX8ehwIAicTz2be2JDN7HIibwibjrpYPP1bTCr1TrjDicauU0P6BLCgFIibZK42GCQ.png"></Figure>

在上文介绍过这么多指令之后,这个任务就不难了。修改文本文件使用的指令是 sed。因为我已经写累了,这一部分就留作练习。你可以作弊,在这里找到代码。