给自己的博客打通 XML-RPC 接口这件事,我拖了很久。总觉得"手动在后台写也挺好",直到需要批量整理旧文、或者想从本地脚本直接把 Markdown 推上去时,才发现命令行这条路的爽点。
现在这条链路已经跑通了:本地写 Markdown → 一条命令 → 草稿箱。整个过程踩了两个不小的坑,都记在这里。
一、接口地址是固定
Typecho 的 XML-RPC 入口不需要装插件,核心自带,路径固定:
https://你的域名/action/xmlrpc先用 system.listMethods 探一下底:
import xmlrpc.client
p = xmlrpc.client.ServerProxy("https://你的域名/action/xmlrpc")
print(p.system.listMethods())会吐出一长串方法名。完整数下来是 58 个,覆盖四个系列:
| 系列 | 代表方法 | 用途 |
|---|---|---|
| metaWeblog | newPost / editPost / deletePost | 文章增删改 |
| metaWeblog | newMediaObject | 上传附件 |
| mt | getCategoryList / publishPost | 分类、发布状态 |
| wp | getCategories / getProfile / getTags | 站点信息 |
| system | listMethods / multicall | 自省与批量调用 |
也就是说,发布、编辑、删除、查分类、传图片,全都支持。这是个完整可用的 CMS API。
二、第一个坑:301 重定向把标准客户端干翻了
第一次用 Python 标准库连,直接报错:
ProtocolError: 301 Moved Permanently诡异的是,用 curl 打同一个地址却返回 200。原因在这里:
- 请求不带尾斜杠的
/action/xmlrpc时,服务器会先返回 301 跳转; curl手动 POST 的那条路径恰好没触发跳转,看着像正常;- 而 Python 的
xmlrpc.client.Transport默认不跟随重定向,301 直接被当成错误抛出。
解决办法是自己重写 Transport,用 urllib 的 opener 发请求——它会自动跟随跳转:
import urllib.request
import xmlrpc.client
import ssl as _ssl
class RedirectSafeTransport(xmlrpc.client.Transport):
def __init__(self, *a, **kw):
super().__init__(*a, **kw)
# 基类 __init__ 不设置 verbose,但 parse_response() 依赖它,
# 不补这一行会报 AttributeError
self.verbose = False
def request(self, host, handler, request_body, verbose=False):
self.verbose = verbose
url = "https://%s%s" % (host, handler)
ctx = _ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = _ssl.CERT_NONE
req = urllib.request.Request(
url, data=request_body,
headers={"Content-Type": "text/xml",
"User-Agent": "Typecho-Client/1.0"},
)
opener = urllib.request.build_opener(
urllib.request.HTTPSHandler(context=ctx))
with opener.open(req, timeout=30) as r:
return self.parse_response(r)注意 self.verbose = False 那一行。Transport.__init__ 并不设置 verbose,而 parse_response() 会读它——自定义 Transport 时漏掉这行,就会得到一个莫名其妙的 AttributeError。
三、第二个坑:metaWeblog.getPost 恒返回 500
读文章列表没问题:
posts = p.metaWeblog.getRecentPosts("1", "用户名", "密码", 10)但想按 ID 读单篇时:
post = p.metaWeblog.getPost("1", "用户名", "密码") # HTTP 500连已发布的正常文章也照样 500,排查了半天确认是 Typecho 这个版本的实现缺陷,不是参数或权限问题。绕开方式有两个:
- 用
getRecentPosts拉一页回来自己筛(字段最全,推荐); - 或改用
blogger.getPost,注意它要 4 个参数:
post = p.blogger.getPost("", "1", "用户名", "密码")四、发布一篇 Markdown 文章
正文直接传 Markdown 源码,Typecho 侧会按站点设置解析渲染:
post_id = p.metaWeblog.newPost("1", "用户名", "密码", {
"title": "文章标题",
"description": "# 正文\n\n直接用 Markdown 写就行。",
"mt_keywords": "Typecho,XML-RPC,Python",
"categories": ["默认分类"],
"post_status": "draft",
}, False) # 最后一个参数 False = 存草稿,True = 直接发布建议默认走草稿:先 publish=False 推进去,在后台看一眼排版,确认无误再 mt.publishPost 发布。尤其是带代码块和表格的长文,草稿能省掉"发出去又赶紧撤"的尴尬。
小结
| 事项 | 结论 |
|---|---|
| 接口 | /action/xmlrpc,核心自带 |
| 重定向 | 必须自定义 Transport 跟随 301 |
| 读单篇 | 避开 metaWeblog.getPost,用 getRecentPosts |
| 格式 | description 传 Markdown 源码 |
| 发布 | 先草稿后发布 |
打通之后,写作和发布终于解耦了:内容在本地用熟悉的编辑器写,版本可以用 Git 管,发布只是最后一条命令。对习惯在终端里干活的人来说,这一步的收益远比想象中大。
下一步打算把「写完 Markdown 自动推草稿」做成一个脚本,配合 front-matter 填标题和标签,那就真的只剩敲字了。
没有评论