xhs-style-kit

SKILL.md

小红书内容爬虫 + 风格分析

触发条件

用户说以下任意内容时启动:

  • 「帮我爬 XX 品牌的小红书」
  • 「分析 @某账号 的内容风格」
  • 「获取某小红书博主的笔记数据」
  • 「我想做小红书竞品分析」

Step 1:信息确认

启动后先问用户两个问题,得到答案再继续:

我需要以下信息来开始:

1. 账号主页 URL(格式:https://www.xiaohongshu.com/user/profile/...)
   ? 打开小红书网页版,进入目标账号主页,复制浏览器地址栏的 URL 即可

2. 爬取多少篇笔记?(默认 50 篇,建议 30-100 篇)

Step 2:生成项目文件

从 URL 提取 user_id(/user/profile/ 后面的字符串),在当前工作目录创建 xhs-{user_id}/,生成以下四个文件:

? requirements.txt

playwright==1.49.0
# 以下为 analyzer.py 可选依赖(按需安装):
# anthropic   —— 使用 Claude API 分析
# openai      —— 使用 OpenAI / 兼容 API 分析(Kimi、DeepSeek 等)
# requests    —— 使用本地 Ollama 分析

? .gitignore

cookies.json
output/
__pycache__/
*.pyc

? scraper.py

#!/usr/bin/env python3
"""
小红书账号笔记爬虫

用法:
    python scraper.py --url https://www.xiaohongshu.com/user/profile/<user_id>
    python scraper.py --url https://www.xiaohongshu.com/user/profile/<user_id> --count 80
"""
import argparse
import asyncio
import json
import re
import sys
import time
from datetime import datetime, timezone
from pathlib import Path

try:
    from playwright.async_api import async_playwright
except ImportError:
    print("❌ 缺少依赖,请先运行:")
    print("   pip install -r requirements.txt")
    print("   playwright install chromium")
    sys.exit(1)

OUTPUT_DIR    = Path("output")
NOTES_FILE    = OUTPUT_DIR / "notes.json"
COOKIES_FILE  = Path("cookies.json")
LOGIN_URL     = "https://www.xiaohongshu.com"
API_PATTERN   = "/api/sns/web/v1/feed"
REQUEST_DELAY = 2.0
SAVE_EVERY    = 10

USER_AGENT = (
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
    "AppleWebKit/537.36 (KHTML, like Gecko) "
    "Chrome/120.0.0.0 Safari/537.36"
)

def extract_user_id(raw: str) -> str:
    m = re.search(r"/user/profile/([a-f0-9]+)", raw)
    if m:
        return m.group(1)
    if re.match(r"^[a-f0-9]{24}$", raw.strip()):
        return raw.strip()
    raise ValueError(f"无法识别 '{raw}',请粘贴完整的账号主页 URL")

def load_existing() -> tuple[list, set]:
    if NOTES_FILE.exists():
        notes = json.loads(NOTES_FILE.read_text(encoding="utf-8"))
        return notes, {n["id"] for n in notes}
    return [], set()

def save(notes: list):
    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    NOTES_FILE.write_text(json.dumps(notes, ensure_ascii=False, indent=2), encoding="utf-8")

def parse_note(raw: dict) -> dict | None:
    note_id   = raw.get("id") or raw.get("note_id", "")
    note_card = raw.get("note_card") or raw
    if not note_id:
        return None

    title   = (note_card.get("title") or "").strip()
    content = (note_card.get("desc")  or "").strip()
    tags    = [t.get("name", "") for t in note_card.get("tag_list", []) if t.get("name")]

    interact      = note_card.get("interact_info") or {}
    like_count    = int(interact.get("liked_count",     0) or 0)
    collect_count = int(interact.get("collected_count", 0) or 0)
    comment_count = int(interact.get("comment_count",   0) or 0)

    images    = note_card.get("image_list") or []
    cover_url = images[0].get("url_default", "") if images else ""

    note_type = "normal"
    video_url = None
    video_info = note_card.get("video") or {}
    if video_info:
        note_type = "video"
        streams = (video_info.get("media") or {}).get("stream") or {}
        for quality in ("h264_0", "h264_1", "h264_2"):
            stream_list = streams.get(quality) or []
            if stream_list:
                video_url = (
                    stream_list[0].get("master_url")
                    or (stream_list[0].get("backup_urls") or [""])[0]
                )
                break

    return {
        "id": note_id, "title": title, "content": content, "tags": tags,
        "like_count": like_count, "collect_count": collect_count, "comment_count": comment_count,
        "note_type": note_type, "cover_url": cover_url, "video_url": video_url,
        "scraped_at": datetime.now(timezone.utc).isoformat(),
    }

async def _new_context(browser):
    return await browser.new_context(user_agent=USER_AGENT, viewport={"width": 1280, "height": 800})

async def login(browser):
    print("\n? 即将打开小红书登录页,请用手机扫码...")
    context = await _new_context(browser)
    page    = await context.new_page()
    await page.goto(LOGIN_URL, wait_until="load", timeout=60000)
    await page.wait_for_timeout(2000)

    clicked = False
    for sel in ["text=登录", "[class*='login']", "button:has-text('登录')", "a:has-text('登录')"]:
        try:
            btn = page.locator(sel).first
            if await btn.is_visible(timeout=1500):
                await btn.click()
                await page.wait_for_timeout(1500)
                clicked = True
                break
        except Exception:
            continue

    if not clicked:
        print("   ℹ️  请在浏览器中手动点击「登录」按钮")

    print("   ✅ 浏览器已打开,请扫描二维码")
    print("   ⏳ 等待登录(最多 120 秒)...")

    deadline = time.time() + 120
    while time.time() < deadline:
        cookies = await context.cookies()
        if any(c["name"] == "web_session" for c in cookies):
            print("   ✅ 登录成功!\n")
            COOKIES_FILE.write_text(json.dumps(cookies, ensure_ascii=False, indent=2), encoding="utf-8")
            return context
        await asyncio.sleep(2)

    raise TimeoutError("登录超时(120 秒),请重新运行")

async def load_session(browser):
    if not COOKIES_FILE.exists():
        return None
    cookies = json.loads(COOKIES_FILE.read_text(encoding="utf-8"))
    if not any(c["name"] == "web_session" for c in cookies):
        return None
    context = await _new_context(browser)
    await context.add_cookies(cookies)
    print("   ✅ 已加载登录状态(无需扫码)\n")
    return context

async def _get_cards(page):
    """获取笔记卡片,捕获 SPA 路由导致的 context 销毁异常"""
    try:
        cards = await page.query_selector_all("section.note-item")
        if not cards:
            cards = await page.query_selector_all(".note-item")
        return cards
    except Exception:
        return None  # None = context 被销毁,调用方需等待页面稳定

async def scrape(context, user_id: str, target_count: int):
    profile_url = f"https://www.xiaohongshu.com/user/profile/{user_id}"
    notes, seen_ids = load_existing()
    print(f"? 已有 {len(notes)} 篇,目标 {target_count} 篇\n")
    if len(notes) >= target_count:
        print("✅ 已达目标,无需继续")
        return notes

    # 优先复用登录后的已有页面,避免被 XHS 检测为 bot(新 tab 直接访问 profile 会被重定向到登录页)
    existing = context.pages
    page = existing[-1] if existing else await context.new_page()
    captured: asyncio.Queue = asyncio.Queue()

    async def on_response(response):
        if API_PATTERN in response.url and response.status == 200:
            try:
                data = await response.json()
                await captured.put(data)
            except Exception:
                pass

    page.on("response", on_response)
    print(f"? 打开账号主页:{profile_url}")
    await page.goto(profile_url, wait_until="load", timeout=60000)

    # 等待 Vue SPA 完成客户端渲染(load 事件后仍需时间)
    await page.wait_for_timeout(5000)

    # 关闭弹窗
    for selector in ["[class*='close']", "button.close"]:
        try:
            btn = page.locator(selector).first
            if await btn.is_visible(timeout=1000):
                await btn.click(force=True)
                await page.wait_for_timeout(1000)
                break
        except Exception:
            pass

    # 点击「笔记」tab,并等待 SPA 路由导航完成
    for tab_text in ["笔记", "全部"]:
        try:
            tab = page.locator(f"text={tab_text}").first
            if await tab.is_visible(timeout=2000):
                await tab.click()
                try:
                    await page.wait_for_load_state("load", timeout=8000)
                except Exception:
                    pass
                await page.wait_for_timeout(3000)
                break
        except Exception:
            pass

    stall_count = 0
    while len(notes) < target_count:
        cards = await _get_cards(page)

        if cards is None:
            # SPA 路由导航中,等待页面稳定后重试
            print("   ⏳ 页面导航中,等待稳定...")
            await page.wait_for_timeout(3000)
            try:
                await page.wait_for_load_state("load", timeout=8000)
            except Exception:
                pass
            await page.wait_for_timeout(2000)
            stall_count += 1
            if stall_count >= 5:
                print("⚠️  页面反复导航,停止")
                break
            continue

        if not cards:
            await page.evaluate("window.scrollBy(0, 500)")
            await page.wait_for_timeout(1500)
            stall_count += 1
            if stall_count >= 6:
                print("⚠️  已到底部或无法加载更多,停止")
                break
            continue

        stall_count = 0
        new_this_round = 0

        for i in range(len(cards)):
            if len(notes) >= target_count:
                break

            cards_now = await _get_cards(page)
            if cards_now is None or i >= len(cards_now):
                break
            card = cards_now[i]

            try:
                await card.click(force=True)
                await page.wait_for_timeout(1200)
                try:
                    data = await asyncio.wait_for(captured.get(), timeout=3.0)
                    items = (data.get("data") or {}).get("items") or []
                    for item in items:
                        note = parse_note(item)
                        if note and note["id"] not in seen_ids:
                            seen_ids.add(note["id"])
                            notes.append(note)
                            new_this_round += 1
                            icon = "?" if note["note_type"] == "video" else "?"
                            title_preview = note["title"][:28] or note["content"][:28] or "(无标题)"
                            print(f"  {icon} [{len(notes)}/{target_count}] {title_preview}")
                            if len(notes) % SAVE_EVERY == 0:
                                save(notes)
                                print(f"     → 自动保存 {len(notes)} 篇\n")
                except asyncio.TimeoutError:
                    pass
                await page.go_back()
                await page.wait_for_timeout(REQUEST_DELAY * 1000)
            except Exception:
                try:
                    await page.go_back()
                    await page.wait_for_timeout(1000)
                except Exception:
                    pass

        if new_this_round == 0:
            await page.evaluate("window.scrollBy(0, 700)")
            await page.wait_for_timeout(1800)

    save(notes)
    print(f"\n✅ 完成!共 {len(notes)} 篇 → {NOTES_FILE}")
    return notes

async def main():
    parser = argparse.ArgumentParser(description="小红书账号笔记爬虫")
    parser.add_argument("--url",   required=True, help="账号主页 URL 或 user_id")
    parser.add_argument("--count", type=int, default=50, help="目标笔记数(默认 50)")
    args = parser.parse_args()
    try:
        user_id = extract_user_id(args.url)
        print(f"? 账号 ID:{user_id}")
    except ValueError as e:
        print(f"❌ {e}")
        sys.exit(1)

    async with async_playwright() as pw:
        browser = await pw.chromium.launch(headless=False)
        context = await load_session(browser)
        if context is None:
            context = await login(browser)
        try:
            await scrape(context, user_id, args.count)
        finally:
            await browser.close()

if __name__ == "__main__":
    asyncio.run(main())

? analyzer.py

用途:当 AI Agent 无法直接读取文件分析时(非 Claude/OpenClaw 等环境),作为独立脚本调用 LLM API 完成分析。 优先级:ANTHROPIC_API_KEYOPENAI_API_KEY → 本地 Ollama

#!/usr/bin/env python3
"""
小红书笔记风格分析器(独立脚本,适用于非 AI Agent 环境)

用法:
    python analyzer.py                        # 自动检测可用 LLM
    python analyzer.py --model claude         # 强制使用 Claude
    python analyzer.py --model openai         # 强制使用 OpenAI
    python analyzer.py --model ollama         # 强制使用本地 Ollama

环境变量:
    ANTHROPIC_API_KEY    使用 Claude(优先)
    OPENAI_API_KEY       使用 OpenAI / 兼容 API
    OPENAI_BASE_URL      自定义 API 地址(Kimi、DeepSeek、混元等)
    OPENAI_MODEL         自定义模型名(默认 gpt-4o)
"""
import argparse
import json
import os
import sys
from datetime import datetime
from pathlib import Path

NOTES_FILE  = Path("output/notes.json")
REPORT_FILE = Path("output/report.md")

ANALYSIS_PROMPT = """\
你是一位专业的小红书内容策略分析师。请基于以下 {n} 篇笔记数据,从六个维度进行深度分析,生成完整的内容风格分析报告。

笔记数据(JSON 格式):
{notes_json}

请输出以下格式的完整 Markdown 报告:

# 小红书内容风格分析报告

> 数据来源:{n} 篇笔记 | 分析时间:{date}

## 数据概况
| 指标 | 数值 |
|------|------|
| 总笔记数 | |
| 图文笔记 | |
| 视频笔记 | |
| 平均点赞 | |
| 平均收藏 | |
| 平均评论 | |

## 1. 标题公式
### 核心规律
### 高频结构(含占比)
### 禁忌

## 2. 开头模式
(分析前三行的钩子类型:提问/场景/数据/悬念/共情,给出典型示例)

## 3. 正文结构
(段落数量、分隔符使用、列表 vs 段落、内容组织逻辑)

## 4. Emoji & 符号使用
(使用频率、位置偏好、高频 Emoji 列表、特殊符号语义)

## 5. 标签策略
(品牌/话题/热门标签比例,数量范围,放置位置)

## 6. 语气调性
(人格化特征、情感倾向、用词特点、与读者的关系定位)

---

## 附:可套用写作模板

### 模板 A:[最常见类型]
**标题公式**:
**示例**:
**开头钩子**:
**正文框架**:
**标签推荐**:

### 模板 B:[第二类型]
**标题公式**:
**示例**:
**开头钩子**:
**正文框架**:
**标签推荐**:

### 模板 C:[第三类型]
**标题公式**:
**示例**:
**开头钩子**:
**正文框架**:
**标签推荐**:
"""


def load_notes(notes_path: str) -> list:
    path = Path(notes_path)
    if not path.exists():
        print(f"❌ 未找到笔记文件:{path},请先运行 scraper.py")
        sys.exit(1)
    notes = json.loads(path.read_text(encoding="utf-8"))
    if not notes:
        print("❌ 笔记文件为空,请先完成爬取")
        sys.exit(1)
    return notes


def build_prompt(notes: list) -> str:
    sample = notes[:80]  # 避免超出 context window
    return ANALYSIS_PROMPT.format(
        notes_json=json.dumps(sample, ensure_ascii=False, indent=2),
        n=len(notes),
        date=datetime.now().strftime("%Y-%m-%d"),
    )


def analyze_with_claude(prompt: str) -> str:
    try:
        import anthropic
    except ImportError:
        print("❌ 缺少依赖:pip install anthropic")
        sys.exit(1)
    api_key = os.environ.get("ANTHROPIC_API_KEY")
    if not api_key:
        print("❌ 未设置 ANTHROPIC_API_KEY")
        sys.exit(1)
    print("? 使用 Claude 分析中...")
    client = anthropic.Anthropic(api_key=api_key)
    msg = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=4096,
        messages=[{"role": "user", "content": prompt}],
    )
    return msg.content[0].text


