折腾自建博客系列

今年年初由 wordpress 迁移到 halo,主要是觉得懂点 Java,有什么定制化的需求,自己改代码会方便一些。但是也没有什么特别的需求需要定制,反而被这种东西折腾得忘记了写博客的初心。技术博客就应该简简单单,只写技术,博客怎么好看、怎么炫酷,都不重要。配置说明&前置条件在 Halo

从CSDN迁移到Github Pages

打算将CSDN上的博客迁移到GitHub Pages上去,怕有一天所有的博客都不见了的时候,自己会有一个备份. 其中,上传到CSDN网站上的图片,是必须要自己有一个备份的。所以,便有了这一篇记录博客。

了解Scrapy

Scrapy是一个Python包,是一个爬取网页的框架。顾名思义,可以理解成Java中的抽象类。它负责流程性的逻辑,我们自己编写具体的处理逻辑。经过此次的使用,发现它确实是一个比较厉害的框架。首先,在爬取网页的过程中,你可能会遇到的问题,网上都有相应的解答;其次,我们只需要编写处理网页的逻辑和小的流程逻辑,这样我们可以更加专注爬取这件事本身,而不用关注使用什么技术来怎么爬。

简易使用

网上的使用方法很多,这里只写一些关键性的步骤以及自己遇到的问题、解决办法。

初始化项目:scrapy startproject CSDNBlogMover

各个文件及目录的作用:

items.py

# 定义所需数据的属性,比如说,想爬取每一篇文章,此文件中则定义一些文章属性,如下:
class CsdnblogmoverItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    link = scrapy.Field()
    title = scrapy.Field()
    time = scrapy.Field()
    tags = scrapy.Field()
    categories = scrapy.Field()
    content = scrapy.Field()
    comments = scrapy.Field()

pipelines.py

# 此文件可以理解成在某些关键时候,所要执行的操作,比如打开、关闭、处理爬虫这3个关键时候,与Android中Activity的生命周期类似
class CsdnblogmoverPipeline(object):
    def open_spider(self, spider):
        self.myconn = mysql_connection()

    def close_spider(self, spider):
        self.myconn.conn.close()

    def process_item(self, item, spider):
        self.myconn.reconnect()
        sql = 'INSERT INTO wp_posts(post_author, post_excerpt, to_ping, pinged, post_content_filtered, post_date, post_date_gmt, post_content, post_title, post_modified, post_modified_gmt) VALUES (1, " "," ", " ", "", %s, %s, %s, %s, %s, %s)'
        vars = (get_uniformed_datetime(item['time'], DATE_FORMAT_CN), get_gmt_datetime(item['time'], DATE_FORMAT_CN), '<!-- wp:html -->\n'+item['content'] + item['comments'] + '\n<!-- /wp:html -->', item['title'], get_now_datetime(), get_gmt_datetime(get_now_datetime()))
        self.myconn.cursor.execute(sql, vars)
        
        self.myconn.conn.commit()
        print(item)
        return item

middlewares.py

这个文件的作用在此次爬取博客中未使用到,但是也了解了部分内容,其实就是可以在这里面加上selenium,来处理动态加载的数据

settings.py

这是一个配置类,里面有很多项,并且都有相应的注释,可以仔细看看。

BOT_NAME = 'CSDNBlogMover'

SPIDER_MODULES = ['CSDNBlogMover.spiders']
NEWSPIDER_MODULE = 'CSDNBlogMover.spiders'
FEED_EXPORT_ENCODING = 'utf-8'

ROBOTSTXT_OBEY = True

ITEM_PIPELINES = {
   'CSDNBlogMover.pipelines.CsdnblogmoverPipeline': 300,
}

爬取的关键:spiders/CSDNBlogSpider.py

import json
import math

import scrapy

# 需要更换相应的Cookie,来确定你对此博客的拥有权,否则会抓取失败。
# 直接从浏览器的开发者工具中,打开编辑器时,所发出的请求的Cookie即可
raw_cookie = 'xxx'
items = raw_cookie.split('; ')
cookies = {}
for item in items:
    kv = item.split('=')
    cookies[kv[0]] = kv[1]

print(cookies)

