自动化文件处理
汝毋手动修改 30 个文件。
——陈思达
问题开始于我把微信公众平台的文章迁移到这里。把所有文本复制黏贴,再重新排版不算很难,但到了图片环节,事情就不简单了。比如,可以看看这篇文章。(它的本地化版本在这里)。
我想要嵌入第一张图片。一开始,为了节约带宽和 GH pages 服务器的存储空间(我想要对服务器尽可能地好一点,但后来我发现 200 张照片也只占了半个 G 的空间),我计划直接引用 URL,就像这样:
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>
但结果是这样的:
看起来微信的图片服务器不喜欢外部用户来偷它的资源。几番努力后,我还是决定在本地存储所有图片。
这时候,我已经把所有图片作为 <Figure>
标签插入在文章中了。剩余的工作包括:
- 按照 URL 下载所有图片,再把它们放在对应的文件夹中(虽然所有东西都是由脚本管理的,但我还是想要保留一些结构化的东西);
- 把每个图片的引用改为本地 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'
所以有两个问题:
- 我如何生成
correct/path/file.png
? - 我如何定位所有的 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。比如,
<Figure src="https://mmbiz.qpic.cn/mmbiz_png/JGibibkelET68EfhySWuOboVia7FJX8ehwIAicTz2be2JDN7HIibwibjrpYPP1bTCr1TrjDicauU0P6BLCgFIibZK42GCQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></Figure>
就会变成
<Figure src="/zh-Hans/img/docs/Science/cavalieri/JGibibkelET68EfhySWuOboVia7FJX8ehwIAicTz2be2JDN7HIibwibjrpYPP1bTCr1TrjDicauU0P6BLCgFIibZK42GCQ.png"></Figure>
在上文介绍过这么多指令之后,这个任务就不难了。修改文本文件使用的指令是 sed
。因为我已经写累了,这一部分就留作练习。你可以作弊,在这里找到代码。