xhs-style-kit
小红书内容爬虫 + 风格分析
触发条件
用户说以下任意内容时启动:
- 「帮我爬 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_KEY→OPENAI_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 等),直接执行:
- 使用 Read 工具读取
xhs-{user_id}/output/notes.json全部笔记 - 基于六维度进行深度分析(无需运行任何脚本)
- 将完整报告写入
xhs-{user_id}/output/report.md - 告知用户报告已生成,并给出关键洞察摘要
? 备用方案:运行 analyzer.py(其他环境)
如果当前环境无法直接读取文件分析,使用 Bash 工具运行:
cd xhs-{user_id} && python analyzer.py
analyzer.py 会自动检测并使用可用的 LLM(优先级:ANTHROPIC_API_KEY → OPENAI_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
六维度分析框架
- 标题公式:长度分布、句式结构、Emoji 位置、高频模式、可套用模板
- 开头模式:钩子类型(提问/场景/数据/悬念)、前三行节奏
- 正文结构:段落数量、分隔方式、内容组织框架(清单/故事/对比)
- Emoji & 符号:使用频率、位置偏好、高频列表、特殊符号(|·✨等)
- 标签策略:品牌标签/话题标签/场景标签比例、数量范围、放置位置
- 语气调性:人格化特征、情感倾向、用词特点、与读者关系
报告格式(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 文件,供后续直接调用来生成符合该账号风格的小红书笔记。
执行步骤
- 综合
report.md的六维度分析结论,提炼品牌专属写作规则(规则要具体、可操作,不能只说"简洁",要说"标题不超过20字") - 将规则写入
xhs-{user_id}/output/xhs-writer.skill.md(符合 Claude Code SKILL 标准格式) - 告知用户:
✅ 文案写作 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/)