class CSDNBlogSpider(scrapy.Spider):
    # 名字,用来使用scrapy crawl csdn_spider来调用此爬虫
    name = 'csdn_spider'
    # 限定爬取的域名,如果域名不为此,则会爬取失败
    allowed_domains = ['blog.csdn.net', 'mp.csdn.net']
    author = ''

    def __init__(self):
        # self.browser = webdriver.Chrome()
        # self.browser.set_page_load_timeout(30)
        pass

    def closed(self, spider):
        print("spider closed")

    # 爬虫入口
    def start_requests(self):
        urls = [
            'https://blog.csdn.net/asahinokawa'
        ]
        for url in urls:
            self.author = url.split('/')[-1]
            # 返回一个Request,并指明:当请求发送成功,收到响应后,执行self.parse()回调
            yield scrapy.Request(url=url, callback=self.parse)

    # 只负责爬取页数
    def parse(self, response):
        # 获取总博客数
        total_blog_cnt = response.xpath(
            '//*[@id="mainBox"]/aside/div[@id="asideProfile"]/div[2]/dl[1]/dd/a/span/text()').get()
        if total_blog_cnt is None or total_blog_cnt == '':
            raise Exception('total blog number parse error')
        else:
            total_blog_cnt = int(total_blog_cnt)
        # 获取第1页的博客总数
        if response.status == 200:
            links = response.xpath('//*[@id="mainBox"]/main/div[2]/div[@data-articleid]/h4/a/@href')
            raw_links = []
            for link in links:
                if link.get().__contains__(self.author):
                    # yield scrapy.Request(link.get(), callback=self.parse_content)
                    raw_links.append(link.get())
                else:
                    print('%s not belong to author %s' % (link, self.author))
            # 获取第1页博客数量
            first_page_blog_cnt = len(raw_links)

            if first_page_blog_cnt <= total_blog_cnt:  # 不止一页
                # 通过总页数与第一页页数,来计算总共有多少页
                pages = int(math.ceil(total_blog_cnt / first_page_blog_cnt)) + 1
                for idx in range(1, int(pages)):
                    page_url = '%s/article/list/%d' % (response.url, idx)
                    print(page_url)
                    # 返回爬取某页页面的请求
                    yield scrapy.Request(url=page_url, callback=self.parse_page)
            else:
                self.parse_page(self, response)
        else:
            print('对CSDN主页访问的响应异常,请检查URL')
    # 爬取某页页面的请求
    def parse_page(self, response):
        if response.status == 200:
            links = response.xpath('//*[@id="mainBox"]/main/div[2]/div[@data-articleid]/h4/a/@href')
            # 依次遍历所有的博客请求,并第每一个博客链接(即所有文章),进行文章页面爬取
            for link in links:
                # 包含作者
                if link.get().__contains__(self.author):
                    yield scrapy.Request(url=link.get(), callback=self.parse_content)
                else:
                    print('%s not belong to author %s, skipped' % (link.get(), self.author))
        else:
            print('对CSDN中某页访问出错')

    # 爬取页面
    @staticmethod
    def parse_content(self, response):
        if response.status == 200:
            data =  {
                'link': response.url,
                'title': response.xpath(
                    '//*[@id="mainBox"]/main/div[@class="blog-content-box"]/div[@class="article-header-box"]/div[@class="article-header"]/div[@class="article-title-box"]/h1/text()').get(),
                # //*[@id="mainBox"]/main/div[1]/div/div/div[2]/div[1]/span[1]
                'time': response.xpath(
                    '//*[@id="mainBox"]/main/div[@class="blog-content-box"]/div[@class="article-header-box"]/div[@class="article-header"]/div[@class="article-info-box"]/div[@class="article-bar-top"]/span[@class="time"]/text()').get(),
                # //*[@id="mainBox"]/main/div[1]/div/div/div[2]/div[1]/span[3]/a
                'tags': response.xpath(
                    '//*[@id="mainBox"]/main/div[@class="blog-content-box"]/div[@class="article-header-box"]/div[@class="article-header"]/div[@class="article-info-box"]/div[@class="article-bar-top"]/span[@class="tags-box artic-tag-box"]/a/text()').get(),
                # //*[@id="mainBox"]/main/div[1]/div/div/div[2]/div[1]/div/a
                'categories': response.xpath(
                    '//*[@id="mainBox"]/main/div[@class="blog-content-box"]/div[@class="article-header-box"]/div[@class="article-header"]/div[@class="article-info-box"]/div[@class="article-bar-top"]/div[@class="tags-box space"]/a/text()').get(),
                # //*[@id="mainBox"]/main/div[1]/article
                # class="blog-content-box"
                'content': response.xpath('//*[@id="mainBox"]/main/div[@class="blog-content-box"]/article').get(),
                # //*[@id="mainBox"]/main/div[3]/div[2]
                'comments': response.xpath(
                    '//*[@id="mainBox"]/main/div[@class="comment-box"]/div[@class="comment-list-container"]').get()
            }
            # 获取markdown文档
            mark_down_url = 'https://mp.csdn.net/mdeditor/getArticle?id=' + str(response.url.split('/')[-1])
            yield scrapy.Request(url=mark_down_url,
                                 callback=self.get_markdown_content,
                                 meta=data,
                                 # Headers模拟浏览器的头部
                                 headers={
                                     'accept-encoding': 'gzip, deflate, br',
                                     'accept': '*/*',
                                     'accept-language': 'zh,en;q=0.9,ja;q=0.8,zh-TW;q=0.7,fr;q=0.6,zh-CN;q=0.5',
                                     'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36',
                                 },
                                 # 需要更换相应的Cookie,来确定你对此博客的拥有权,否则会抓取失败
                                 cookies=cookies)
        else:
            print('对CSDN中某篇文章访问出错')
    # 接收markdown内容,并作为Item的属性
    @staticmethod
    def get_markdown_content(response):
        content = json.loads(response.body_as_unicode())
        data = content['data']
        if 'markdowncontent' in data:
            content = data['markdowncontent']
            # print('$$$$ == ' + content)
            response.meta['content'] = content
            response.meta['tags'] = data['tags']
            response.meta['categories'] = data['categories']
            # 此处返回的是一个完整的Item
            return response.meta
        else:
            print('获取失败 : '+response.meta['link'])

