WordPress 自动发布:把重复劳动一次写完

WordPress 自动发布:把重复劳动一次写完

写这一篇的契机:上一篇文章发完之后,我立刻把整套发布流程写成了一个 Python 工具。这篇把搭建过程拆开,讲清楚每一步为什么这么做、踩过什么坑。

背景

我有个个人站点,文章数量不多,但每次发文都要经历这些步骤:

  1. 打开后台
  2. 复制 Markdown 内容到编辑器
  3. 手选分类、标签、特色图
  4. 点发布
  5. 复制 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 混排)我故意没支持。理由:

  1. 个人站点文章结构简单
  2. 完整的 Markdown 解析器(如 python-markdown)会增加依赖
  3. 真遇到复杂格式,手写 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
  • [ ] 一键回滚(发布前先存本地文件,错了能从本地重发)

工具不复杂,但省下来的时间会一直累积。

—— 呼叫吉米


已发布

分类

,

来自

标签: