博客搬迁踩坑指南:实现markdown图片的自动化替换
需求来源
Hexo博客难以长期维护
博客,是学习计算机科学的同学所必备的网站,好的博文能够体现一个人的能力和思考水平,一个长期维护的博客更是一个人是否拥有长期坚持品质的重要证明。毕竟,如果你能长期维护好一个博客,那么由你负责的项目大概率不会被你中途放弃。
说起博客,我自22年11月起搭建起了个人博客CagurZhan's Blog
(现改名AjaxZhan's Blog
),我原先使用的是Hexo进行博客的发布,然而,Hexo博客的最大头疼点就在于折腾,由于他是纯前端,导致文章的编辑、发布、上传都十分麻烦。
当时我不以为然,我认为一个计算机的同学最需要的就是动手能力,hexo的繁琐配置正好可以锻炼我的动手能力。然而事实证明,当你的博客规模日渐扩大,这些繁琐的事情会让你压根不想维护这个博客,导致我后面零零散散地将博文发布到CSDN和掘金。
CMS:将博客更换为Halo
鉴于上面的原因,我在近日将博客换成了Halo。然而,如何将博客的文章搬运到新系统就成为一个问题。之前我的博文主要写到了下面几个地方:
- 本地Obsidian:所有笔记都存于此,部分经过整理和润色后会发布。
- Hexo:原来大部分博客存放的地方。
- 掘金:主要是一些后端技术文章。
- CSDN:主要是一些学科复习笔记。
手工迁移博文十分麻烦,我们的时间应该花在更有价值的事情。上面的几种博客来源在迁移过程中分别存在一些问题:
- 本地Obsidian:图片格式一般是
![[]]
,不同于一般的markdown图片格式。而且图片都在本地,需要手动上传到OSS。 - Hexo:文中用了大量地外挂标签,无法简单的搬运,需要手工修改。
- 掘金/CSDN:图片都在它们的服务器上,无法在Halo中显示,估计是加了限制。
从人工到自动化
思路
从手工到自动化
对于Obsidian,以前我都是手工搬运的,就是将写好的笔记复制到CSDN/掘金,然后人工一个个替换图片。
这件事情也很繁琐和搬砖,因此我打算写一个脚本自动处理。具体来说,我的思路如下:
- 先读取markdown文件内容
- 通过正则匹配图片链接
- 自动将图片上传到对象存储服务
- 将返回的CDN链接自动替换原有的本地文件链接
对于Hexo,“外挂标签”用法较多,无法简单通过正则匹配来修改,而且麻烦的就是front matter。由于Halo这边没有front matter的功能,最好的做法是将front matter解析出来,然后发HTTP请求的形式创建文章,由于这个比较麻烦,我暂时还没开发。
对于CSDN/掘金的文章,我们的核心需求就是自动化地将上传到juejin/CSDN的图片下载下来,然后替换为我们自己CDN的图片。这里我的思路比较简单,就是正则匹配链接后,发HTTP请求下载文件,再传到对象存储和替换链接。
代码
下面的Python代码可以实现上述需求,阅读下面代码读者需要的前置知识有:
- 了解OSS的基本概念
- 有七牛云SDK的使用经历
- 知道什么是HTTP请求
- 了解正则的概念
我对代码的封装性做了优化,想要直接使用的话只需要修改前面配置部分的内容就可以啦。目前支持将obsidian文章中的图片自动传到OSS并替换链接,同时图片格式为普通markdown格式;还支持从CSDN/juejin上下载图片并传到OSS。
PS:要想替换Obsidian的链接,起码你的图片要有规律。推荐在设置中固定将资源放到pwd/assets,找起来和用起来也方便。如果你的文件名不是assets,可以在下方代码的配置中替换。
使用方法:
# 使用示例,type可选obsidian 或 internet,name是本地markdown的路径
python main.py --type obsidian --name 你的markdown文件名
import argparse
import json
import re
import os
import time
import requests
from qiniu import Auth, put_data
# 七牛云配置
access_key = '' # ak
secret_key = '' # sk
bucket_name = '' # 桶名字
oss_domain = "" # CDN域名
oss_folder_prefix = "test" # OSS中的文件夹前缀
# 配置
obsidian_img_prefix = "./assets" # obsidian中的资源路径,建议和我一样统一将文件存放到 ./assets/
new_file_predix = "new-" # 生成的新文件前缀,默认文件名是这个前缀+时间戳
# 常量
img_pattern_dict = { # 不同类型的正则表达式,都用于查找图片链接
"obsidian": r'!\[\[(.*?)(?:\|.*?)?\]\]',
"internet": r'!\[.*?\]\((.*?)\)'
}
task_type="" # 处理方式:默认是Obsidian处理方式
def upload_to_qiniu(file_data, file_name):
"""
上传文件到七牛云存储。
参数:
file_data: 要上传的文件数据。
file_name: 上传到七牛云后的文件名。
返回值:
成功上传后返回文件的访问URL,如果上传失败则抛出异常。
"""
# 构建鉴权对象
q = Auth(access_key, secret_key)
# 生成上传 Token
token = q.upload_token(bucket_name, file_name, 3600)
# 上传文件
_, info = put_data(token, file_name, file_data)
if info.status_code == 200:
return f"{oss_domain}/{file_name}"
else:
raise Exception(f"Upload failed: {info}")
def process_markdown_file(md_path):
"""
处理Markdown文件中的图片链接,将其上传到七牛云,并将原链接替换为七牛云上的链接。
:param md_path: Markdown文件的路径
"""
# 读取Markdown文件内容
with open(md_path, 'r', encoding='utf-8') as file:
content = file.read()
# 根据类型不同,选择不同的正则表达式,用于匹配所有的图片链接
img_pattern = img_pattern_dict[task_type]
img_paths = re.findall(img_pattern, content)
print(img_paths)
if not img_paths and task_type == "obsidian":
print("No images found in the markdown file.")
return
new_content = content
# 处理每个图片路径
for idx,img_path in enumerate(img_paths):
if(task_type == "obsidian"):
# ob中的文件名
img_path_with_assets = os.path.join(obsidian_img_prefix, img_path)
# 资源的相对路径
full_img_path = os.path.join(os.path.dirname(md_path), img_path_with_assets)
if not os.path.exists(full_img_path):
print(f"Image file not found: {full_img_path}")
continue
# 读取图片文件到内存
with open(full_img_path, 'rb') as img_file:
img_data = img_file.read()
# 上传到七牛云
file_name = f"{oss_folder_prefix}/{int(time.time())}_{idx}"
qiniu_url = upload_to_qiniu(img_data, file_name)
print(f"[obsidian-upload-success-{idx}]: 上传成功,原始图片为{img_path},原始文件名为{full_img_path},新图片地址是{qiniu_url}")
# 替换Markdown内容中的图片链接, obsidian的 ![[]] 格式,需要换成![]()
new_content = re.sub(
rf'!\[\[{re.escape(img_path)}(?:\|.*?)?\]\]',
f'',
new_content
)
elif(task_type == "internet"):
# 将图片地址下载到内存并上传到七牛云
response = requests.get(img_path)
img_data = response.content
# 上传到七牛云
file_name = f"{oss_folder_prefix}/{int(time.time())}_{idx}"
qiniu_url = upload_to_qiniu(img_data, file_name)
print(f"[internet-upload-success-{idx}]: 上传成功,原始图片地址为{img_path},新图片地址是{qiniu_url}")
# 替换markdown
new_content = new_content.replace(img_path, qiniu_url)
# 将新的内容写到新的Markdown文件
new_md_path = os.path.join(os.path.dirname(md_path), new_file_predix + os.path.basename(md_path))
with open(new_md_path, 'w', encoding='utf-8') as file:
file.write(new_content)
def main():
# 读取参数
parser = argparse.ArgumentParser(description="Process Markdown files and upload images to Qiniu Cloud.")
parser.add_argument('--name', nargs='+', required=True, help="List of Markdown files to process.")
parser.add_argument('--type',required=False,default="obsidian", help="Process Obsidian or Internet Markdown")
args = parser.parse_args()
# 修改全局变量
global task_type
task_type = args.type
# 对于每个文件,替换markdown中的本地图片上传到云端,并将图片地址替换为云端地址
for md_file in args.name:
if os.path.exists(md_file):
process_markdown_file(md_file)
print(f"==>恭喜您!文件名:{md_file},所有图片处理完成!")
else:
print(f"File not found: {md_file}")
if __name__ == "__main__":
main()
总结与后续
这个例子体现了自动化的好处,在实际学习和工作中,自动化能帮助我们很好地从重复性劳动中解放出来,去做更加有创造性价值的事情。
后续这个项目还可以继续完善,比如打造成一款通用的markdown博客转换器,在不同平台之间丝滑切换;又比如说加个UI界面等等。
但这都是后话了,通过挖掘工作中的需求并用自动化的方式解决它,再次过程中不断优化解决方案并沉淀为经验,从而把自己和他人从重复性劳动中解放出来,这是这篇博客想要分享的中心。
- 感谢你赐予我前进的力量