关键性的API获取

在上面的处理中,对于Hexo来说,最为关键的API是获取markdown的API,这里记录一下发现的过程。此处要求原文是markdown格式,否则得到的markdown数据为空。

第一步、在登录状态下,点开编辑

点开编辑

观察API

链接:https://mp.csdn.net/mdeditor/getArticle?id=89402970

观察API

入参为一个id加一些Headers(这里选择与浏览器保持一致),返回的参数为一个JSON串,里面的数据如下:

{
    "data": {
        "id": "89402970",
        "title": "Flask如何使用logging.FileHandler将日志保存到文件",
        "articleedittype": 1,
        "description": "需求\n将日志尽可能往文件中输,自带的默认只输出到屏幕上。\n代码\n获取文件名\ndef get_custom_file_name():\n    def make_dir(make_dir_path):\n        path = make_dir_path.strip()\n        if not os.path.exists(path):\n            os.makedirs(pat...",
        "content": "<h2><a id=\"_0\"></a>需求</h2>\n<p>将日志尽可能往文件中输,自带的默认只输出到屏幕上。</p>\n<h2><a id=\"_3\"></a>代码</h2>\n<p>获取文件名</p>\n....",
        "markdowncontent": "## 需求\n将日志尽可能往文件中输,自带的默认只输出到屏幕上。\n\n## 代码\n获取文件名\n```python\ndef get_custom_file_name():\n    def make_dir(make_dir_path):\n        path = make_dir_path.strip()\n        if not os.path.exists(path):\n            os.makedirs(path)\n        return path\n    log_dir = \"ac_logs\"\n    file_name = 'logger-' + time.strftime('%Y-%m-%d', time.localtime(time.time())) + '.log'\n    file_folder = os.path.abspath(os.path.dirname(__file__)) + os.sep + log_dir\n    make_dir(file_folder)\n    return file_folder + os.sep + file_name\n```\n配置logging\n\n```python\ndictConfig({\n    'version': 1,\n    'formatters': {'default': {\n        'format': '%(asctime)s - %(levelname)s - %(filename)s - %(funcName)s - %(lineno)s - %(message)s',\n    }},\n    'handlers': {\n        'default': {\n            'class': 'logging.StreamHandler',\n            'stream': 'ext://flask.logging.wsgi_errors_stream',\n            'formatter': 'default'\n        },\n        'custom': {\n            'class' : 'logging.FileHandler',\n            'formatter': 'default',\n            'filename' : get_custom_file_name(),\n            'encoding' : 'utf-8'\n        },\n    },\n    'root': {\n        'level': 'INFO',\n        'handlers': ['custom']\n    }\n})\n```\n## 代码分析...",
        "private": 0,
        "tags": "Flask,日志",
        "categories": "Python",
        "channel": "31",
        "type": "original",
        "status": 1,
        "read_need_vip": 0
    },
    "error": "",
    "status": true
}