def analyze_with_openai(prompt: str) -> str:
    try:
        from openai import OpenAI
    except ImportError:
        print("❌ 缺少依赖:pip install openai")
        sys.exit(1)
    api_key  = os.environ.get("OPENAI_API_KEY")
    if not api_key:
        print("❌ 未设置 OPENAI_API_KEY")
        sys.exit(1)
    base_url = os.environ.get("OPENAI_BASE_URL")
    model    = os.environ.get("OPENAI_MODEL", "gpt-4o")
    print(f"? 使用 OpenAI 兼容 API ({model}) 分析中...")
    client = OpenAI(api_key=api_key, base_url=base_url or None)
    resp = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
        max_tokens=4096,
    )
    return resp.choices[0].message.content


def analyze_with_ollama(prompt: str, model: str) -> str:
    try:
        import requests
    except ImportError:
        print("❌ 缺少依赖:pip install requests")
        sys.exit(1)
    print(f"? 使用 Ollama ({model}) 分析中...")
    try:
        resp = requests.post(
            "http://localhost:11434/api/generate",
            json={"model": model, "prompt": prompt, "stream": False},
            timeout=300,
        )
        resp.raise_for_status()
        return resp.json()["response"]
    except Exception as e:
        print(f"❌ Ollama 调用失败:{e}")
        print("   请确认服务已启动:ollama serve")
        sys.exit(1)


