前言

Markdown 已然成为事实上的技术文档编写标准,作为 markdown 编辑器,typora 也收到越来越多人的推荐和喜爱。在 markdown 中我们经常需要插入图片,而 markdown 只是普通文本文件,因此图片只能作为外部链接而存在。这里的链接可以使用本地的相对路径,也可以使用网络 url。当使用网络 url 的时候,我们需要一个地方去维护和存储图片,这就是我们所谓的“图床”。

而我之前一直没有考虑使用图床,图片都是和 markdown 文件一起在 git 里面维护,主要的考虑点有:

  1. markdown 文件以及图片是作为一个整体,可以理解成是一个项目,那么项目内容本身就是密不可分的;
  2. 图片的访问权限可以跟随 markdown 文件,需要时可以有统一的访问鉴权策略;
  3. 方便管理一篇文章的所有图片,对于无用的图片可以直接删除;

在使用过程中遇到了越来越多的不便之处:

  1. github 网站在国内访问速度较慢,文字影响不大,大量的图片下载耗时很影响体验;
  2. 不方便直接分享给他人 markdown 文件,需要导出 pdf 或者打包进行分享;
  3. 在 github 仓库中,大量的图片也影响了 git 仓库的导出速度;
  4. 本地存储图片时,图片存储路径在不同场景有差异,不方便统一管理;
  5. 不能方便地进行图片的动态缩放;

权衡之后决定还是决定拥抱“图床”。typora 自带支持 iPic、uPic、PicGo 等图片上传工具,我选择国人开发的 PicGo。另外因为一直在用腾讯云服务器,自然选择了腾讯云 cos 作为图床的云存储。

腾讯云

对象存储

  1. 对象存储控制台创建一个存储桶,选择所属地域,填写桶名称,访问权限选择“公有读私有写”

    image-20211103181029412

  2. 点击桶名称进入管理页面,在左侧“域名与传输管理”中打开默认 CDN 加速域名,这里需要理解下 CDN 和源站的概念

    image-20211103181539258

  3. 回到文件列表页,可以在页面进行上传测试,点击“详情”可以查看文件的具体信息,这里对象有两个访问地址,一个是源站域名,一个是加速域名,我们一般都会选择加速域名。

    image-20211103181734118

数据万象

使用图床还有一点很重要的作用是能实现动态的图片处理,简单的比如缩放、裁剪,复杂的比如高斯模糊、水印等等,这里需要用到腾讯的数据万象。在数据万象的存储通管理中,选择绑定存储桶即可。

image-20211103182342325

点击存储通名称进入管理页面,发现这里也有个域名管理,通过这个域名访问才会支持图片的在线处理功能,这个域名本身也是支持 cdn 加速的,我们会统一采用这个域名来提供用户访问。

image-20211103182312319

访问授权

腾讯云控制台的访问管理中新建用户,可以直接使用”快速创建”。这里访问方式修改为“编程访问”,用户权限清空,可接受消息类型清空,用户名称可以用比较清晰明了的,比如picgo-upload。创建成功之后能看到子账号的账号 ID,还有 SecretId 和 SecretKey,把这些信息记录下来,我们后续需要用到。

image-20211103183050087

回到对象存储控制台,选择“授权管理”,勾选存储桶后修改“用户权限”,增加子账号的权限,权限内容可以勾选数据读取和数据写入。

image-20211103183428148

到此,腾讯云上的工作做完了。其实有一点没有谈的是费用问题,这个的话还是有必要了解的,只是这篇文章略过了。

Picgo

PicGo是一款开源跨平台的图片上传工具,能方便地上传至各种图床和云存储服务器上。可以使用带图片 GUI 的应用,也可以直接使用其核心部分基于命令行的PicGo-Core。我推荐直接使用 PicGo-Core,再加上插件能力足够满足我们的需求了。

# 如果没有npm的话需要先安装
# brew install npm
# 安装picgo
npm install picgo -g
# 安装picgo插件
picgo install autocopy
picgo install rename-file