对图片的处理

至此,所有格式为markdown的CSDN博客都被抓取下来,接下来就是对其中图片的处理。对于图片,初步的想法是,先把markdown中的图片下载下来,再随机命名,然后把原来markdown中的图片链接修改成改名之后的,图片的baseUrl使用gitee前缀,也就是说,把所有下载下来的图片都存到一个仓库中,(也算是作为一种备份吧)然后通过如下形式的地址,来访问该图片:https://eucham.me/content/images/pics/xxx.jpg。处理代码:

import os
import random
import re
import string

import filetype as filetype

dir = "C:\\Users\\yangyu\\sasuraiu.github.io\\source\\_posts"
img_dir = "C:\\Users\\yangyu\\sasuraiu.github.io\\source\\_posts\\images"
mds = os.listdir(dir)
pattern = re.compile('!\[.*\]\(([^\)]+)\)')

# 读取markdown内容
def get_file_content(path):
    content = ''
    if not os.path.isdir(path):
        with open(path, 'r', encoding='utf-8') as f:
            for line in f:
                content = content + line
        return content
    else:
        return None
# 修改markdown中的图片原地址
def parse_content(content):
    changed = False
    for pic in pattern.findall(content):
        # print(pic)
        new_pic_link = download_pic(pic, img_dir, random_name())
        if new_pic_link is not None:
            content = content.replace(pic, 'https://eucham.me/content/images/pics/' + new_pic_link)
            changed = True

    return changed, content
# 随机名称
def random_name():
    return ''.join(random.sample(string.ascii_letters + string.digits, 32)).upper()
# 下载图片并重命名
def download_pic(url, path, name):
    import requests
    r = requests.get(url)
    if r.status_code == 200:
        filepath = path + '\\' + name
        # 下载
        with open(filepath, 'wb') as f:
            f.write(r.content)
        # 重命名,通过文件魔数确定图片类型
        # 这里进行类型判断是为了避免格式错误导致的图片无法展示
        kind = filetype.guess(filepath)
        if kind is None:
            print('Cannot guess file type!')
            os.remove(filepath)
            return None
        else:
            os.rename(filepath, filepath+'.'+kind.extension)
            return name+'.'+kind.extension
    else:
        print(url)
        print('下载图片失败,请手动确认\n\n')
        return None
# 修改后保存回文件
def write_back_file(filepath, content):
    with open(filepath, 'w', encoding='utf-8') as f:
        f.write(content)

if __name__ == '__main__':
    for md in mds:
        print('当前正在处理的文件为:' + md)
        filepath = dir + "\\" + md
        # 获取文件内容
        content = get_file_content(filepath)
        # 解析文件中是否含有图片链接
        if content is not None and content != '':
            changed, content = parse_content(content)
            # 替换回原文件
            if changed:
                write_back_file(filepath, content)
                print('replaced original file with the latest link')

后记

至此,大部分重复性质的工作已经完成了,剩下就是对每一个篇文章进行格式检查。此爬虫的代码地址为(欢迎fork):https://github.com/sasuraiu/CSDNBlogMover

wordpress添加https访问

docker中的wordpress

申请证书

可以从freessl.cn免费申请。免费的SSL证书时间长度为1年,但是只能对单个域名,不支持多域名通配符,选择的话以个人需求为准。

选择浏览器生成

步骤1

点击确认创建后,得到如下信息:

步骤2

接下来到域名管理里面,按上述信息配置域名的信息,可以参考上面的验证配置指南,如下:

步骤3

配置完了之后,不一定会立马生效,取决于配置改解析项的TTL。

apache配置

  1. 将容器里面的443端口映射到宿主机的443端口。如果已启动了容器,可能需要重新创建。
  2. 将申请好的证书和私钥上传到宿主机中,并将其挂载到容器中。
docker run --name wp \
-p 80:80 \
-p 443:443 \
-e WORDPRESS_DB_HOST=host \
-e WORDPRESS_DB_USER=user \
-e WORDPRESS_DB_PASSWORD="" \
-v /root/wordpress:/var/www/html \
-v /root/ssl:/ssl \
-d wordpress
  1. 先进入容器中