def detect_model() -> str | None:
    """按优先级自动检测可用 LLM:Claude → OpenAI → Ollama"""
    if os.environ.get("ANTHROPIC_API_KEY"):
        return "claude"
    if os.environ.get("OPENAI_API_KEY"):
        return "openai"
    try:
        import requests
        r = requests.get("http://localhost:11434/api/tags", timeout=2)
        if r.status_code == 200:
            return "ollama"
    except Exception:
        pass
    return None


def main():
    parser = argparse.ArgumentParser(description="小红书笔记风格分析器")
    parser.add_argument("--notes",        default=str(NOTES_FILE),  help="笔记 JSON 文件路径")
    parser.add_argument("--output",       default=str(REPORT_FILE), help="报告输出路径")
    parser.add_argument(
        "--model",
        choices=["auto", "claude", "openai", "ollama"],
        default="auto",
        help="指定 LLM(默认 auto)",
    )
    parser.add_argument("--ollama-model", default="qwen2.5:14b", help="Ollama 模型名")
    args = parser.parse_args()

    notes  = load_notes(args.notes)
    prompt = build_prompt(notes)
    print(f"? 已加载 {len(notes)} 篇笔记")

    model = args.model
    if model == "auto":
        model = detect_model()
        if not model:
            print("❌ 未检测到可用 AI 服务,请设置以下任一环境变量:")
            print("   export ANTHROPIC_API_KEY=sk-ant-...")
            print("   export OPENAI_API_KEY=sk-...")
            print("   或启动本地 Ollama:ollama serve && ollama pull qwen2.5:14b")
            sys.exit(1)
        print(f"   ✅ 自动选择:{model}")

    if   model == "claude": report = analyze_with_claude(prompt)
    elif model == "openai": report = analyze_with_openai(prompt)
    elif model == "ollama": report = analyze_with_ollama(prompt, args.ollama_model)

    output = Path(args.output)
    output.parent.mkdir(parents=True, exist_ok=True)
    output.write_text(report, encoding="utf-8")
    print(f"\n✅ 报告已生成 → {output}")