安装完 picgo 和插件之后需要进行相关配置,同样有两种方式,一种是基于命令行的交互输入,而另一种是推荐的直接修改配置文件。配置文件在 Windows 下路径为 %HOMEPATH%\.picgo\config.json,Mac 下路径为~/.picgo/config.json

{
  "picBed": {
    "current": "tcyun",
    "tcyun": {
      "secretId": "子账号的SecretId",
      "secretKey": "子账号的SecretKey",
      "bucket": "Bucket名称",
      "appId": "",
      "area": "COS区域,类似ap-shanghai",
      "path": "",
      "customUrl": "数据万象url",
      "version": "v5"
    },
    "uploader": "tcyun",
    "transformer": "path"
  },
  "picgoPlugins": {
    "picgo-plugin-rename-file": true,
    "picgo-plugin-autocopy": true
  },
  "picgo-plugin-rename-file": {
    "format": "pic/{y}/{m}/{d}/{rand:6}"
  }
}

根据上面的注释进行字段的编辑,重命名插件的具体参数可以参考这里。配置完成之后可以通过执行picgo upload xxx.png来验证图片上传及插件配置是否生效。这里 xxx.png 可以支持本地也可以支持网络的 url。如果上传成功之后能看到完整的 url,同时也会将 url 写入剪切板,可以直接在浏览器中进行访问验证。

image-20211103191206334

比如这个地址https://pic-1251468582.picsh.myqcloud.com/pic/2021/11/03/80da56.png,,可以查看其链接规则是符合 rename-file 插件的配置的。

Typora

打开偏好设置,按需要勾选之后点击“验证图片上传选项”确认上传是否正常。

image-20211103191432287

这里要注意下 mac 系统的PicGo-Core选项并不可用,需要选择Custom Commeand,手动输入命令。另外命令还需要输入完整地址(我尝试了三遍才知道)。我配置的命令内容如下:

/opt/homebrew/bin/node /opt/homebrew/bin/picgo upload

image-20211103191923824

好了,到这里就可以在文章中很方便的插入图片了。使用过程中,可以发现本地图片转化为网络图片是需要一些时间,在上传成功之后才会替换掉本地 url。如果在中途不小心修改或者删除了相关内容,会导致后续替换 url 失败。好在我们是用了 autocopy 的插件,正确地址已经写入剪切板了,只要 ctrl+v 就可以了啦。

将历史文章中的本地图片批量上传

不想旧文章使用本地图片,而新文章才使用网络图片,这些批量化的工作当然得交给程序。用 node 或许是比较理想的方式,可以直接以 API 形式调用 picgo。但这是在我用 python 写到最后才想起的点。不多说,直接给代码,直接保存运行就好了:

import os
import re
import pyperclip

def Upload(img):
    # 使用picgo上传,需要安装插件autocopy
    pyperclip.copy("")
    ret = os.system('picgo upload ./{}'.format(img))
    if ret != 0:
        print('图片[{}]上传失败'.format(img))
        return img
    new_img = pyperclip.paste().rstrip('\n')
    if not new_img:
        print('图片[{}]似乎上传失败'.format(img))
        return img
    print('图片[{}]上传成功 ->[{}]'.format(img, new_img))
    return new_img

def Process(root, file):
    content = ''
    print('process file:{}/{}'.format(root, file))
    inf = open('{}/{}'.format(root, file), 'r')
    for line in inf.readlines():
        result = re.finditer('!\[([^]]*)\]\(([^)]*)\)', line)
        update = False
        new_line = ''
        last_pos = 0
        for r in result:
            img = r.group(2)
            if not (img.startswith('http://') or img.startswith('https://')):
                update = True
                new_line += line[last_pos : r.start(2)]
                last_pos = r.end(2)
                new_line += Upload(img)
        new_line += line[last_pos:]
        if update:
            content += new_line
        else:
            content += line
    inf.close()

    outf = open('{}/{}'.format(root, file), 'w')
    outf.write(content)
    outf.close()

if __name__ == '__main__':
    for root, dirs, files in os.walk('./_posts/'):
        for file in files:
            if file.endswith(".md") or file.endswith(".markdown"):
                Process(root, file)