本文是博客搭建系列文章第六篇,其他文章链接:
- 从零开始免费搭建自己的博客(一)——本地搭建 Hexo 框架
- 从零开始免费搭建自己的博客(二)——基于 GitHub pages 建站
- 从零开始免费搭建自己的博客(三)——基于 Gitee pages 建站
- 从零开始免费搭建自己的博客(四)——编写Markdown文章利器 Typora
- 从零开始免费搭建自己的博客(五)——Typora + PicGo + GitHub/Gitee图床
- 从零开始免费搭建自己的博客(六)——三个站点一键发布博客
- 从零开始免费搭建自己的博客(七)——迁移 CSDN 博客到个人博客站点
- 从零开始免费搭建自己的博客(八)——博客网站个性化设置及优化
前言
本文目标:本地写好文章后,双击执行一个脚本,文章可以自动发布到自己的 github.io 、gitee.io 和 CSDN。
实现原理:
- 写文章时自己在开头添加好title,date,tags等字段,用python实现文章的新旧版本替换。
- 配置好 Hexo 的配置文件,用shell脚本实现
hexo g
和hexo d
等操作,Python脚本实现Gitee Pages更新。 - 利用puppeteer模拟浏览器登录CSDN发布博客。
- 将前面三步的代码整合,使用bat脚本双击运行。
文件比较
我平时在本地写文章保存在E:\Markdown
目录,而 Hexo 保存在D:\MyBlog
目录。发布 Hexo 之前需要先把文章拷贝到D:\MyBlog\source\_posts
目录,有时候文章有修改还要重新拷贝覆盖。所以考虑用Python脚本实现拷贝文件,判断文件最后修改时间决定是否需要覆盖旧文章。
因为我们省略了hexo create "title"
这一步,直接把文件拷贝到了_posts
目录,所以写文章时需要确保在开头加上 title、date、tags、category信息,不然发布的文章会没有没有标题、发布时间、标签、分类信息,其实hexo create
命令做的就是这件事。注意最后的空行一定要有。
---
title: 我的第一篇博客
date: 2021-01-11 19:50:43
tags: [博客搭建]
---
如果是在 Typora 中编辑,直接在第一行输入---
回车就行了。
Python 代码:
# copy_to_hexo.py
import os
import shutil
import time
def copy_to_hexo():
local_list = os.listdir(LOCAL_ARTICLE_PATH)
hexo_list = os.listdir(HEXO_ARTICLE_PATH)
flag = True
for file in local_list:
if file in IGNORE_LIST:
continue
if file.endswith('.md'):
local_version = os.path.join(LOCAL_ARTICLE_PATH, file)
hexo_version = os.path.join(HEXO_ARTICLE_PATH, file)
if file not in hexo_list:
flag = False
print("新增文章: %s..." % file,
"最后修改时间:%s" % TimeStampFormat(os.path.getmtime(local_version)))
shutil.copy(local_version, hexo_version)
elif os.path.getmtime(local_version) > os.path.getmtime(hexo_version):
flag = False
print("更新文章: %s..." % file,
"上次修改时间:%s" % TimeStampFormat(os.path.getmtime(hexo_version)),
"最后修改时间:%s" % TimeStampFormat(os.path.getmtime(local_version)))
shutil.copy(local_version, hexo_version)
print('文章无变化' if flag else '更新完毕')
# 时间格式标准化
def TimeStampFormat(timestamp):
return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp))
IGNORE_LIST = ['欢迎使用Markdown编辑器.md']
HEXO_ARTICLE_PATH = 'D:/MyBlog/source/_posts'
LOCAL_ARTICLE_PATH = 'E:/Markdown'
copy_to_hexo()
运行效果:
发布Hexo到GitHub/Gitee
本来以为需要把配置文件deploy
参数改成 GitHub 地址 hexo d
一次,再改成 Gitee 地址hexo d
一次,后来看文档发现 Hexo 支持一次发布到多个仓库,只需要修改一下配置文件,然后hexo g && hexo d
就可以了。
1修改Hexo配置文件
deploy:
type: git
repo:
github: git@github.com:用户名/用户名.github.io.git,main
gitee: git@gitee.com:用户名/仓库名.git,master
shell发布脚本
Git Bash支持直接运行 shell 脚本,只需要把下面代码保存为 .sh
后缀的文件即可。
# deploy_hexo.sh
cd /d/MyBlog
pwd
# 白底黑字效果
echo -e "\033[47;30m>>>>>>>>>>>>>>>>>>>>hexo g<<<<<<<<<<<<<<<<<<<<\033[0m"
hexo g
echo -e "\033[47;30m>>>>>>>>>>>>>>>>>>>>hexo d<<<<<<<<<<<<<<<<<<<<\033[0m"
hexo d
sleep 5
# 执行完毕不退出
# exec /bin/bash
运行效果:
Gitee Pages更新脚本
上一篇博客已经实现了用 Python 脚本自动更新 Gitee pages,这里直接拿来用就行了。
# update_gitee_pages.py
# 注意: 更改25、36、52行的用户名密码为自己的Gitee的用户名密码,第45行的仓库名为图床仓库的名字
# 每处延时都有用,是我花了好长时间调试过的
import asyncio
import os
from pyppeteer import launch
async def _update_gitee_pages(usr_name, repo_name):
browser = await launch(devtools=False, dumpio=True, autoClose=True,
args=['--start-maximized', # 设置浏览器全屏
'--no-sandbox', # 取消沙盒模式,沙盒模式下权限太小
'--disable-infobars', # 关闭受控制提示
# 设置ua
'--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3542.0 Safari/537.36'
],
userDataDir=os.path.abspath('./cookies'))
page = await browser.newPage()
# 登录
await page.goto('https://gitee.com/login')
await page.waitFor(2000)
if '登录' in await page.title():
await page.type('#user_login', '用户名')
await page.type('#user_password', '密码')
await page.keyboard.press('Enter')
print('使用账号密码登录成功...')
await page.waitFor(2000)
else:
print('使用cookies缓存登录成功...')
# 更新
await page.goto('https://gitee.com/%s/%s/pages' % (usr_name, repo_name))
await page.waitFor(2000)
page.on('dialog', lambda dialog: asyncio.ensure_future(_handle_dialog(page, dialog)))
await page.click('#pages-branch > div.button.orange.redeploy-button.ui.update_deploy')
await page.waitFor(20000)
print('更新 Gitee Pages %s 成功...' % repo_name)
async def _handle_dialog(page, dialog):
await page.waitFor(2000)
print('点击确定更新')
await dialog.accept()
def update_gitee_pages(usr_name, repo_name):
asyncio.get_event_loop().run_until_complete(_update_gitee_pages(usr_name, repo_name))
if __name__ == '__main__':
update_gitee_pages('用户名', '仓库名')
运行效果:
发布到CSDN
最不好处理的是自动发布文章到 CSDN ,据说 CSDN 官方之前是提供了发布文章的接口的,后来关闭了。一文多发这个功能对于做自媒体的人来说需求很大,于是有了 OpenWrite这种一文多发平台,还有开源的ArtiPub。各大平台都没有提供发布文章的官方接口,这种一分多发平台想必也是通过模拟 http 请求来实现自动发文。
本文利用 Python 的第三方库 puppeteer 操作无头浏览器模拟登陆 CSDN 发布文章,类似 Selenium,更加接近真实操作,防止被网站检测到机器操作导致封号。最近发现 Go 语言的 go-rod 库对于模拟浏览器操作更加好用,官方文档写的很详细,两者都不用另外下载 driver,相比于 Selenium 方便了很多,后面打算用 Golang 重新实现一版,今天先看下 Python 代码。注意这只是个 Demo,实现了自动发布一篇文章的功能,我们的需求是发布多篇文章,实际使用要稍作修改,实际使用的代码是这个 post_to_csdn.py)。
# post_to_csdn_demo.py
import asyncio
import base64
import os
from PIL import Image
from pyppeteer import launch
async def main(blog_name, title, content, tags, category):
"""
:param blog_name: 博客名字,自己博客主页url的最后部分
:param title: 文章标题
:param content: 文章内容
:param tags: 标签,多个用英文","隔开
:param category: 分类
:return:
"""
browser = await launch(devtools=False, dumpio=True, autoClose=True,
args=['--start-maximized', # 设置浏览器全屏
'--no-sandbox', # 取消沙盒模式,沙盒模式下权限太小
'--disable-infobars', # 关闭受控制提示
# 设置ua
'--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3542.0 Safari/537.36'
],
userDataDir=os.path.abspath('./cookies'))
print(await browser.userAgent())
pages_list = await browser.pages()
page = pages_list[0]
# await page.setViewport(viewport={'width': 1920, 'height': 1080})
# 打开博客主页
await page.goto('https://blog.csdn.net/%s' % blog_name)
await page.waitFor(1000)
# 选择文章类型:原创
print('文章类型: 原创')
select_box = await page.querySelector(
'body > div.app.app--light > div.modal > div > div.modal__inner-2 > div.modal__content > div.inline-box > div > div')
await select_box.click()
await page.waitFor(500)
# 点击 创作中心
await page.click(
'#csdn-toolbar > div > div > div.toolbar-container-right > div > div.toolbar-btn.toolbar-btn-write.csdn-toolbar-fl > a')
await page.waitFor(4000)
if '登录' in await page.title():
print('正在登录...')
# 点击 CSDN App扫码
await page.click('#app > div > div > div.main > div.main-login > div.main-select > ul > li:nth-child(1) > a')
# 获取登录二维码
img_element = await page.querySelector('#appqr > span.app-code-wrap > img')
img_src = await (await img_element.getProperty('src')).jsonValue()
img_src = str(img_src).split(',')[1]
img_data = base64.b64decode(img_src)
img_name = 'login.png'
if os.path.exists(img_name):
os.remove(img_name)
with open(img_name, 'wb') as f:
f.write(img_data)
img = Image.open(img_name)
img.show()
# 等待登录成功,最多等5分钟
await page.waitForSelector(
'#view-containe > div.left_box > div.left_box_top > a.routerlink-bt.routerlink-bt-md > span',
timeout=300 * 1000)
print('已登录')
# 点击 Markdown 编辑器
await page.click('#view-containe > div.left_box > div.left_box_top > a.routerlink-bt.routerlink-bt-md > span')
await page.waitFor(3000)
# 切换到写文章页签
pages_list = await browser.pages()
page = pages_list[-1]
# 选中标题输入框
print('输入标题: %s' % title)
title_input = await page.querySelector(
'body > div.app.app--light > div.layout > div.layout__panel.layout__panel--articletitle-bar > div > div.article-bar__input-box > input')
# 清空原有内容
await title_input.focus()
await page.keyboard.down('Control')
await page.keyboard.press('KeyA')
await page.keyboard.up('Control')
await page.keyboard.up('Backspace')
# 输入标题内容
await title_input.type(title)
# 选中文章输入框
print('输入文章内容: %s' % content)
content_input = await page.querySelector(
'body > div.app.app--light > div.layout > div.layout__panel.flex.flex--row > div > div.layout__panel.flex.flex--row > div.layout__panel.layout__panel--editor > div.editor > pre')
# 清空原有内容
await content_input.focus()
await page.keyboard.down('Control')
await page.keyboard.press('KeyA')
await page.keyboard.up('Control')
await page.keyboard.up('Backspace')
# 输入文章内容
await content_input.type(content)
await page.waitFor(2000)
# 点击 发布文章
await page.click(
'body > div.app.app--light > div.layout > div.layout__panel.layout__panel--articletitle-bar > div > div.article-bar__user-box.flex.flex--row > button.btn.btn-publish')
# 删除原来的标签,如果有的话
exist_tags = await page.querySelectorAll(
'body > div.app.app--light > div.modal > div > div.modal__inner-2 > div.modal__content > div:nth-child(3) > div > div > div > span > span > i')
for i in exist_tags:
# 每删一个,页面会有变化
tag = await page.querySelector(
'body > div.app.app--light > div.modal > div > div.modal__inner-2 > div.modal__content > div:nth-child(3) > div > div > div > span > span > i')
await tag.click()
await page.waitFor(500)
# 点击 添加文章标签
print('添加标签: %s' % tags)
add_tag = await page.querySelector(
'body > div.app.app--light > div.modal > div > div.modal__inner-2 > div.modal__content > div:nth-child(3) > div > div > div > button')
await add_tag.click()
# 添加标签
tag_input = await page.querySelector(
'body > div.app.app--light > div.modal > div > div.modal__inner-2 > div.modal__content > div:nth-child(3) > div > div > div.mark_selection_box > div.mark_selection_box_header > div > div.el-input.el-input--suffix > input')
tag_list = tags.split(',')
for tag in tag_list:
await tag_input.type(tag.strip())
await page.waitFor(500)
await page.keyboard.press('Enter')
await page.waitFor(500)
# 收起 添加文章标签
await add_tag.click()
# 添加分类
print('添加分类: %s' % category)
add_category = await page.querySelector('#tagList > button')
await add_category.click()
category_input = await page.querySelector(
'body > div.app.app--light > div.modal > div > div.modal__inner-2 > div.modal__content > div:nth-child(4) > div > div > input')
await category_input.type(category)
await page.waitFor(500)
await page.keyboard.press('Enter')
await page.waitFor(500)
# 选择文章类型:原创
print('文章类型: 原创')
select_box = await page.querySelector(
'body > div.app.app--light > div.modal > div > div.modal__inner-2 > div.modal__content > div.inline-box > div > div')
await select_box.click()
await page.waitFor(500)
# 下拉框的 id 是个随机值,每次都不一样,通过按键曲线救国
await page.keyboard.press('ArrowDown')
await page.keyboard.press('Enter')
# 选择发布形式:公开 2, 私密 4, 粉丝可见 6, VIP可见 8
print('发布形式: 公开')
flag = 2
await page.click(
'body > div.app.app--light > div.modal > div > div.modal__inner-2 > div.modal__content > div.form-entry.flex.form-entry__field-switch-box.overflow-unset.form-entry-marginBottom > div > div > label:nth-child(%d)' % flag)
# 发布文章:保存为草稿 btn-c-blue, 发布文章 btn-b-red
print('点击发布...')
flag = 'btn-b-red'
await page.click(
'body > div.app.app--light > div.modal > div > div.modal__inner-2 > div.modal__button-bar > button.button.%s' % flag)
await page.waitFor(3000)
url_element = await page.querySelector('#alertSuccess > div > div.pos-top > div:nth-child(4) > a')
url = await (await url_element.getProperty('href')).jsonValue()
await browser.close()
print('发布成功, 文章地址: %s' % url)
blog_name = 'yushuaigee'
title = '自动发布的第一篇文章'
content = '自动发布的第一篇文章自动发布的第一篇文章自动发布的第一篇文章'
tags = '博客搭建,自动发布文章'
category = '博客搭建'
asyncio.get_event_loop().run_until_complete(main(blog_name, title, content, tags, category))
运行效果:
一键发布脚本!
上面写的4个脚本一步一步实现了我们的每个小目标,对于 Windows 系统来说,可以使用 bat 脚本把它们整合在一起,完成“一键”发布的需求。
:: post_my_blog.bat
python copy_to_hexo.py
D:\Git\git-bash.exe deploy_hexo.sh
python update_gitee_pages.py
python post_to_csdn.py
pause
写好文章后,直接双击post_my_blog.bat
就可以发布到三个地方了。
最终运行效果:
后续
本文解决了一文多发的问题,以前发布在 CSDN 上的博客就不用一篇一篇复制到自己的博客站点了,但是要想一键发布,需要先将 CSDN 上的文章先下载到本地,而且原来的文章是使用富文本编辑器写的,怎么转化成 Markdown 格式呢?下一篇文章将介绍这两个问题的解决方法。