if __name__ == "__main__":
    main()

Step 3:AI Agent 自动安装依赖

AI Agent 执行:使用 Bash 工具在项目目录下安装依赖,用户无需操作。

cd xhs-{user_id} && pip install -r requirements.txt -q && playwright install chromium

安装完成后告知用户。


Step 4:AI Agent 自动运行爬虫

AI Agent 执行:使用 Bash 工具运行爬虫,设置超时为 600000ms(10 分钟)。 运行前告知用户:「浏览器即将弹出,请扫描二维码登录小红书。登录后爬虫自动开始,无需其他操作。」

cd xhs-{user_id} && python scraper.py --url {完整URL} --count {N}
  • 首次运行:浏览器弹出,等待用户扫码(最多 120 秒)
  • 再次运行:自动加载已保存登录状态,无需扫码
  • 爬取期间终端实时显示进度
  • 完成后自动进入 Step 5

Step 5:分析并生成报告

爬虫完成后,按以下优先级选择分析方案

✅ 优先方案:AI Agent 直接分析(Claude、OpenClaw 等)

如果你是具备文件读取和语言分析能力的 AI Agent(Claude Code、OpenClaw 等),直接执行:

  1. 使用 Read 工具读取 xhs-{user_id}/output/notes.json 全部笔记
  2. 基于六维度进行深度分析(无需运行任何脚本)
  3. 将完整报告写入 xhs-{user_id}/output/report.md
  4. 告知用户报告已生成,并给出关键洞察摘要