docker container exec -it wp bash
  1. 加载apache的ssl模块
a2enmod ssl
  1. 修改证书和私钥路径
vim /etc/apache2/sites-available/default-ssl.conf

找到SSLCertificateFileSSLCertificateKeyFile这两个配置项,改成把私钥和证书挂载进容器里面后的路径,这里都在/ssl/目录下。修改后为:

apache ssl配置
  1. 让ssl配置被apache加载
ln -s /etc/apache2/sites-available/default-ssl.conf /etc/apache2/sites-enabled/default-ssl.conf

退出容器,并重启容器。 docker container restart wp

强制http请求转到https 编辑 /etc/apache2/sites-available/000-default.conf,找到<VirtualHost *:80> </VirtualHost>标签中增加下面的配置:

<Directory "/var/www/html"> 
    RewriteEngine   on
    RewriteBase /
    # FORCE HTTPS
    RewriteCond %{HTTPS} !=on
    RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L]
</Directory>

如下:

http强跳https配置
  1. 退出容器,并重启容器。 docker container restart wp

检验

如果不能访问,可以往如下两方面考虑:

  1. 查看容器的日志,看报什么错误信息:
docker container logs -f wp
  1. 看宿主机的443端口是否开放

参考: https://blog.csdn.net/yori_chen/article/details/88577249

从hexo批量迁移到wordpress

感觉"业务"有扩展,hexo不能动态添加文章有点不太适应

wordpress添加markdown支持

选择了WP Editor.md这个插件,新增post,测试markdown能够生效。

获取hexo博客的md文档

source/_posts下有所有的markdown文件,全都是博客的内容,并且是有一定的格式规律的。这里我需要的关于博客的数据有标题、发布日期、标签以及目录,当然还有博客正文。非常好解析。

读取所有md文件的代码如下:

dir = "/xxxxx/blog-source/source/_posts"
files = os.listdir(dir)
count = 0
if __name__ == '__main__':
    files = os.listdir(dir)
    can_go_on = False
    for file in files:
        full_path = dir + '/' + file
        print(full_path)
		parse_md_file(full_path)
        # if count >= 10:
        # break;
        count = count + 1
        print("Count: ", count)
    print(count)

解析每个md文档

首先,是文件最开始有两个---,在这两个---之间的全部是文章的属性,之外的全是文章的内容。解析文章属性的时候,需要对文章的标签、目录做可能存在多个处理,所以用list存储。其中post_meta_data_status的各值的含义如下:

post_meta_data_status 含义
0 初始状态,刚开始解析md文件
1 正在解析文章属性状态
2 文章属性解析完成,正在解析文章内容

解析md文件的代码如下:

def get_property(line, splitter=':'):
    items = line.split(splitter)
    item = items[len(items) - 1].strip()
    return item

def parse_md_file(file_path, print_content=False):
    title = ""
    tag = []
    category = []
    last_item = []
    date = ""
    post_content = ""
    with open(file_path, encoding='utf8') as f:
        post_meta_data_status = 0
        for line in f:
            if post_meta_data_status == 2:
                post_content += line
                # print(line, end='')
            else:
                if line.__contains__("---"):
                    if post_meta_data_status == 0:
                        post_meta_data_status = 1
                    else:
                        post_meta_data_status = 2
                else:
                    if line.__contains__("title"):
                        title = get_property(line).strip()
                    elif line.__contains__("date"):
                        date = get_property(line, ': ').strip()
                    elif line.__contains__("tags"):
                        item = get_property(line)
                        if item!='':
                            tag.append(item)
                        last_item = tag
                    elif line.__contains__("categories"):
                        item = get_property(line)
                        if item != '':
                            category.append(item)
                        last_item = category
                    elif line.__contains__('-'):
                        item = get_property(line, '-')
                        if item != '':
                            last_item.append(item)
        print("title: ", title)
        print("date: ", date)
        print("tags: ", tag)
        print("categories: ", category)
        date=datetime.datetime.strptime(date, "%Y-%m-%d %H:%M:%S")

将解析后的数据上传到wordpress

上传主要用到了wordpress-xmlrpc。其基本操作可参看该官网上的用例

安装方式:pip install python-wordpress-xmlrpc

from wordpress_xmlrpc import Client, WordPressPost
from wordpress_xmlrpc.methods.posts import GetPosts, NewPost
from wordpress_xmlrpc.methods.users import GetUserInfo

