使用Python和MetaWeblog API发博客
又开始折腾了。
最近开始用obsidian作为主力笔记软件,自然也想用它写博客,但obsidian没有MWeb一样强大的发布功能,于是自己动手,基于Python和MetaWeblog API写了一段脚本,实现命令行发布文章到typecho博客。
这段时间博客中经常出现测试文章,就是在调试脚本。本文是obsidian写完、该脚本发布的。
一、脚本实现功能
- 发布文件,若已发布过,自动更新。
- 自动将obsidian的[[]]转化为markdown标准格式,包括链接和图片。
- 上传图片到又拍云,并替换文章中的图片链接。
支持常用的Front Matter信息,与正文之间用
+++
分隔,meta支持:- title: 标题,必填
- date: 日期,必填,yyyymmddhhmm格式,比如202008231859
- slug: 自定义URL,可选,若不填以date中的一段作为slug
- categories: 分类,可选,英文逗号分隔
- tags: 标签,可选,英文逗号分隔
二、文章格式
如下图,+++
分隔Front Matter和正文。
三、脚本使用
脚本在文末,基于Python3.8开发,依赖第三方库upyun
(又拍云的SDK)。准备好环境后,正确填写脚本中的个人信息配置,保存为typecho.py
,按如下格式使用:
typecho.py post_file.md post
第一个参数是要发布的markdown文件,必须。
第二个参数是可选的,表示发布文章的类型,post是文章,page是页面,不填默认是post。
更新成功返回如下内容:
更新成功,信息如下:
{'title': '使用Python和MetaWeblog API发博客', 'date': '202008231735', 'categories': ['科技'], 'tags': 'Python,MetaWeblog API,Typecho', 'slug': '20082317'}
发布成功信息类似。
四、开发过程
非专业人士,代码不保证健壮性,勉强能用。过程中有些想法:
1、要实现更新文章,需要在本地保存发布状态,MetaWeblog API只能使用cid对文章进行更新,所以需要在本地保存cid,最初我考虑将cid回写到本地文件的Front Matter区域,但最后放弃了。因为回写是高风险的操作,可能破坏本地文件。所以,最后也选择了使用本地文件.slug_cid_mapping.txt
保存发布状态。若是MetaWeblog API支持使用slug更新文章,就完美了。
2、图片的上传状态没有记录,所以原则上每次更新都会重新上传图片到又拍云,但发现又拍云上并没有重复,可能又拍云有相关策略控制。
3、Front Matter是个非常好的理念,可扩展性强,但我觉得每次写Front Matter还是有些麻烦。博文最重要的信息包括标题、发布日期、类目和slug,类目可以取本地文件夹名、标题和发布日期可以写在文件名上、slug可以直接用发布日期。这样就不需要Front Matter了。
4、[[]]的处理,没有过滤代码块,如果代码块中被正则匹配到,代码块也会被修改。这个问题暂时不会解,好在我的文章代码不多。
5、MetaWeblog API有个坑,struct
中,categories
是字典,mt_keywords
(即tags)却是逗号分隔的字符串。
五、脚本分享
#!/usr/bin/python3
# -*- coding: UTF-8 -*-
import xmlrpc.client
import datetime
import time
import re
import upyun
import sys
# 个人信息配置
BLOG_USERNAME = '' # 博客用户名
BLOG_PASSWORD = '' # 博客密码
UP_SERVICE = '' #又拍云服务名
UP_USERNAME = '' # 又拍云操作员名称
UP_PASSWORD = '' #又拍云操作员密码
UP_PATH = '/blog_static/' # 又拍云上传图片的目录
PICTURE_PATH = '/User/obsidian_note/media/' # 本地保证图片的目录
METAWEBLOG_API = '' # 博客metaweblog api地址
SLUG_CID_MAPPING = '/User/obsidian_note/blog/.slug_cid_mapping.txt' # 保存slug与cid的mapping文件,默认隐藏
# 解析markdown文件
# 入参:markdown文件路径
# 返回:data字典,data['meta']为front_meta字典,包含title/date/category/tags(列表),data['content']为文章正文[[]]未经特殊处理
def file_read(file):
with open(file, 'r') as f:
file_content = f.read().strip()
array = file_content.split('+++\n')
if len(array) != 2:
print('格式错误\n\n\n')
else:
front_meta_txt = array[0].strip().split('\n')
front_meta_dict = {}
for kv in front_meta_txt:
key = kv.split(':')[0].strip()
value = kv.split(':', 1)[1].strip()
front_meta_dict[key] = value
if 'title' not in front_meta_dict or front_meta_dict['title'] == '':
print('必须有title且不能为空')
if 'date' not in front_meta_dict or front_meta_dict['date'] == '':
print('必须有date且不能为空')
if 'categories' not in front_meta_dict:
front_meta_dict['categories'] = []
else:
front_meta_dict['categories'] = list_strip(front_meta_dict['categories'].split(','))
if 'tags' not in front_meta_dict:
front_meta_dict['tags'] = ''
else:
front_meta_dict['tags'] = ','.join(list_strip(front_meta_dict['tags'].split(',')))
if 'slug' not in front_meta_dict or front_meta_dict['slug'] == '':
front_meta_dict['slug'] = front_meta_dict['date'][2:-2]
data = {'meta': front_meta_dict, 'content': array[1].strip()}
return data
# 格式化字符串list,去掉空值,去掉字符串两端的空字符
def list_strip(lst):
result = []
for v in lst:
if v.strip() != '': result = result + [v]
return result
# URL替换函数:把[[]]替换为[]()标准markdown链接
def url_repl(matchobj):
meta = matchobj.group(0)[2:-2].split('|', 1)
if len(meta) == 2:
return '[{txt}]({url})'.format(txt=meta[1], url='/' + meta[0][-10:-2] + '.html')
else:
return '[{txt}]({url})'.format(txt=meta[0], url='/' + meta[0][-10:-2] + '.html')
# IMG替换函数:上传图片到又拍云,并把![[]]替换为![]()标准markdown图片
def img_repl(matchobj):
meta = matchobj.group(0)[3:-2].split('|', 1)
file_path = PICTURE_PATH + meta[0]
up_path = '/blog_static/' + meta[0]
upload_picture_to_upyun(file_path, up_path)
if len(meta) == 2:
return '![{txt}]({url})'.format(txt=meta[1], url=up_path)
else:
return '![{txt}]({url})'.format(txt='', url=up_path)
# 将[]]和![[](/]]和![[.html)转化为标准的[]()和![]()
def to_stard_markdown(content):
content = re.sub('[^!]\[\[(.+?)\]\]', url_repl, content)
content = re.sub('!\[\[(.+?[png|PNG|jpg|JPG|jpeg|JPEG|gif|GIF].*?)\]\]', img_repl, content)
return content
# 上传图片到又拍云
# 入参:file_path本地文件路径,up_path定义又拍云路径
def upload_picture_to_upyun(file_path, up_path):
up = upyun.UpYun(UP_SERVICE, UP_USERNAME, UP_PASSWORD)
with open(file_path, 'rb') as f:
res = up.put(up_path, f)
# 根据slug获取本地的cid
def get_cid(slug):
with open(SLUG_CID_MAPPING, 'r') as f:
kvs = list_strip(f.read().split('\n'))
if kvs == []:
return '0'
else:
mapping = {}
for kv in kvs:
mapping[kv.split(':')[0].strip()] = kv.split(':')[1].strip()
if slug not in mapping:
return '0'
else:
return mapping[slug]
# 保存slug和cid的关系
def save_cid(slug, cid):
if int(cid) > 0:
with open(SLUG_CID_MAPPING, 'a') as f:
f.write(slug + ':' + cid + '\n')
else:
return 'cid不对,保存失败'
# 创建文章,若文件已经存在,则自动更新,
# 入参:file要发布的文件,post_type发布类型(post-文章,page:页面)
def new_post(file, post_type='post'):
data = file_read(file)
slug = data['meta']['slug'] # 基于date获取slug
# 构建发布内容
struct = {
'title': data['meta']['title'],
'wp_slug': slug,
'dateCreated': datetime.datetime.strptime(data['meta']['date'],'%Y%m%d%H%M')-datetime.timedelta(hours=8),
'description': to_stard_markdown(data['content']),
'categories': data['meta']['categories'],
'mt_keywords': data['meta']['tags'],
'post_type': post_type,
}
client = xmlrpc.client.ServerProxy(METAWEBLOG_API)
cid = get_cid(slug)
if int(cid) > 0:
try:
result = client.metaWeblog.editPost(cid, BLOG_USERNAME, BLOG_PASSWORD, struct, True)
print('更新成功,信息如下:\n', data['meta'], '\n\n')
except Exception as e:
print(e)
else:
cid = client.metaWeblog.newPost('',BLOG_USERNAME, BLOG_PASSWORD, struct, True)
if cid != 0:
save_cid(str(slug), str(cid))
print('发布成功,信息如下::\n', data['meta'], '\n\n')
if __name__ == '__main__':
try:
if len(sys.argv) == 3:
file_path, post_type = sys.argv[1], sys.argv[2]
new_post(file_path, post_type)
elif len(sys.argv) == 2:
file_path = sys.argv[1]
new_post(file_path)
except Exception as e:
print(sys.argv)
print(e)
好厉害,我回去研究看看
这是曲线救国,如果能直接开发一个obsidian插件就更好了,可惜我不会
我也试了一下,感觉如果这货出了手机版,那么市面上一个能打的都没有,包括我做群主的笔记软件
哈哈,你好快啊,我还在用这篇文章调试代码呢,终于搞定了。
obsidian明显要做笔记界的vscode,非常期待v1.0版本api出来,到时应该能玩出各种花样来。