? 备用方案:运行 analyzer.py(其他环境)

如果当前环境无法直接读取文件分析,使用 Bash 工具运行:

cd xhs-{user_id} && python analyzer.py

analyzer.py 会自动检测并使用可用的 LLM(优先级:ANTHROPIC_API_KEYOPENAI_API_KEY → 本地 Ollama)。

也可指定模型:

python analyzer.py --model claude    # 使用 Claude API
python analyzer.py --model openai    # 使用 OpenAI / 兼容 API(Kimi、DeepSeek 等)
python analyzer.py --model ollama    # 使用本地 Ollama

六维度分析框架

  1. 标题公式:长度分布、句式结构、Emoji 位置、高频模式、可套用模板
  2. 开头模式:钩子类型(提问/场景/数据/悬念)、前三行节奏
  3. 正文结构:段落数量、分隔方式、内容组织框架(清单/故事/对比)
  4. Emoji & 符号:使用频率、位置偏好、高频列表、特殊符号(|·✨等)
  5. 标签策略:品牌标签/话题标签/场景标签比例、数量范围、放置位置
  6. 语气调性:人格化特征、情感倾向、用词特点、与读者关系

报告格式(output/report.md

# @{账号名} 小红书内容风格分析报告

> 数据来源:{n} 篇笔记 | 分析时间:{date}

## 数据概况
| 指标 | 数值 |
|------|------|
| 总笔记数 | N |
| 图文笔记 | N |
| 视频笔记 | N |
| 平均点赞 | N |
| 平均收藏 | N |
| 平均评论 | N |

## 1. 标题公式
### 核心规律
### 高频结构(含占比)
### 禁忌

## 2. 开头模式

## 3. 正文结构

## 4. Emoji & 符号使用

## 5. 标签策略

## 6. 语气调性

---

## 附:可套用写作模板

### 模板 A:[最常见类型]
**标题公式****示例****开头****正文框架****标签推荐**
### 模板 B:[第二类型]

### 模板 C:[第三类型]

Step 6:生成文案风格写作 SKILL

报告完成后,基于六维度分析结论自动生成一个可复用的文案写作 SKILL 文件,供后续直接调用来生成符合该账号风格的小红书笔记。

执行步骤

  1. 综合 report.md 的六维度分析结论,提炼品牌专属写作规则(规则要具体、可操作,不能只说"简洁",要说"标题不超过20字")
  2. 将规则写入 xhs-{user_id}/output/xhs-writer.skill.md(符合 Claude Code SKILL 标准格式)
  3. 告知用户:
    ✅ 文案写作 SKILL 已生成 → output/xhs-writer.skill.md
    
    安装方法(二选一):
    mkdir -p ~/.claude/skills/xhs-{user_id}-writer
    cp output/xhs-writer.skill.md ~/.claude/skills/xhs-{user_id}-writer/SKILL.md
    
    安装后在任意项目中输入:「帮我写一篇 @{账号名} 风格的小红书」即可调用
    

? 投产前请人工 Review SKILL

生成的写作 SKILL 是基于数据分析的自动推断,可能存在偏差。 正式使用前请打开 output/xhs-writer.skill.md,按实际情况核查并调整:

  • 标题公式 —— 模板是否真实反映该账号的标题规律和字数习惯?
  • 语气调性 —— 品牌人格描述是否准确?有无夸大/遗漏?
  • Emoji 规范 —— 高频 Emoji 清单和位置规则是否与实际一致?
  • 标签策略 —— 必带标签、推荐标签是否符合真实发布习惯?
  • DO / DON'T —— 有没有误判为禁忌的常用表达,或遗漏重要规则?

确认无误后,SKILL 即可投入生产使用。

生成文件路径

xhs-{user_id}/output/xhs-writer.skill.md

生成的 SKILL 内容结构

重要:所有风格规则必须将分析结论内嵌到 SKILL 正文中,而非引用外部文件。用户在新会话中加载该 SKILL 时不会有 report.md 上下文。

参考高质量 SKILL 范例(xiaohongshu-jnby/SKILL.md),生成的 SKILL 必须包含以下完整结构:

---
name: xhs-{账号名}-writer
description: 为 @{账号名} 生成符合其小红书内容风格的笔记文案(标题 + 正文 + 标签)。基于 {n} 篇真实笔记分析提炼,覆盖[主要内容类型]allowed-tools:
  - Write
---

# @{账号名} 小红书文案生成器

## 触发场景

当用户需要生成以下类型的笔记时使用:
[从 report.md 数据概况提炼,列出 3-5 个具体使用场景]

---

## 品牌调性定义

### 核心人格

> 账号:@{账号名} | 数据来源:{n} 篇真实笔记 | 分析日期:{date}

[从 report.md「6. 语气调性」提炼,生成核心人格维度表格:]

| 维度 | 定义 |
|------|------|
| **情感基调** | [具体描述,如:务实温和,不夸张] |
| **表达风格** | [具体描述] |
| **叙述视角** | [具体描述] |
| **与读者关系** | [具体描述] |

> [从互动数据中提炼 1-2 句量化洞察,如:娱乐类平均互动 XX 赞 vs 产品类 XX 赞]

### 品牌词汇库

[从 report.md「6. 语气调性 - 高频词汇」提炼,按类别分组:]

| 类别 | 高频词汇 |
|------|----------|
| [类别1] | [词汇列表] |
| [类别2] | [词汇列表] |

---

## 标题公式

### 核心数据

- [主要内容类型]标题:**[字数范围]**,[关键特征]
- [次要内容类型]标题:**[字数范围]**,[关键特征]

### [N] 大可复用公式

[从 report.md「1. 标题公式 - 高频结构」提炼,每个公式包含:]

#### 公式 1:[名称](约 X%)

[描述公式逻辑]

| 套用方法 | 标题示例 |
|----------|----------|
| [方法说明] | [真实标题案例] |

#### 公式 2:[名称](约 X%)
...

---

## 开头 / 正文 / 结尾模板

### 开头模式(N 种)

[从 report.md「2. 开头模式」提炼,每种模式包含代码块示例]

#### 模式 A:[名称](约 X%)

[描述]

[真实开头案例]


#### 模式 B:[名称]
...

### 正文结构框架

[从 report.md「3. 正文结构」提炼:字数分布、段落数据、分隔符规范]

**[主要类型]**:

[结构框架代码块]


---

## 按内容类型分模板

### 类型 1:[名称]

> [简短描述,含占比或互动数据]

**标题**:[推荐公式]

**结构模板**:

[完整模板]


**示例**:

[真实案例或仿写示例]


**特点**:
- [3-5 条关键规则]

### 类型 2:[名称]
...

---

## Emoji & 标签规范

### Emoji 使用规则

| 类型 | 使用频率 | 主要位置 | 高频 Emoji |
|------|----------|----------|-----------|
| [类型1] | [频率] | [位置] | [emoji列表] |
| [类型2] | ... | ... | ... |

### 标签规范

**数量**:[范围],平均 [N] 个

**必带标签**:

| 标签 | 使用率 | 说明 |
|------|--------|------|
| [标签] | ~X% | [功能说明] |

**分类标签池**:

| 内容类型 | 推荐标签 |
|----------|----------|
| [类型] | [标签列表] |

---

## 风格 DO / DON'T 规则

### DO(必须做)

- [从 report.md 所有章节提炼,5-10 条可操作规则]

### DON'T(禁止做)

- **❌** [从 report.md「标题公式-禁忌」+「调性」提炼,5-8 条禁忌]

---

## 参考案例

[从 notes.json 中选取 3-4 个最具代表性的真实笔记(含不同类型),格式:]

### 案例 1:[类型] — [标题]

> **标题**:[真实标题]
>
> **正文**:
> ```
> [真实正文内容]
> ```
>
> **互动数据**:[赞/藏/评]
>
> **分析**:[2-3句解析为什么这条笔记有效,提炼可复用的技巧]

### 案例 2:...
### 案例 3:...

---

## 生成流程 & 质量检查清单

### Step 1:判断内容类型

| 用户需求 | 推荐类型 |
|----------|----------|
| [场景描述] | [类型] |

### Step 2:选择标题公式

| 内容类型 | 推荐公式 |
|----------|----------|
| [类型] | [公式名称,按优先级排列] |

### Step 3:撰写正文

1. **选开头模式**:[各类型对应模式说明]
2. **控制字数**:[各类型字数规范]
3. **套用结构框架**:[各类型结构说明]
4. **配置标签**:[标签配置规则]

### Step 4:质量检查

| 检查项 | 标准 | 通过条件 |
|--------|------|----------|
| 标题长度 | [范围] | [具体上限] |
| [类型]正文字数 | [范围] | [具体要求] |
| 调性一致性 | [描述] | [通过标准] |
| Emoji 使用 | [类型对应规范] | [通过标准] |
| 标签数量 | [范围] | 含品牌固定标签 |
| 禁用词 | 调性禁忌清单 | 无违禁表达 |

### Step 5:输出格式

默认输出 1 个方案;用户要求「多个备选」时,每个方案使用不同标题公式和开头钩子,标签池保持一致。

常见错误处理

❌ Cookie 过期 / 登录失效

症状:爬取过程中突然没有数据
解决:Claude 删除 cookies.json 后重新运行爬虫

❌ analyzer.py 无可用 LLM

症状:python analyzer.py 提示未检测到 AI 服务
解决:设置环境变量 ANTHROPIC_API_KEY 或 OPENAI_API_KEY,
      或启动 Ollama:ollama serve && ollama pull qwen2.5:14b

❌ 登录框未自动弹出

症状:浏览器打开了但没显示二维码
解决:用户在浏览器中手动点击「登录」,脚本等待 120 秒

❌ 笔记数量不足

原因:账号发布笔记本身不足目标数,或部分私密
解决:正常情况,已保存所有可获取笔记

❌ 反爬验证码

解决:用户手动完成验证码后,爬虫自动继续(已爬数据已保存)

输出文件说明

xhs-{user_id}/
├── scraper.py              # 爬虫脚本
├── analyzer.py             # 独立分析脚本(非 AI Agent 环境使用)
├── cookies.json            # 登录状态(自动生成,.gitignore 已排除)
└── output/
    ├── notes.json          # 全部笔记结构化数据
    ├── report.md           # 六维度风格分析报告
    └── xhs-writer.skill.md # 文案生成 SKILL(可安装到 ~/.claude/skills/)
Installs
3
First Seen
Apr 2, 2026