WordPress 自动发布:把重复劳动一次写完
写这一篇的契机:上一篇文章发完之后,我立刻把整套发布流程写成了一个 Python 工具。这篇把搭建过程拆开,讲清楚每一步为什么这么做、踩过什么坑。
背景
我有个个人站点,文章数量不多,但每次发文都要经历这些步骤:
- 打开后台
- 复制 Markdown 内容到编辑器
- 手选分类、标签、特色图
- 点发布
- 复制 permalink 验证
5 个步骤,每次花 3 到 5 分钟。看起来不多,但累积起来就是不小的时间成本。更关键的是:每次都在做同样的事,本身就是低效的。
既然我有 Python,为什么不让它替我做?
备选方案
发布到 WordPress 主要有两条技术路线:
| 方案 | 入口 | 优点 | 缺点 |
|---|---|---|---|
| REST API | /wp-json/wp/v2/... |
现代、官方推荐 | 部分站点需应用密码、鉴权配置复杂 |
| XML-RPC | /xmlrpc.php |
老牌、协议简单 | 部分主机默认关闭 |
REST API 看起来更”现代”,但实际操作中:
- 部分 WordPress 站点即便开了”应用密码”,Basic Auth 仍返回 401
- Cookie 鉴权需要从 wp-login.php 登录再拿 nonce,流程更繁琐
- 编辑器(古腾堡)的 nonce 校验跟 REST API 的 nonce 不是同一套
XML-RPC 的优势是:一个用户名密码走到底,不需要额外的 token 流程。我这次就选了 XML-RPC。
第一步:验证连接
import xmlrpc.client
import ssl
class SafeTransport(xmlrpc.client.SafeTransport):
def make_connection(self, host):
conn = super().make_connection(host)
conn.context = ssl._create_unverified_context()
return conn
proxy = xmlrpc.client.ServerProxy(
"https://你的站点/xmlrpc.php",
transport=SafeTransport()
)
# 拿用户信息验证
user = proxy.wp.getProfile(0, "用户名", "密码")
print(user.get("display_name"), "ID:", user.get("user_id"))
关于 SSL:
ssl._create_unverified_context()跳过证书校验。生产环境你应该用真正的证书,但个人站点调试阶段先用这个跑通。
第二步:写一个最小可用的发布函数
import datetime
def publish(title, content_html, category_ids):
post = {
"post_title": title,
"post_content": content_html,
"post_status": "publish",
"post_type": "post",
"categories": category_ids,
"post_date": datetime.datetime.now().strftime("%Y%m%dT%H:%M:%S"),
}
return int(proxy.wp.newPost(0, "用户名", "密码", post))
这 10 行代码就能发一篇文章。但是——
第三步:踩坑
坑 1:categories 字段被忽略
我第一次发布时,传入 "categories": [13],期望文章落在「呼叫吉米」分类。结果文章发出了,但 permalink 是:
https://你的站点/uncategorized/125/
uncategorized(未分类)。categories 字段被静默忽略了。
绕过办法:发完之后用 wp.editPost + post_category 字段强制修正:
proxy.wp.editPost(0, "用户名", "密码", post_id, {
"post_category": [13] # 注意是 post_category,不是 categories
})
坑 2:permalink 不会自动重定向
修正分类后,文章 permalink 仍然显示 uncategorized/125/,但实际访问会被重定向到 呼叫吉米/125/。这是 WordPress 的设计:permalink 包含历史分类路径,文章移动分类后旧链接会 301 跳到新链接。
对访问者没影响,但如果你写脚本验证”分类是否正确”,不要只看 permalink 字符串,要看 permalink 是否包含目标分类名。
坑 3:第三方库的兼容性
python-wordpress-xmlrpc 是社区里最常用的封装库,但它依赖 collections.Iterable(Python 3.9 之前的位置)。Python 3.10+ 移除了这个属性,直接 import 就会报错。
解决办法:不用第三方库,直接用标准库的 xmlrpc.client。代码量多 20 行,但稳定性高、不依赖第三方。
第四步:把 Markdown 转 HTML
XML-RPC 接口要求 post_content 是 HTML,不能直接传 Markdown。我写了一个轻量转换器,覆盖以下语法:
# ~ ######标题- item无序列表1. item有序列表> quote引用- `
code` 行内代码 `代码块粗体斜体link
复杂的 Markdown 语法(表格、嵌套列表、HTML 混排)我故意没支持。理由:
- 个人站点文章结构简单
- 完整的 Markdown 解析器(如
python-markdown)会增加依赖 - 真遇到复杂格式,手写 HTML 反而更可控
第五步:加分类白名单
个人站点只有一个专栏,但接口本身支持任意分类。我加了一道白名单校验:
ALLOWED_CATEGORIES = {13: "呼叫吉米"}
def publish(title, content_html, category_id):
if category_id not in ALLOWED_CATEGORIES:
raise ValueError(f"分类 {category_id} 不在白名单")
# ...
这么做的好处是:以后加新分类要明确加进白名单,不会被一时手滑发到错误的栏目。
最终工具代码
把上面的步骤组装起来,最终的发布工具就长这样(精简版):
import xmlrpc.client
import ssl
import datetime
import re
class WP:
def __init__(self, url, user, pwd):
class T(xmlrpc.client.SafeTransport):
def make_connection(self, h):
c = super().make_connection(h)
c.context = ssl._create_unverified_context()
return c
self.proxy = xmlrpc.client.ServerProxy(url, transport=T())
self.user = user
self.pwd = pwd
def publish(self, title, html, cat_id, allow):
if cat_id not in allow:
raise ValueError("分类不在白名单")
pid = int(self.proxy.wp.newPost(0, self.user, self.pwd, {
"post_title": title,
"post_content": html,
"post_status": "publish",
"post_type": "post",
"post_date": datetime.datetime.now().strftime("%Y%m%dT%H:%M:%S"),
}))
# 强制设置分类
self.proxy.wp.editPost(0, self.user, self.pwd, pid,
{"post_category": [cat_id]})
return pid
# 用法
wp = WP("https://你的站点/xmlrpc.php", "用户名", "密码")
post_id = wp.publish("标题", "<p>HTML 内容</p>", 13, {13: "呼叫吉米"})
这次跑通的总结
| 项目 | 状态 |
|---|---|
| XML-RPC 鉴权 | 通 |
| Markdown → HTML | 通(自写转换器,~50 行) |
| 分类设置 | 一次不生效,二次 wp.editPost 修正成功 |
| 公开访问 permalink | 通(301 跳到正确分类) |
| 总代码量 | 约 150 行(含 Markdown 转换、异常处理、白名单) |
后续可以加的功能
- [ ] 图片上传(
metaWeblog.newMediaObject) - [ ] 定时发布(
post_date字段加未来时间) - [ ] 草稿自动保存(
post_status: "draft") - [ ] 标签管理(
wp.newTerm) - [ ] 一键回滚(发布前先存本地文件,错了能从本地重发)
工具不复杂,但省下来的时间会一直累积。
—— 呼叫吉米