wp = Client('http://www.wordpress.site/xmlrpc.php', 'username', 'password')

def add_post(title, date, content, tag, category):
    post = WordPressPost()
    post.title = title
    post.content = content
    post.post_status = 'publish'
	# date为python的datetime类型
    post.date = date
    post.terms_names = {
        'post_tag': tag,
        'category': category,
    }
    post_id = wp.call(NewPost(post))
    print(post_id)

如果需要更新更多的post相关的信息,可参看WordPressPost文档

从halo迁移到hexo

今年年初由 wordpress 迁移到 halo,主要是觉得懂点 Java,有什么定制化的需求,自己改代码会方便一些。但是也没有什么特别的需求需要定制,反而被这种东西折腾得忘记了写博客的初心。技术博客就应该简简单单,只写技术,博客怎么好看、怎么炫酷,都不重要。

配置说明&前置条件

  • 在 Halo 中,数据库使用的是 MySQL 5.7,并将 halo 服务所用数据库,内容导入到本地的数据库中,这样速度会快一些。
  • hexo 所用主题是 Icarus

编码

总体思路:将 halo 的数据,导出成 hexo 所需要的格式。总体分为下面几个步骤:

  1. halo 字段与 hexo 字段对比 参考 hexo 文档,与之一一对应即可。后续根据 Icarus 的几个配置,又在 front-matter 里面添加了几个属性。
参数 描述 默认值
layout 布局
title 标题 文章的文件名
date 建立日期 文件建立日期
updated 更新日期 文件更新日期
comments 开启文章的评论功能 true
tags 标签(不适用于分页)
categories 分类(不适用于分页)
permalink 覆盖文章网址

将 halo 的 tags、categories 转化成 hexo 里面的 tags、categories

tag与post的关联关系

category 与 tag 类似,都是通过一个关联表,与 post 建立关联关系。转换的逻辑自然变成了:根据 post_id 查 post_tags 表中的关联关系,得到 tag_id,再从 tag 表中获取 tag 的名称。

转化文章预览 这部分不太好转,直接用 markdown 有些 markdown 的语法符号,没办法过滤掉、或者过滤起来很难受。这里的做法是这样的:

  • 在 front-matter 中添加 excerpt 属性
  • 如果原文章(halo)中存在 summary,直接赋值给 excerpt
  • 没有 summary,则先将 markdown 转化成 HTML,然后获取 HTML 中的前几个 dom 元素。

文章属性

type Post struct {
	Title      string
	Content    string
	Password   string
	Date       string
	Updated    string
	Thumbnail  string
	Tags       []string
	Categories []string
	Summary    string
	Priority   string
}

主流程

package main

import (
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"github.com/gomarkdown/markdown"
	"html"
	"io/ioutil"
	"path"
	"regexp"
	"strings"
	"xorm.io/xorm"
)

func main() {
	basePath := "/Users/akina/hexo-blog/source/_posts"
	db, _ := xorm.NewEngine("mysql", "root:root@tcp(localhost:3306)/halodb-tmp")
	db.ShowSQL(true)
	queriedPosts, _ := db.Query("select * from posts")
	for _, qp := range queriedPosts {
		pid := qp["id"]
		post := Post{
			Title:     string(qp["title"]),
			Content:   html.UnescapeString(string(qp["original_content"])),
			Password:  string(qp["password"]),
			Date:      string(qp["create_time"]),
			Updated:   string(qp["update_time"]),
			Thumbnail: string(qp["thumbnail"]),
			Summary:   string(qp["summary"]),
			Priority:  string(qp["top_priority"]),
		}
		// 处理标签
		queriedTagIds, _ := db.Query("select tag_id from post_tags where post_id = " + string(pid))
		if len(queriedTagIds) > 0 {
			tagIds := make([]string, 0)
			for _, tagId := range queriedTagIds {
				tagIds = append(tagIds, string(tagId["tag_id"]))
			}
			queriedTagNames, _ := db.Query(fmt.Sprintf("select name from tags where id in (%s)", strings.Join(tagIds, ",")))
			tagNames := make([]string, 0)
			for _, tagName := range queriedTagNames {
				tagNames = append(tagNames, string(tagName["name"]))
			}
			post.Tags = tagNames
		}
		// 处理分类
		queriedCategoryIds, _ := db.Query("select category_id from post_categories where post_id = " + string(pid))
		if len(queriedCategoryIds) > 0 {
			categoryIds := make([]string, 0)
			for _, categoryId := range queriedCategoryIds {
				categoryIds = append(categoryIds, string(categoryId["category_id"]))
			}
			queriedCategoryNames, _ := db.Query(fmt.Sprintf("select name from categories where id in (%s)", strings.Join(categoryIds, ",")))
			categoryNames := make([]string, 0)
			for _, categoryName := range queriedCategoryNames {
				categoryNames = append(categoryNames, "- ["+string(categoryName["name"])+"]")
			}
			post.Categories = categoryNames
		}
		// 写文件
		postHexoData := strings.Join([]string{
			"---",
			"title: \"" + post.Title + "\"",
			"date: " + post.Date,
			"updated: " + post.Updated,
			"tags: [" + strings.Join(post.Tags, ",") + "]",
			"categories: \n" + strings.Join(post.Categories, "\n"),
			"thumbnail: " + post.Thumbnail,
			"password: " + post.Password,
			"excerpt: \"" + getPostSummary(post) + "\"",
			"top: " + post.Priority,
			"toc: true",
			"---",
			post.Content,
		}, "\n")
		ioutil.WriteFile(path.Join(basePath, post.Title+".md"), []byte(postHexoData), 0666)
	}
}

