zhihu — search, read & publish on Zhihu
Two authentication layers, two scripts:
| Script |
Auth |
Capabilities |
scripts/search.py |
ZHIHUDEVELOPERTOKEN (Bearer) |
Search Zhihu, search the web, get hot topics |
scripts/blog.py |
ZHIHU_COOKIES (login cookie) |
Read/write own articles & answers |
No browser, no third-party deps — just urllib.
The connector injects credentials as env vars:
ZHIHUDEVELOPERTOKEN — Zhihu Developer Platform access secret (Bearer token).
Used for search and hot-list queries. Secret — never echo or print it.
ZHIHU_COOKIES — a JSON array of {name, value, domain, path, ...} cookies.
Used for reading/writing the user's own content. Secret — never echo or print it.
Locate the scripts first (every Bash block)
The connector sets $SKILLDIR to this skill's directory, so the scripts live at $SKILLDIR/scripts/. Do NOT hard-code python3 $SKILLDIR/scripts/… directly. If more than one skill was loaded in the same turn, $SKILLDIR can point at the other skill and the call fails with No such file or directory (this is the #1 cause of a Zhihu run silently not finishing). Resolve the path defensively at the top of every Bash block — each Bash call is a fresh shell, so the variable does not carry over:
# blog.py is unique to this skill, so it anchors zhihu's dir even if $SKILL_DIR is wrong.
ZDIR="$SKILL_DIR"; [ -f "$ZDIR/scripts/blog.py" ] || ZDIR=$(find /tmp -maxdepth 8 -path '*/skills/*/scripts/blog.py' 2>/dev/null | head -1 | sed 's#/scripts/blog.py##')
[ -f "$ZDIR/scripts/blog.py" ] || { echo "zhihu scripts not found (SKILL_DIR=$SKILL_DIR) — is the skill loaded?" >&2; exit 1; }
SEARCH="$ZDIR/scripts/search.py"; BLOG="$ZDIR/scripts/blog.py"
Then use "$SEARCH" / "$BLOG" (always quoted) as shown below.
Search CLI (search.py)
[scripts/search.py](scripts/search.py) — search Zhihu and the web. Requires only ZHIHUDEVELOPERTOKEN (no cookies needed).
# Resolve the scripts first (see "Locate the scripts first" above) — robust to $SKILL_DIR.
ZDIR="$SKILL_DIR"; [ -f "$ZDIR/scripts/blog.py" ] || ZDIR=$(find /tmp -maxdepth 8 -path '*/skills/*/scripts/blog.py' 2>/dev/null | head -1 | sed 's#/scripts/blog.py##')
SEARCH="$ZDIR/scripts/search.py"
# Search Zhihu content (站内搜索) — questions, answers, articles
python3 "$SEARCH" search "Python 爬虫"
python3 "$SEARCH" search "Python 爬虫" --count 5
# Search the entire web (全网搜索) — all indexed sites
python3 "$SEARCH" global "AI Agent"
python3 "$SEARCH" global "AI Agent" --count 15
# Filter by site or time
python3 "$SEARCH" global "React" --filter 'host=="github.com"'
python3 "$SEARCH" global "新闻" --filter 'publish_time>=1720000000'
python3 "$SEARCH" global "技术" --filter 'host=="github.com" AND publish_time>=1720000000'
python3 "$SEARCH" global "实时新闻" --db realtime
# Get Zhihu trending topics (热榜)
python3 "$SEARCH" hot
python3 "$SEARCH" hot --limit 10
Search commands
| Goal |
Command |
| Search Zhihu (max 10 results) |
python3 "$SEARCH" search "<query>" --count N |
| Search entire web (max 20) |
python3 "$SEARCH" global "<query>" --count N |
| Filter by site |
--filter 'host=="example.com"' |
| Filter by time |
--filter 'publishtime>=<unixts>' |
| Search only realtime/static index |
--db realtime or --db static |
| Zhihu trending topics (max 30) |
python3 "$SEARCH" hot --limit N |
Search result fields
zhihusearch returns: title, type (Article/Answer), contentid, url, excerpt, voteup (赞同), comments, author, authority level, edittime.
globalsearch returns the same fields plus hasmore indicator. The url includes utm tracking params from Zhihu's platform.
hot_list returns: rank, title, url, summary, thumbnail.
global_search Filter syntax
host=="example.com" — filter by domain (note: host=="zhihu.com" not
supported — use search command instead)
publish_time>=1720000000 — filter by publish time (unix seconds)
- Logical operators:
AND, OR (must be uppercase)
- Parentheses for grouping:
(host=="a.com" OR host=="b.com") AND publish_time>=T
Blog CLI (blog.py)
The skill ships [scripts/blog.py](scripts/blog.py) — self-contained, stdlib only. Requires ZHIHU_COOKIES (login cookie).
# Resolve the scripts first (see "Locate the scripts first" above) — robust to $SKILL_DIR.
ZDIR="$SKILL_DIR"; [ -f "$ZDIR/scripts/blog.py" ] || ZDIR=$(find /tmp -maxdepth 8 -path '*/skills/*/scripts/blog.py' 2>/dev/null | head -1 | sed 's#/scripts/blog.py##')
BLOG="$ZDIR/scripts/blog.py"
# Read (run directly)
python3 "$BLOG" whoami # who is logged in
python3 "$BLOG" articles --limit 20 # my published articles + stats
python3 "$BLOG" article <article-id> # one article's details + stats
python3 "$BLOG" answers --limit 20 # my published answers + stats
python3 "$BLOG" answer <answer-id> # one answer's details + stats
python3 "$BLOG" question <question-id> # a question's info + whether I answered it
Verify the connection first
ZDIR="$SKILL_DIR"; [ -f "$ZDIR/scripts/blog.py" ] || ZDIR=$(find /tmp -maxdepth 8 -path '*/skills/*/scripts/blog.py' 2>/dev/null | head -1 | sed 's#/scripts/blog.py##')
python3 "$ZDIR/scripts/blog.py" whoami
# → {"id": "...", "name": "<the connected display name>", "url_token": "<the connected handle>", ...}
On a 401/403 the cookie is expired — tell the user to reconnect at <https://studio.acedata.cloud/console/connectors> (re-capture with the ACE extension). Do not retry in a loop.
Reading recipes
| Goal |
Command |
| Who am I |
python3 "$BLOG" whoami |
| My latest articles + vote/comment counts |
python3 "$BLOG" articles --limit 20 |
| My latest answers + like/favorite/comment counts |
python3 "$BLOG" answers --limit 20 |
| Next page (any list) |
add --offset 20 |
| One article's stats |
python3 "$BLOG" article <id> |
| One answer's stats (incl. 赞同 voteup) |
python3 "$BLOG" answer <id> |
| A question's info + my answer id (if any) |
python3 "$BLOG" question <id> |
Article stats: voteupcount (赞同), commentcount (评论). Zhihu does not expose per-article read counts on these endpoints.
Answer stat caveat: the answers list endpoint does not return voteupcount (赞同) — it only exposes likecount (喜欢), favoritecount (收藏) and commentcount. For the authoritative 赞同 count of an answer, call answer <id> (the single-answer endpoint returns voteupcount). question <id> reports alreadyanswered + myanswerid so you know whether to use answer-question (new) or edit-answer (update).
Publishing — GATED (dry-run unless trailing --confirm)
publish writes to the user's real account. Without a trailing --confirm it dry-runs (prints what it would do, changes nothing). --confirm is honored only as the last argument, so a title/content containing "--confirm" can never silently go live. Always show the dry-run to the user, get an explicit "yes", then re-run with --confirm last.
# Content is HTML. For Markdown, convert to HTML first (e.g. `pandoc -f gfm -t html`).
python3 "$BLOG" publish --title "标题" --content-file article.html # dry-run
python3 "$BLOG" publish --title "标题" --content-file article.html --draft-only --confirm # save a private draft
python3 "$BLOG" publish --title "标题" --content-file article.html --confirm # PUBLIC, goes live
--draft-only stops after saving a private draft (safe — nothing public).
- Without
--draft-only, the article is published publicly under the user's
name. Default to --draft-only unless the user clearly asked to go live.
- Images are auto-hosted. Zhihu strips any
<img> whose src is not on its
own CDN, so on --confirm the CLI re-uploads every external image (HTML <img src> and Markdown , plus data: URIs) to Zhihu's image service and rewrites the URLs first — images already on *.zhimg.com are left untouched. The result reports images: {found, rehosted, failed}; the dry-run reports images_found. Pass --no-images to skip this. So you can hand the CLI HTML/Markdown with normal public image URLs and the pictures survive.
Answering questions — GATED (dry-run unless trailing --confirm)
Two write commands cover the question/answer side. Both gate exactly like publish: no trailing --confirm → dry-run; --confirm is honored only as the last argument. Always show the dry-run, get an explicit "yes", then re-run with --confirm last.
# Content is HTML (same as articles). For Markdown, convert to HTML first.
# Post a NEW answer to a question
python3 "$BLOG" answer-question --question <qid> --content-file ans.html # dry-run
python3 "$BLOG" answer-question --question <qid> --content-file ans.html --draft-only --confirm # PRIVATE draft (safe)
python3 "$BLOG" answer-question --question <qid> --content-file ans.html --confirm # PUBLIC, goes live
# Edit an EXISTING answer (replaces its live, public content)
python3 "$BLOG" edit-answer --id <answer-id> --content-file ans.html # dry-run
python3 "$BLOG" edit-answer --id <answer-id> --content-file ans.html --confirm # overwrites live answer
- One answer per question. Zhihu allows a single answer per user per
question. If the user already answered, answer-question --confirm returns a clear error telling you to use edit-answer instead — find the existing answer id with answers or question <qid> (which reports myanswerid).
--draft-only is the safe path for new answers — it saves a private
draft on the question (nothing public). The user reviews it on Zhihu, then you re-run without --draft-only (with --confirm) to publish. Prefer this unless the user clearly asked to go live immediately.
edit-answer has no private mode — the answer is already public, so any
--confirm edit is live immediately. It preserves the answer's current repost setting unless you override with --repost allowed|disallowed.
- 转载授权 (
--repost allowed|disallowed): new answers default to
disallowed (don't grant repost rights); editing keeps the current setting.
- Images are auto-hosted for answers too — same behavior and
--no-images
flag as publish.
Gotchas — surface before the user is surprised
- This is the user's real Zhihu account. Confirm before any publish / answer /
edit; reading exposes their own private drafts.
- Cookie expiry: Zhihu cookies are short-lived. A
401/403 means
reconnect at studio.acedata.cloud/console/connectors — never loop-retry.
- ToS: cookie automation is against most platforms' terms. This only ever
acts on the user's own account with their own captured cookie; the user owns that risk. Never use it to scrape other people's content at scale.
- Never print
ZHIHU_COOKIES — it is full account access.
- Scope: Zhihu only. Other Chinese platforms (掘金 / CSDN / …) ship as their
own per-platform skills (e.g. csdn, juejin), each with its own connector — not a --platform switch here.
Record the output
After you successfully publish and obtain the live result URL, call the built-in publish_artifact tool ONCE so the user can track this deliverable in My Outputs:
publish_artifact(kind="article", channel="zhihu", title="<title>", url="<the REAL returned URL>", status="delivered")
Use the real returned URL — never fabricate one. Call it once per published item, only after delivery is confirmed; skip it (or use status="failed") if publishing failed. See _shared/artifacts.md.