xhs
A command line for Xiaohongshu.
xhs is a single pure-Go binary. It reads public data from xiaohongshu.com over
plain HTTPS, shapes the responses into clean records, and pipes into the rest of
your tools. No paid API key, nothing to run alongside it. It signs its own
requests and bootstraps an anonymous web session the way a browser does.
Install
go install github.com/tamnd/xiaohongshu-cli/cmd/xhs@latest
Or grab a prebuilt binary from the releases, or run
the container image:
docker run --rm ghcr.io/tamnd/xhs:latest --help
Usage
When stdout is a pipe, xhs prints JSONL, one record per line, so a run feeds
jq, awk, or another xhs command with no flags. When stdout is a terminal it
prints a compact table. Pick a format yourself with -o.
# open a note (the xsec_token comes from a listing or a share URL)
xhs note 6849c2f0000000001e034c8e --token <xsec_token>
xhs note 'https://www.xiaohongshu.com/explore/<id>?xsec_token=<t>&xsec_source=pc_feed'
# a creator profile, or the creator's notes
xhs user 5ff0e6500000000001008400
xhs user <id> --notes -n 50
# search notes or users
xhs search 'latte art' -n 40
xhs search 'travel japan' --users
# a note's comments, optionally with replies
xhs comments <note-id> --token <t> --deep -n 100
# the recommendation homefeed
xhs feed --category food -n 40
xhs feed --list
# topics, related notes, autocomplete
xhs tag coffee
xhs related <note-id> --token <t>
xhs suggest cof
# parse ids, urls, and tokens out of any link
xhs id 'https://www.xiaohongshu.com/explore/<id>?xsec_token=<t>'
Pipe one command into the next. Every command that prints notes can emit just the
URL with -o url, and the next command reads ids from stdin with -:
xhs search coffee -o url | xhs note -
xhs search coffee -n 100 | xhs crawl - --out ./data --comments
Output
-o table|json|jsonl|csv|tsv|yaml|url|raw picks the format. --fields a,b,c
keeps and orders columns. --template '{{.note_id}} {{.title}}' renders each
record with Go text/template. -n caps the record count. --raw prints each
record as pretty JSON.
How it reads data
Xiaohongshu serves each page in two ways. The server renders the page once with
the data already embedded in a window.__INITIAL_STATE__ script, and the browser
then keeps the page fresh over a signed JSON API. The signed API refuses
anonymous callers with a login error, so xhs reads the server-rendered state
first and only falls back to the signed API when you give it a logged-in cookie.
What that means per surface, with no cookie:
- note and feed read the server-rendered page and work anonymously from
any IP, including servers and CI. These are the reliable surfaces.
- user, user --notes, and related also read the server-rendered
profile page. Xiaohongshu rate-limits that page hard per IP: a cold IP serves
it, then it redirects to login for a cooldown window. They work on a fresh IP
and at a slow pace, and need a cookie for sustained crawling.
- comments, search, suggest, tag, and me are only ever loaded
over the signed JSON API, so they need a logged-in cookie.
So:
- Opening a note needs an
xsec_token. You get one from the feed, a listing, a
search result, or a share URL; it travels with the note and xhs id pulls it
out. The feed command is the easiest anonymous source of notes and tokens.
- Run it at a polite pace. The default
--rate is 600ms; raise it with
--rate 2s when you walk many profiles.
- For the signed surfaces, or to crawl profiles without hitting the wall, pass a
real cookie:
xhs me --cookie 'web_session=...; a1=...'
export XHS_COOKIE='web_session=...; a1=...'
The anonymous session (the a1 cookie) is bootstrapped on first use and cached
under your config dir. Inspect or reset it with xhs session show and
xhs session forget.
Crawling
xhs crawl is the scraping engine. It seeds a frontier from the explore feed and
from any note ids you pass, then walks outward breadth-first: each note reaches
its author, the author's other notes, and its related notes. Every record kind
streams to its own JSONL file (notes.jsonl, users.jsonl, comments.jsonl) as
it is found, so a long crawl leaves usable output even if it stops early. Notes
and users are de-duplicated, and --depth and --max bound the walk.
# seed from the explore feed and walk two hops, capped at 500 notes
xhs crawl --explore --depth 2 --max 500 --out ./data
# crawl a category, following each author's other notes
xhs crawl --category food --author-notes --out ./food
# crawl specific notes with their comments and related notes
xhs crawl <note-id> --token <t> --related --comments --out ./data
# pipe ids in from another command
xhs search coffee -o url | xhs crawl - --out ./data
Configuration
Flags win over environment variables, which win over defaults.
| Variable |
Meaning |
XHS_COOKIE |
cookie header for gated surfaces |
XHS_COOKIE_FILE |
path to a cookie file (header or Netscape format) |
XHS_PROXY |
HTTP or SOCKS proxy URL |
XHS_USER_AGENT |
override the default desktop UA |
XHS_OUTPUT |
default output format |
XHS_CACHE_DIR |
cache location |
XHS_CONFIG_DIR |
config and session location |
xhs config show prints the resolved settings with the cookie redacted.
xhs cache stat|clear|path manages the on-disk response cache.
Exit codes: 0 success, 3 needs a login, 4 not found, 5 rate-limited or
walled by anti-bot, 6 network error, 1 anything else.
Development
cmd/xhs/ thin main, wires cli.Root into fang and maps exit codes
cli/ the cobra command tree and the output formatter
xiaohongshu/ the library: signed HTTP client, session, and data models
pkg/xhssign/ the request signer (x-s/x-t/x-s-common)
pkg/xhsurl/ the id, url, and xsec_token parser
pkg/xhshtml/ the __INITIAL_STATE__ extractor for server-rendered pages
docs/ tago documentation site
make build # ./bin/xhs
make test # go test ./...
make vet # go vet ./...
Releasing
Push a version tag and GitHub Actions runs GoReleaser, which builds the
archives, Linux packages, the multi-arch GHCR image, checksums, SBOMs, and a
cosign signature:
git tag v0.1.0
git push --tags
The Homebrew and Scoop steps self-disable until their tokens exist, so the first
release works with no extra secrets.
License
Apache-2.0. See LICENSE. The request signer is a clean-room
reimplementation built from observing the public web client; no third-party code
is vendored.