获取文章的预览

func getPostSummary(post Post) string {
	var res string
	if post.Summary != "" {
		res = post.Summary
	} else {
		htmlContent := string(markdown.ToHTML([]byte(post.Content), nil, nil))

		for i := 1; i <= 4; i++ {
			re := regexp.MustCompile("<.*?>.*?</.*?>")
			label := string(re.Find([]byte(htmlContent)))
			if label == "" {
				break
			}
			res = res + label

			htmlContent = strings.ReplaceAll(htmlContent, label, "")
			if htmlContent == "" {
				break
			}
		}
	}

	for _, ch := range []string{
		"\n",
	} {
		res = strings.ReplaceAll(res, ch, " ")
	}
	res = strings.ReplaceAll(res, "\"", "'")
	res = strings.TrimPrefix(res, " ")
	res = strings.TrimSuffix(res, " ")

	fmt.Println(res)

	return res
}

Conclusion

后续有几个需要改一下预览的内容,貌似不能通过 hexo g,原因是转义符的问题、单、双引号的问题等,只有少量,手动改了改,还算可以接受。

Read more

容器镜像(4):镜像的常用工具箱

容器镜像(4):镜像的常用工具箱

前几篇在讲多架构镜像时已经用过 skopeo 和 crane 做镜像复制,这篇系统整理这两个工具的完整能力,同时介绍几个日常操作镜像时同样好用的工具。 一、skopeo:不依赖 Daemon 的镜像瑞士军刀 skopeo 的核心价值是绕过 Docker daemon,直接与 Registry API 交互。上一篇用它做镜像复制和离线传输,但它的能力远不止于此。 1.1 安装 # Ubuntu / Debian sudo apt install -y skopeo skopeo --version # skopeo version 1.15.1 1.2 inspect:免拉取检查镜像元数据 docker inspect 需要先把镜像拉到本地,skopeo inspect 直接向 Registry

容器镜像(3):多架构镜像构建

容器镜像(3):多架构镜像构建

一、什么是多架构镜像 1.1 OCI Image Index 上一篇介绍了单平台镜像的结构:一个 Manifest 指向 Config 和若干 Layer blob。多架构镜像在此之上多了一层——OCI Image Index(也叫 Manifest List),是一个轻量的索引文件,把多个单平台 Manifest 组织在一起: $ docker manifest inspect golang:1.22-alpine { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.index.v1+json", "manifests&

容器镜像(2):containerd 视角下的镜像

容器镜像(2):containerd 视角下的镜像

一、为什么需要了解 containerd 如果你只用 docker run 跑容器,从来不关心底层,那可以不了解 containerd。但如果你在用 Kubernetes,或者想真正理解"容器运行时"是什么,containerd 是绕不开的。 事实上,当你执行 docker run 的时候,containerd 早就在后台悄悄工作了——Docker 从 1.11 版本开始,就把核心运行时剥离出来交给 containerd 负责。 1.1 Docker 的架构演变 早期的 Docker(1.10 及之前)是一个"大一统"的单体程序:一个 dockerd