RedNoteHow-to

Export a RedNote (Xiaohongshu) account's notes to CSV with Python

A tested Python script that exports a RedNote profile's notes, with likes, saves, comments and shares, to CSV. 200 of Luckin Coffee's notes in 16 seconds.

5 min read

Short answer

  • RedNote's open platform homepage lists no API for profiles or notes (it covers shop tools), so this script reads public profiles through our RedNote API.
  • Run on Luckin Coffee's official account: 1,263,918 followers, and its latest 200 notes exported to CSV in 11 calls and 16 seconds.
  • 15 credits a call, 20 notes per call: 1,000 notes cost $0.76 to $1.45.
On this page

This script exports a RedNote (Xiaohongshu, 小红书) account's notes to CSV: date, title, type, likes, saves, comments and shares for each note, plus the account's follower count. We ran it on 2026-10-12 on Luckin Coffee's official account, 瑞幸咖啡, and exported its 200 most recent notes in 16 seconds.

Which API to call

RedNote's open platform at open.xiaohongshu.com is for shop tools such as ERP and order printing, and its homepage lists no API for profiles or notes (what it covers). We make ToolzerHub; the script calls our RedNote API, which returns public profiles and their notes as JSON.

What you need

  • Python 3 and requests (pip install requests).
  • An API key from the dashboard. Each call is 15 credits, so the free tier's 100 credits a month cover the profile call and 5 pages, 100 notes.
  • The account's profile ID, or its profile URL. To find an ID by name, search profiles:
bash
curl "https://api.toolzerhub.com/v2/rednote/search/profiles?query=瑞幸咖啡" \
  -H "x-api-key: $TOOLZERHUB_API_KEY"

The first of 20 results was the verified 瑞幸咖啡 account, "id": "5bac7d93168a6000019384c5", with "Fans 1.3m". A second verified account, 瑞幸咖啡美国 ("Fans 4.2k"), came next, so check the follower count as well as red_official_verified before you copy an ID.

The script

python
import csv
import os
import sys
import time
from datetime import datetime, timezone

import requests

BASE = "https://api.toolzerhub.com/v2/rednote/profiles"
HEADERS = {"x-api-key": os.environ["TOOLZERHUB_API_KEY"]}
FIELDS = ["note_id", "posted", "title", "type", "pinned", "likes", "saves", "comments",
          "shares"]


def cell(value):
    # Spreadsheets run text that starts with = + - or @ as a formula; an apostrophe keeps it text.
    if isinstance(value, str) and value[:1] in ("=", "+", "-", "@", "\t", "\r"):
        return "'" + value
    return value


def get(path, params, tries=5):
    # 429 and 503 are retryable; the error body says how long to wait.
    for attempt in range(tries):
        r = requests.get(f"{BASE}/{path}", headers=HEADERS, params=params, timeout=90)
        if r.status_code not in (429, 503) or attempt == tries - 1:
            r.raise_for_status()
            return r.json()["data"]
        wait = r.json().get("error", {}).get("details", {}).get("retryAfter", 5 * (attempt + 1))
        time.sleep(min(wait, 60))


def row(note):
    return {
        "note_id": note["id"],
        "posted": datetime.fromtimestamp(note["create_time"], timezone.utc).date().isoformat(),
        "title": note.get("title") or note.get("desc", "")[:80],
        "type": note.get("type"),
        "pinned": note.get("sticky", False),
        "likes": note.get("likes"),
        "saves": note.get("collected_count"),
        "comments": note.get("comments_count"),
        "shares": note.get("share_count"),
    }


def notes(profile, max_pages):
    cursor = None
    for _ in range(max_pages):
        params = {"profile_id_or_url": profile}
        if cursor:
            params["cursor"] = cursor
        data = get("posts", params)
        yield from data["notes"]
        cursor = data.get("next_cursor")
        if not data.get("has_more") or not cursor:
            break


def main(profile, max_pages=10, out="profile_notes.csv"):
    p = get("profile", {"profile_id_or_url": profile})
    print(f"{p['nickname']}: {p['fans']} followers, {p['ndiscovery']} notes, "
          f"{p['liked']} likes and {p['collected']} saves received")
    seen = set()
    with open(out, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        writer.writeheader()
        for note in notes(profile, int(max_pages)):
            if note["id"] not in seen:
                seen.add(note["id"])
                writer.writerow({k: cell(v) for k, v in row(note).items()})
    print(f"wrote {len(seen)} notes to {out}")


if __name__ == "__main__":
    main(*sys.argv[1:])

The script makes one profile call, then pages through the notes 20 at a time with next_cursor, stopping at max_pages. Pinned notes come first (pinned is True), then the rest newest first. ndiscovery is the field we read as the account's note count; our reference does not document it, so treat it as approximate.

cell() puts an apostrophe in front of any text that starts with =, +, - or @, so a spreadsheet shows it as text instead of running it as a formula.

Run the script

bash
export TOOLZERHUB_API_KEY=your_key
python rednote_profile.py 5bac7d93168a6000019384c5 10 luckin.csv

Output on 2026-10-12:

text
瑞幸咖啡: 1263918 followers, 3694 notes, 8276854 likes and 1414420 saves received
wrote 200 notes to luckin.csv

That took 16.1 seconds and 11 calls: 1 profile call and 10 pages. The account's five most-liked notes in the export. Titles are in Chinese, with emoji removed; the numbers are what to compare.

posted type likes saves comments title
2026-07-27 image 51,502 1,952 5,729 我们赢了!
2026-08-06 video 51,262 8,023 1,377 不同年代下闯祸be like:
2026-07-29 image 24,942 859 3,225 妙生笛
2026-08-10 image 23,286 1,486 3,100 恭喜最佳男主角易烊千玺
2026-08-10 image 19,785 552 1,637 再次幸会,最佳男主角!

What 200 notes from one brand account showed

The account posts steadily, and a few notes sit far above the median.

  • The 200 notes covered 2026-07-13 to 2026-10-12, so the account posted about two notes a day. At 20 notes a page, all 3,694 would be about 185 pages; we ran 10 and did not test paging to the end.
  • 125 were image notes (type is normal) and 75 were videos. The median video had 1,909 likes and the median image note 1,354.
  • The median note had 1,513 likes, 336 comments and 170 saves.
  • Comments outnumbered likes on 16 notes. The most-commented was a back-to-school "surprise box" note (开学瑞一杯, 惊喜盒子限时掉落): 20,482 comments, 17,744 likes. For a brand, rows like these mark the posts that drew replies, which you can then pull with /v2/rednote/posts/comments.

The image-versus-video gap and the posting rate describe one account over three months, not RedNote in general.

Export several accounts or more history

  • A competitor set: run the script once per account and add an account column, or loop over several IDs.
  • More history: raise max_pages. About 185 pages for the whole Luckin account would be roughly 2,790 credits.
  • A profile URL works in place of the ID, since the parameter is profile_id_or_url. We ran the ID only.

What it costs

Each call is 15 credits. This run was 11 calls, 165 credits. 1,000 notes take 50 pages plus the profile call, 765 credits: $0.76 to $1.45 at our rates of $0.99 to $1.90 per 1,000 credits.

We are not affiliated with RedNote or Luckin Coffee.

Sources

Get this data from your own code

The RedNote API returns it as JSON with one x-api-key header. 100 free credits monthly.