Skip to content

Repository files navigation

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.

About

Read Xiaohongshu notes, user profiles, comments, and feeds as JSONL

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages