用一个 API 挖掘知乎问答:一套实用工作流 | SandBase
搭一套知乎问答挖掘工作流:读热榜、打开问题、拉它的回答——一把 SandBase key,不用知乎登录。

在知乎上挖掘知识,归根到底是三个动作:看社区在讨论什么、打开一个问题、拉它下面的回答。这篇教程用 SandBase 知乎 API 把这三个动作串成一套知乎问答挖掘工作流——不用知乎登录,不用爬虫。端点参考保证的是响应信封(id、status、model,以及 outputs[0].data);下面出现的业务字段是示例结构,不是保证的 schema,请以一份真实响应为准核对。
如果你想先看完整的端点全景,从 知乎公开数据 API hub 开始。这一篇是落地的工作流。
先说结论
- 三步:
hot-list(热榜)→question-detail(问题)→question-answers(回答)。- 每次都是
POST /v1/api/zhihu/<path>,一把SANDBASE_API_KEY;响应共用{ id, status, model, outputs }信封。- 把热榜里的
question_id带进详情和回答读取。- 只是公开、只读数据;你这边不用登录,也不能发帖。
工作流全貌
| 步骤 | 端点 | 输入 | 你拿到 |
|---|---|---|---|
| 1. 读热榜 | zhihu/web/hot-list | 无 | 热门问题,每个带一个 target.id |
| 2. 打开问题 | zhihu/web/question-detail | question_id | 标题、回答数/关注数 |
| 3. 拉回答 | zhihu/web/question-answers | question_id(外加按 schema 的分页请求参数,如 offset/limit/cursor) | 一页回答 |
端点的 API 参考是每个参数名和响应路径的权威来源。
第 1 步 —— 读热榜
先写一个判 status 的帮助函数,再读热榜,并从一条热门项的 target 里取一个 question_id:
import os
import requests
BASE = "https://api.sandbase.ai/v1/api/zhihu"
HEADERS = {
"Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
"Content-Type": "application/json",
}
def call(path: str, payload: dict) -> dict:
resp = requests.post(f"{BASE}/{path}", headers=HEADERS, json=payload, timeout=90)
resp.raise_for_status()
body = resp.json()
if body.get("status") != "completed":
raise RuntimeError(body.get("error", {}).get("message", "request did not complete"))
# 只有信封是保证的;用 .get() 防御式读取 outputs[0].data。
return (body.get("outputs") or [{}])[0].get("data", {})
# 嵌套的业务字段(data/target/title/id)是示例结构,不是保证的 schema,
# 所以用 .get() 做防御式读取。
hot = call("web/hot-list", {}).get("data", [])
target = hot[0].get("target", {}) if hot else {}
question_id = str(target.get("id"))
print(target.get("title"), "->", question_id)
每一条热榜项把问题包在一个 target 对象下,含像 title 和 id 这样的字段。这些嵌套字段是示例结构,不是保证的 schema——以一份真实响应为准核对,因为榜单一直在变。
第 2 步 —— 打开问题
用 question-detail 读问题的元数据:
question = call("web/question-detail", {"question_id": question_id})
# 这些业务字段是示例结构、不是保证的 schema;用 .get()。
print(question.get("title"))
print(question.get("answer_count"), "answers,", question.get("follower_count"), "followers")
详情读取可能返回像 title、answer_count、comment_count、follower_count 和 excerpt 这样的字段。这些是示例结构、不是保证的 schema,请以一份真实响应为准核对。存在时,这些数量在你拉回答之前告诉你一个问题承载了多少材料。
question-detail 返回一个问题的标题和互动数量。
第 3 步 —— 拉回答
用 question-answers 读一页回答。分页由请求参数驱动——按端点 schema,可能是一对 offset/limit,也可能是你在下一次调用里带上的 cursor,而不是你在响应里去跟的某个字段:
# 分页是请求参数。确切名称看端点 schema;许多知乎列表端点
# 接受 offset/limit(有的用 cursor)。要读下一页,在下一次调用里
# 递增 offset,而不是读响应里的某个字段。
result = call("web/question-answers", {"question_id": question_id, "offset": 0, "limit": 20})
answers = result.get("data", [])
print(len(answers), "answers on this page")
回答响应把一个 data 回答列表嵌在里面。下面是一个示例响应结构——其中的业务字段是示例结构、不是保证的 schema,请把字段名和数值当作示例,以一份真实响应为准核对:
{
"id": "fad23166-43be-45dc-a1e8-8a2e60050349",
"status": "completed",
"model": "zhihu/web/question-answers",
"outputs": [
{
"data": {
"data": [
{ "type": "answer", "target": { "…": "…" } }
]
}
}
]
}
question-answers 返回回答 feed 和一个 paging 块。
把它串起来
一次最小的挖掘过程长这样:
hot = call("web/hot-list", {}).get("data", [])
report = []
for item in hot[:10]:
target = item.get("target", {}) # 业务字段是示例结构;用 .get()
question_id = str(target.get("id"))
detail = call("web/question-detail", {"question_id": question_id})
report.append({
"question": detail.get("title"),
"answers": detail.get("answer_count"),
"followers": detail.get("follower_count"),
})
# 然后对你想读全的问题调 question-answers
因为每次调用共用同一个信封和同一个 call 帮助函数,加重试或速率退避是一处改动的事。要读到第一页回答之后,发送端点的分页请求参数——按 schema 递增 offset(或带上下一个 cursor)。当你需要的不止这些读取时,查线上知乎列表找到合适的端点,接入前先确认它的参数。
处理粗糙的边角
- 注意嵌套。 热榜项把问题包在
target下;回答响应把列表嵌在data里。用防御式方式读准确的路径,别假设是顶层字段,并把这些业务字段当作示例结构、不是保证的 schema。 - 用请求参数翻页。
question-answers通过请求参数翻页——按 schema 是一对offset/limit或一个cursor——所以在下一次调用里递增参数,别假设一页就是整个问题串。 - 对
status分支。failed或timeout的请求带error而没有outputs。call帮助函数已经强制这一点。 - 尊重速率限制。 作为客户端韧性措施,遇到 HTTP 429 这类瞬时错误时用退避重试。
- 只是公开数据。 不登录、不发帖,也拿不到私密/仅账号可见的内容。
为什么在 API 层做这件事
你当然可以在浏览器里打开知乎手动抄回答,但那不 scale,也给不了你结构化数据。把这三次调用排成定时任务,就把一个实时榜变成了可度量的信号:能做趋势的回答数和关注数、能按 question_id 去重的问题、以及能整串拉下来做摘要或搜索的问题串。因为调用返回的是命名的 JSON 字段,每一轮都能干净地落进一张表,再和上一轮做 diff——新的热门问题、在增长的问题串,以及社区在讨论内容的变化。把回答文本变成洞察,是你在采集到的数据之上另跑的一个分析步骤。
组合这套工作流
同样的统一信封让它可组合。把一个 question_id 换成另一个,再加第四次读取——比如某条回答的评论,或用 user-info 读回答者的资料——它就用同一个 call 帮助函数、同一套判状态接进来。你也可以把中间那步铺开:一次过程里对热榜上每个问题都拉 question-detail,按 answer_count 或 follower_count 排序,这样在你为 question-answers 花调用之前,就先决定哪些问题串值得读全。因为这些读取共用一种结构,从一个快速脚本走到一个定时任务,基本上只是加个退避和一个存每轮结果的地方——读取逻辑不变。
常见问题
我需要知乎登录或 OAuth 吗?
不需要。你用自己的 SANDBASE_API_KEY 向 SandBase 鉴权,读热榜、读一个问题以及它的回答,都不需要你这边有知乎账号或 OAuth。发起这些调用时你仍然要提供一把 SandBase API key。
怎么读到第一页回答之后?
下一次调用时带上 question-answers 接受的分页请求参数来取下一页,不要假设一页就是整个问题串。具体参数名以端点参考为准,迭代前先对着一份真实响应核对。
我能用这种方式读私密或仅账号可见的内容吗?
不能。这些是公开、只读的读取——拿不到私密、仅关注者可见或需登录才可见的内容。另外,端点参考只保证 { id, status, model, outputs } 这层信封,里面的业务字段会变,请以一份真实响应为准去对准。
下一步
你现在有了一套可复用的问答挖掘工作流,建立在三次公开、只读的调用上。