下面是一篇可以直接复制到 CSDN 的 Markdown 博文。标题中特意写成“评论数与收藏数”,避免让读者误以为本文会批量抓取评论用户隐私或绕过平台限制。
# TypeScript + Playwright 实战:合规拉取小红书笔记评论数与收藏数 > 本文介绍一种面向“本人账号、本人笔记或已获得明确授权内容”的小红书数据采集方案。 > 不讨论验证码绕过、风控规避、Cookie 窃取、匿名批量爬取等行为。 ## 前言 在多平台内容发布系统中,发布成功并不是流程的终点。 运营人员通常还需要持续查看: - 笔记是否仍然存在; - 当前评论数; - 当前收藏数; - 点赞数; - 指标最后更新时间; - 指标变化趋势。 小红书创作服务平台本身提供面向创作者的数据分析能力,但普通开发者未必能够直接获得一个适用于任意笔记的通用公开接口。 因此,实际项目中通常采用三层降级方案: 1. 优先申请并使用官方授权接口; 2. 无官方接口时,使用用户授权的浏览器登录会话读取本人数据; 3. 无法自动采集时,支持人工导入指标。 本文使用 TypeScript、Playwright、Express 和 SQLite,实现第二种方案,同时预留官方接口适配器。 --- ## 一、整体架构 推荐的数据流如下: ```text 小红书笔记页面 ↓ Playwright 授权浏览器会话 ↓ XhsMetricsCollector ↓ 指标校验与标准化 ↓ platform_metrics 指标快照表 ↓ 前端趋势图、历史列表和 CSV 导出需要特别强调:
浏览器采集器只负责“读取” 指标服务负责“校验与保存” 前端只访问自己的后端接口不要让前端页面直接请求小红书,也不要把登录 Cookie 返回给浏览器前端。
二、为什么使用指标快照,而不是覆盖旧数据
错误做法:
UPDATE publish_results SET like_count = 100, comment_count = 20;这样会丢失历史变化。
更合理的方式是,每次采集新增一条快照:
10:00 收藏 20,评论 5 11:00 收藏 28,评论 8 12:00 收藏 41,评论 13数据表可以设计为:
CREATE TABLE platform_metrics ( id TEXT PRIMARY KEY, publish_result_id TEXT NOT NULL, platform_code TEXT NOT NULL, view_count INTEGER, like_count INTEGER, comment_count INTEGER, favorite_count INTEGER, share_count INTEGER, data_source TEXT NOT NULL, collected_at INTEGER NOT NULL, created_at INTEGER NOT NULL );其中:
comment_count 表示评论数 favorite_count 表示收藏数 data_source 表示数据来源 collected_at 表示实际采集时间建议的数据来源枚举:
type DataSource = | 'REAL_API' | 'BROWSER' | 'MANUAL_IMPORT' | 'MOCK';不要把模拟数据标记成REAL_API或BROWSER。
三、安装 Playwright
pnpm add playwright zod首次运行还需要安装浏览器:
pnpm exec playwright install chromium项目建议使用 Node.js 24。
四、解析“1.2万”等指标文本
小红书页面上的数字不一定都是普通整数,可能出现:
986 1.2万 3万+可以先编写统一的解析函数:
export function parseMetricCount(rawText: string): number | null { const normalized = rawText .trim() .replaceAll(',', '') .replaceAll('+', ''); if (!normalized) { return null; } const tenThousandMatch = normalized.match(/(\d+(?:\.\d+)?)\s*万/); if (tenThousandMatch) { return Math.round(Number(tenThousandMatch[1]) * 10_000); } const thousandMatch = normalized.match(/(\d+(?:\.\d+)?)\s*千/); if (thousandMatch) { return Math.round(Number(thousandMatch[1]) * 1_000); } const integerMatch = normalized.match(/\d+/); if (!integerMatch) { return null; } return Number(integerMatch[0]); }测试:
console.log(parseMetricCount('986')); // 986 console.log(parseMetricCount('1.2万')); // 12000 console.log(parseMetricCount('3万+')); // 30000五、使用持久化浏览器会话
第一次运行时,由用户本人手动登录小红书。
后续运行复用本地浏览器配置目录,不需要在代码中保存明文账号和密码。
import { chromium, type BrowserContext, type Page } from 'playwright'; import path from 'node:path'; const profileDir = path.resolve( process.cwd(), '.auth', 'xiaohongshu-metrics', ); export async function openAuthorizedXhsContext(): Promise<BrowserContext> { return chromium.launchPersistentContext(profileDir, { headless: false, viewport: { width: 1440, height: 960, }, locale: 'zh-CN', }); }第一次启动后,在打开的浏览器中手动登录。
需要把目录加入.gitignore:
.auth/禁止把浏览器配置目录、Cookie、Token 或用户凭证提交到 Git。
六、实现小红书指标采集器
由于页面结构可能变化,不建议把选择器散落在业务代码中。
可以把选择器集中配置:
const METRIC_SELECTORS = { comment: [ '[aria-label*="评论"]', 'button:has-text("评论")', '[class*="comment"]', ], favorite: [ '[aria-label*="收藏"]', 'button:has-text("收藏")', '[class*="collect"]', ], like: [ '[aria-label*="点赞"]', 'button:has-text("点赞")', '[class*="like"]', ], } as const;编写一个安全读取函数:
import type { Page } from 'playwright'; async function readMetricFromSelectors( page: Page, selectors: readonly string[], ): Promise<number | null> { for (const selector of selectors) { const locator = page.locator(selector).first(); if ((await locator.count()) === 0) { continue; } const text = await locator .innerText() .catch(() => ''); const value = parseMetricCount(text); if (value !== null) { return value; } } return null; }定义统一输出:
export interface XhsMetricSnapshot { articleUrl: string; likeCount: number | null; commentCount: number | null; favoriteCount: number | null; dataSource: 'BROWSER'; collectedAt: string; }实现采集逻辑:
export async function collectXhsMetrics( articleUrl: string, ): Promise<XhsMetricSnapshot> { const context = await openAuthorizedXhsContext(); try { const page = await context.newPage(); await page.goto(articleUrl, { waitUntil: 'domcontentloaded', timeout: 30_000, }); await page .waitForLoadState('networkidle', { timeout: 10_000, }) .catch(() => undefined); const currentUrl = page.url(); if (currentUrl.includes('/login')) { throw new Error('XHS_LOGIN_REQUIRED'); } const [likeCount, commentCount, favoriteCount] = await Promise.all([ readMetricFromSelectors( page, METRIC_SELECTORS.like, ), readMetricFromSelectors( page, METRIC_SELECTORS.comment, ), readMetricFromSelectors( page, METRIC_SELECTORS.favorite, ), ]); return { articleUrl, likeCount, commentCount, favoriteCount, dataSource: 'BROWSER', collectedAt: new Date().toISOString(), }; } finally { await context.close(); } }这里没有实现验证码绕过。
出现登录失效或验证码时,应停止自动采集,并提示用户重新完成授权登录。
七、不要把“取不到”自动写成 0
下面两种情况含义不同:
0 指标真实为零 null 当前无法可靠获取因此不要这样写:
const favoriteCount = parsedValue ?? 0;正确做法是保留null:
const favoriteCount = parsedValue;前端可以显示:
收藏数:暂不可获取而不是误导用户:
收藏数:0八、设计统一采集器接口
为了以后能够切换到官方 API,不要把 Playwright 逻辑直接写进 Controller。
可以定义统一接口:
export interface PlatformMetricsCollector { supports(platformCode: string): boolean; collect(input: { articleUrl: string; resultId: string; }): Promise<{ likeCount: number | null; commentCount: number | null; favoriteCount: number | null; collectedAt: string; dataSource: 'REAL_API' | 'BROWSER'; }>; }Playwright 实现:
export class XhsBrowserMetricsCollector implements PlatformMetricsCollector { supports(platformCode: string): boolean { return platformCode === 'XIAOHONGSHU'; } async collect(input: { articleUrl: string; resultId: string; }) { const snapshot = await collectXhsMetrics( input.articleUrl, ); return { likeCount: snapshot.likeCount, commentCount: snapshot.commentCount, favoriteCount: snapshot.favoriteCount, collectedAt: snapshot.collectedAt, dataSource: snapshot.dataSource, }; } }将来获得官方接口权限后,只需新增:
class XhsOfficialApiMetricsCollector implements PlatformMetricsCollector { // 调用经过授权的官方接口 }不需要修改指标查询页面和数据库结构。
九、设计后端同步接口
建议提供:
POST /api/v1/publish-results/{resultId}/sync-metrics业务流程:
1. 查询 publish_result 2. 确认 platformCode=XIAOHONGSHU 3. 检查 articleUrl 4. 选择可用采集器 5. 拉取评论数与收藏数 6. 校验非负整数 7. 新增 platform_metrics 快照 8. 返回本次采集结果Express 示例:
import type { Request, Response } from 'express'; export async function syncMetricsController( req: Request, res: Response, ): Promise<void> { const resultId = req.params.resultId; const publishResult = await publishResultRepository.findById(resultId); if (!publishResult) { res.status(404).json({ code: 'PUBLISH_RESULT_NOT_FOUND', message: '发布结果不存在', }); return; } if (!publishResult.articleUrl) { res.status(409).json({ code: 'ARTICLE_URL_MISSING', message: '发布结果尚未包含小红书笔记地址', }); return; } const collector = metricsCollectorRegistry.find( publishResult.platformCode, ); if (!collector) { res.status(501).json({ code: 'XHS_METRICS_COLLECTOR_UNAVAILABLE', message: '当前尚未接入小红书授权指标采集器', }); return; } const snapshot = await collector.collect({ articleUrl: publishResult.articleUrl, resultId, }); const saved = await metricSnapshotService.create({ resultId, likeCount: snapshot.likeCount, commentCount: snapshot.commentCount, favoriteCount: snapshot.favoriteCount, viewCount: null, shareCount: null, dataSource: snapshot.dataSource, collectedAt: snapshot.collectedAt, }); res.status(201).json({ data: saved, }); }十、指标历史查询接口
建议提供:
GET /api/v1/publish-results/{resultId}/metrics支持:
page pageSize latest例如:
GET /api/v1/publish-results/result-001/metrics?page=1&pageSize=20查询 SQL:
SELECT * FROM platform_metrics WHERE publish_result_id = ? ORDER BY collected_at DESC LIMIT ? OFFSET ?;如果只查询最新快照:
GET /api/v1/publish-results/result-001/metrics?latest=true十一、完整评论内容与评论数不是一回事
本文主要采集的是:
评论数量 收藏数量 点赞数量如果需要拉取完整评论内容,还需要额外考虑:
评论分页;
回复层级;
评论用户信息脱敏;
删除评论的处理;
重复评论去重;
数据保存期限;
用户隐私和平台授权范围。
不建议在没有明确授权的情况下保存评论用户头像、昵称、主页地址等信息。
课程项目或内部运营系统中,优先只保存聚合指标,风险更低,也更容易维护。
十二、异常处理建议
1. 登录状态失效
返回:
XHS_LOGIN_REQUIRED页面提示:
小红书登录状态已失效,请重新完成浏览器授权。2. 页面结构变化
返回:
XHS_METRIC_SELECTOR_NOT_MATCHED不要自动把所有指标写成 0。
3. 笔记不存在
返回:
XHS_NOTE_NOT_FOUND4. 指标部分可用
允许保存:
{ "likeCount": 120, "commentCount": 15, "favoriteCount": null }不应该因为一个字段无法获取,就丢弃其他有效指标。
十三、安全与合规要求
实际开发中应遵循以下原则:
只采集本人账号、本人笔记或明确授权的数据;
优先使用官方接口和创作者平台;
不绕过验证码和平台访问控制;
不保存明文密码;
不把 Cookie、Token 和 Authorization 写入日志;
浏览器用户目录不得提交 Git;
控制采集频率,不进行大规模并发请求;
采集失败不应影响文章发布主流程;
Mock 数据必须明确标记为 Mock;
页面结构改变时应停止采集,而不是盲目写入错误结果。
十四、总结
一个可靠的小红书指标采集模块,不只是写一个页面爬虫。
更合理的设计是:
授权浏览器会话 + 可替换采集器 + 指标标准化 + 追加式历史快照 + 数据来源标记 + 安全与异常处理最终可以形成:
小红书发布成功 → 定时或手动刷新指标 → 写入评论与收藏快照 → 查看指标变化趋势 → 导出运营报告如果未来获得官方数据接口权限,只需替换采集器实现,不需要重写数据库、接口和前端页面。
当前小红书创作服务平台公开页面明确提到“数据分析”能力;与此同时,公开开放平台文档目录主要展示商品、订单、物流、售后等接口场景。基于当前公开文档,我没有找到面向任意笔记评论数、收藏数的通用公开 API,因此博文采用“官方授权优先、授权浏览器会话降级、人工导入兜底”的表述更严谨。:contentReference[oaicite:0]{index=0} **推荐 CSDN 标题:** ```text TypeScript + Playwright 实战:合规拉取小红书笔记评论数与收藏数摘要:
本文基于 TypeScript、Playwright、Express 和 SQLite,设计并实现一个面向授权账号的小红书指标采集模块,支持读取评论数、收藏数与点赞数,保存追加式指标快照,并通过统一采集器接口为后续官方 API 接入预留扩展点。标签:
TypeScript Playwright Node.js 小红书 数据采集 Express SQLite