从真实浏览器 HAR(DevTools 录制 zhuanlan.zhihu.com 发布一篇文章)逆向得出。发一篇文章不是单接口,而是 建草稿 → 自动保存 → 加话题 → 发布 四步。

四步流程

步骤接口说明
1 建草稿POST https://zhuanlan.zhihu.com/api/articles/draftsBody: {"content": "<p>HTML…</p>", "table_of_contents": false, "delta_time": 0, "can_reward": false}。返回完整草稿对象,含文章 id。
2 自动保存PATCH https://zhuanlan.zhihu.com/api/articles/{id}/draft编辑过程中反复调用;delta_time 为编辑耗时(秒)。
3 加话题POST https://zhuanlan.zhihu.com/api/articles/{id}/topics ×NBody 直接传整个 topic 对象:{name, url, excerpt, introduction, followersCount, avatarUrl, type, id, questionsCount}。每个话题调一次。
4 发布POST https://www.zhihu.com/api/v4/content/publish真正的"发文章"。见下。

发布接口详情

  • host 是 www.zhihu.com,但 origin / referer 是 zhuanlan.zhihu.com(sec-fetch-site: same-site)——编辑页在 zhuanlan 域,发布 API 在 www 域,跨子域调用。
  • Body 是嵌套 JSON,pc_business_params 和响应 data.result 都是字符串化的 JSON(双层转义),解析要 json.loads 两次:
{
  "action": "article",
  "data": {
    "publish": { "traceId": "时间戳,random-uuid" },
    "extra_info": {
      "publisher": "pc",
      "pc_business_params": "{\"content\":\"<h3>…HTML…</h3>\",\"title\":\"…\",\"commentPermission\":\"anyone\",\"disclaimer_type\":\"none\",\"disclaimer_status\":\"close\",\"table_of_contents_enabled\":false,\"canReward\":false}"
    },
    "draft": { "disabled": 1, "id": "2071994212195759032", "isPublished": false },
    "commentsPermission": { "comment_permission": "anyone" },
    "creationStatement": { "disclaimer_type": "none", "disclaimer_status": "close" },
    "contentsTables": { "table_of_contents_enabled": false },
    "commercialReportInfo": { "isReport": 0 },
    "appreciate": { "can_reward": false, "tagline": "" },
    "hybrid": { "html": "<h3>…</h3>…", "textLength": 2365 },
    "title": { "title": "…" }
  }
}

响应 200:{"message": "success", "data": {"result": "{\"publish\":{…id…}}"}}。

老接口发布链路(已验证可跑通,无需动态签名)

HAR 里记录的是新接口 POST /api/v4/content/publish(要 x-zse-96),但逆向工具 zhihu-cli 用的是老接口链路,全程只要 cookie + x-xsrf-token,2026-08 实测三步全 200、文章真实发布成功:

1. POST   https://zhuanlan.zhihu.com/api/articles/drafts       # 建【空】草稿,json={},返回 id
2. PATCH  https://zhuanlan.zhihu.com/api/articles/{id}/draft   # 填 {"title","content"}(content 是 HTML)
3. PUT    https://zhuanlan.zhihu.com/api/articles/{id}/publish # 发布,json={"column":null,"commentPermission":"anyone"}

关键点:

  • 建草稿传空 body(老接口),正文在 PATCH 才填;新接口是建草稿时就带全文。
  • 发布请求本身不含正文——正文已在草稿里。
  • 全程只依赖 x-xsrftoken(= cookie 里的 _xsrf)+ z_c0。不需要 x-zse-93/96。
  • 删除文章:DELETE https://zhuanlan.zhihu.com/api/articles/{id}(200)。
  • 读已发布文章(验证用):GET https://zhuanlan.zhihu.com/api/articles/{id} 返回 200 且含 content;但 GET www.zhihu.com/api/v4/articles/{id} 读接口会 403(读接口风控更严,不影响发布)。

发布到专栏(column)

老接口 PUT /publish 的 column 字段必须传完整专栏对象,传字符串或裸 id 会 500:

# 正确
{"column": {"id": "c_1943647118402248830", "title": "计算机基础",
            "type": "column", "url": "http://www.zhihu.com/api/v4/columns/c_1943647118402248830"},
 "commentPermission": "anyone"}

# 错误 → 500
{"column": "c_1943647118402248830", ...}
{"column": "1943647118402248830", ...}

2026-08 实测:这样发布后 GET 文章,column 字段返回完整对象,文章确实挂在专栏下。GET https://www.zhihu.com/api/v4/columns/{c_id} 可取专栏 title/id。

顺带修正:新接口 POST /api/v4/content/publish 其实不需要 x-zse-96——纯 requests + 有效 cookie 直接调用返回 200 success(之前"要签名"的判断是错的)。但新接口在 data 里加 column 也带不出来(返回 null),挂专栏只有老接口传完整对象这一条路走通。

列出自己的专栏

GET https://www.zhihu.com/api/v4/members/{url_token}/column-contributions?offset=0&limit=50&include=data[*].column.articles_count — 返回 data[].column 数组,含 id/title/articles_count。url_token 用 GET /api/v4/me 取。

表格标签(知乎编辑器专用)

知乎编辑器(ProseMirror)不认标准 <table>——markdown 库转出的 <table><thead>… 发布后会被服务端静默丢弃。必须转成知乎自定义格式:

<table data-draft-node="block" data-draft-type="table"
       data-size="normal" data-row-style="normal"><tbody>
  <tr><th>表头1</th><th>表头2</th></tr>
  <tr><td>数据</td><td>数据</td></tr>
</tbody></table>

即:把 markdown 库输出的 <thead><tr> 合并进 <tbody><tr>,去掉 thead/tbody 嵌套,加上三个 data-* 属性。代码块 <pre>、粗体、标题层级则无需转换,直接可用。

反爬签名(重放难点)

  • 发布接口要求 x-zse-93: 101_3_3.0 + x-zse-96: 2.0_… + x-zst-81: 3_2.0aR_… 三个签名头 + cookie z_c0(登录凭证)+ x-xsrftoken。
  • x-zse-* 由页面 JS 按 cookie / UA / 时间实时计算,curl 无法直接重放;带 z_c0 登录 cookie 也不够。
  • 实测签名头稳定性(HAR 里逐条比对):x-zst-81 会话内静态(16 条请求同一值,可复用);x-zse-93 是版本号,静态;x-zse-96 每次请求都不同(7 条请求 7 个值),绑定请求内容/时间,不可复用。
  • 自动保存(PATCH draft)和建草稿(POST drafts)只验 x-zst-81 + x-xsrf-token,只用静态 auth 就能直接发请求;加话题 / 发布必须每次重算 x-zse-96。
  • cookie 里的 __utma/__utmz(GA)、Hm_lvt_*(百度统计)等是纯统计埋点,对请求无作用。
  • 坑:Chrome DevTools 导出的 HAR 会把请求头的 cookie 整条剥掉(98 条请求 0 条带 cookie 头),xsrf/zst81 还在;cookie 要从「复制为 cURL」里取。

坑

  • 正文 title/content 在发布体里出现多处(pc_business_params、hybrid.html、title),以 pc_business_params 内为准。
  • hybrid.html 是前端渲染好的正文,与 content 基本一致;发布成功后服务端会给段落加 data-pid。
  • 先建草稿拿到 id,发布体 draft.id 引用它,isPublished: false 表示草稿态。

关联:cURL (Client URL 命令行请求工具)、Web 登录鉴权 (Session, Cookie, Bearer, JWT)


本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Web