㊗️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~
㊙️本期爬虫难度指数:⭐⭐⭐
🉐福利: 一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。

全文目录:
-
- [🌟 开篇语](#🌟 开篇语)
- [1️⃣ 摘要(Abstract)](#1️⃣ 摘要(Abstract))
- [2️⃣ 背景与需求(Why)](#2️⃣ 背景与需求(Why))
- [3️⃣ 合规与注意事项(必读)](#3️⃣ 合规与注意事项(必读))
-
- [robots.txt 检查](#robots.txt 检查)
- 合规原则(这部分我必须认真写,不是套话)
- [4️⃣ 技术选型与整体流程(What/How)](#4️⃣ 技术选型与整体流程(What/How))
- [5️⃣ 环境准备与依赖安装(可复现)](#5️⃣ 环境准备与依赖安装(可复现))
- [6️⃣ 核心实现:请求层(Browser 管理器)](#6️⃣ 核心实现:请求层(Browser 管理器))
- [7️⃣ 核心实现:解析层(Parser)](#7️⃣ 核心实现:解析层(Parser))
-
- [HTML 结构分析](#HTML 结构分析)
- 解析器实现
- [8️⃣ 数据存储与导出(Storage)](#8️⃣ 数据存储与导出(Storage))
- [9️⃣ 运行方式与结果展示](#9️⃣ 运行方式与结果展示)
- [🔟 常见问题与排错](#🔟 常见问题与排错)
-
- [Q1: 遇到滑块验证码/人机验证怎么办?](#Q1: 遇到滑块验证码/人机验证怎么办?)
- [Q2: 笔记卡片解析为空(notes = [])?](#Q2: 笔记卡片解析为空(notes = [])?)
- [Q3: 点赞数、收藏数都是 0?](#Q3: 点赞数、收藏数都是 0?)
- [Q4: CSV 打开后中文乱码?](#Q4: CSV 打开后中文乱码?)
- [Q5: 每次运行都需要重新登录验证?](#Q5: 每次运行都需要重新登录验证?)
- [1️⃣1️⃣ 进阶优化(加分项)](#1️⃣1️⃣ 进阶优化(加分项))
-
- [网络拦截:直接捕获 XHR 数据](#网络拦截:直接捕获 XHR 数据)
- 断点续爬
- 多关键词并行(谨慎使用)
- 数据分析:找出高潜力选题
- [1️⃣2️⃣ 总结与延伸阅读](#1️⃣2️⃣ 总结与延伸阅读)
- [🌟 文末](#🌟 文末)
-
- [✅ 专栏持续更新中|建议收藏 + 订阅](#✅ 专栏持续更新中|建议收藏 + 订阅)
- [✅ 互动征集](#✅ 互动征集)
- [✅ 免责声明](#✅ 免责声明)
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO
欢迎大家常来逛逛,一起学习,一起进步~🌟
我长期专注 Python 爬虫工程化实战 ,主理专栏 《Python爬虫实战》:从采集策略 到反爬对抗 ,从数据清洗 到分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上。
📌 专栏食用指南(建议收藏)
- ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
- ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
- ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
- ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用
📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅专栏👉《Python爬虫实战》👈,一次订阅后,专栏内的所有文章可永久免费阅读,持续更新中。
💕订阅后更新会优先推送,按目录学习更高效💯~
1️⃣ 摘要(Abstract)
一句话概括:
使用 Python + Playwright(或 requests 逆向 API)采集小红书搜索结果页的热门笔记,提取笔记标题、作者、点赞数、收藏数、封面图、关键词等字段,最终输出为结构化 CSV 数据,为内容分析与竞品研究提供数据支撑。
读完本文你将获得:
- 理解现代 SPA 搜索页的爬取本质:为什么 requests 直接请求只能拿到空壳,以及两种可行的突破思路
- 学会用 Playwright 驱动真实浏览器,完整抓取动态渲染内容,同时控制好频率与风险
- 获得一套可落地的笔记数据采集框架,适用于关键词调研、内容热度分析等真实业务场景
2️⃣ 背景与需求(Why)
为什么要爬小红书笔记?
小红书已经成为国内内容营销领域绕不开的平台。每天有海量用户在上面分享生活方式、产品测评、旅行攻略。对于以下几类人群,搜索关键词对应的热门笔记数据价值极高:
- 内容运营: 分析某个关键词下的高赞笔记有什么共同特征------封面风格、标题句式、发布时间
- 品牌竞调: 追踪竞品在小红书的曝光量、热门内容方向
- 自媒体创作者: 了解某个垂类的高互动内容长什么样,有针对性地优化自己的创作
- 学术研究: 社交媒体热点话题传播路径、用户行为分析
问题是,小红书没有开放的数据接口,APP 内容也做了加密。网页端(www.xiaohongshu.com)虽然可以访问,但搜索结果是 JavaScript 动态渲染的,直接用 requests 拿到的 HTML 里几乎没有笔记数据。
目标站点与字段清单
目标站点: https://www.xiaohongshu.com/search_result
核心字段:
| 字段名称 | 字段说明 | 例如 |
|---|---|---|
| keyword | 搜索关键词 | "露营装备" |
| note_title | 笔记标题 | "去年花3000买的帐篷,今天终于用上了" |
| author | 作者昵称 | "户外老王" |
| likes | 点赞数 | 12500 |
| collects | 收藏数 | 8300 |
| cover_url | 封面图 URL | "https://sns-img-qc.xhscdn.com/..." |
| note_url | 笔记详情链接 | "https://www.xiaohongshu.com/explore/..." |
| crawl_time | 采集时间 | "2025-02-11 16:30:00" |
3️⃣ 合规与注意事项(必读)
robots.txt 检查
json
https://www.xiaohongshu.com/robots.txt
关键内容:
- 小红书在 robots.txt 中禁止了大多数爬虫路径,包括搜索页
- 即便如此,学习性、低频的技术研究在国内通常属于灰色地带,法律风险较低
- 商业用途的大规模采集则明确违反平台 ToS,存在法律风险
合规原则(这部分我必须认真写,不是套话)
✅ 相对可接受的:
- 个人学习研究,每次采集数据量控制在百条以内
- 只采集公开展示的笔记元数据(标题、作者、点赞数)
- 请求频率极低,不对服务器造成任何压力
❌ 务必避免的:
- 规模化批量采集(每次跑几千条、持续运行)
- 采集用户的私信、个人账号、联系方式等个人信息
- 将数据二次出售或用于商业目的
- 绕过登录限制获取非公开内容
我需要特别说明的一点:
小红书对反爬投入较大。本文的技术方案能帮你理解爬虫开发的完整流程和思路,但在实际执行中,遇到 IP 封锁、滑块验证码、账号检测等情况是完全正常的。本文的目标是技术教学,不是帮你无限制地薅平台数据。如果你有大规模数据分析需求,认真考虑一下官方合作渠道或第三方数据服务商。
4️⃣ 技术选型与整体流程(What/How)
先搞清楚问题本质
很多同学上来就想用 requests + BeautifulSoup,拿到 HTML 一看,笔记列表是空的。这不是技术问题,是对目标网站渲染方式的误判。
打开 Chrome 开发者工具,访问小红书搜索页,做两件事:
- 右键"查看网页源代码":你会看到 HTML 里只有
<div id="app"></div>,笔记数据一条没有 - 打开 Network 面板,过滤 XHR 请求:你会发现笔记数据来自几个内部 API 接口
这说明小红书属于典型的 CSR(客户端渲染)应用,数据走异步接口。
两种技术路线:
| 路线 | 核心工具 | 优点 | 缺点 |
|---|---|---|---|
| 路线A:API 逆向 | requests | 速度快,资源消耗低 | 接口有签名验证(X-S、X-T 参数),逆向难度较高 |
| 路线B:浏览器自动化 | Playwright | 无需破解签名,数据完整 | 速度慢,资源消耗大 |
本文采用路线B(Playwright)+ 路线A 思路作为扩展讲解。
原因很简单:小红书的 API 签名算法变化较频繁,今天写好的逆向代码一周后可能就失效了,维护成本太高。Playwright 方案虽然慢,但稳定性和可维护性更强,学到的技能也更通用。
整体流程设计
json
┌────────────────────────┐
│ 1. 启动浏览器,打开 │
│ 搜索结果页 │ → 等待笔记卡片渲染完毕
└────────────────────────┘
┌────────────────────────┐
│ 2. 解析当前页笔记卡片 │
│ │ → 提取标题/作者/点赞/封面/链接
└────────────────────────┘
┌────────────────────────┐
│ 3. 模拟滚动翻页 │
│ │ → 触发下一批数据加载
└────────────────────────┘
┌────────────────────────┐
│ 4. 数据清洗与存储 │
│ │ → 去重 → 写入 CSV
└────────────────────────┘
5️⃣ 环境准备与依赖安装(可复现)
Python 版本要求
推荐 Python 3.9+(本文测试环境:3.10.12)
依赖安装
bash
# 核心依赖
pip install playwright==1.42.0
pip install pandas==2.1.4
pip install lxml==5.1.0
# 安装浏览器内核(只需运行一次)
playwright install chromium
安装完成后验证 Playwright 是否正常:
bash
python -c "from playwright.sync_api import sync_playwright; print('Playwright OK')"
项目结构(推荐)
json
xhs_spider/
│
├── main.py # 主程序入口
├── browser.py # Playwright 浏览器管理
├── parser.py # 笔记卡片解析器
├── storage.py # 数据存储
├── config.py # 配置(关键词、数量限制等)
├── requirements.txt # 依赖清单
│
├── data/ # 数据输出
│ └── notes_露营装备_20250211.csv
│
├── cookies/ # Cookie 持久化(可选)
│ └── xhs_cookies.json
│
└── logs/
└── spider.log
6️⃣ 核心实现:请求层(Browser 管理器)
小红书在 Playwright 默认模式下可能会检测到自动化工具特征,需要做一些反检测处理。
浏览器启动配置
python
# browser.py
from playwright.sync_api import sync_playwright, Page, BrowserContext
import json
import os
import time
import random
class XhsBrowser:
"""小红书 Playwright 浏览器管理器"""
def __init__(self, headless: bool = False, cookie_path: str = None):
"""
Args:
headless: 是否无头模式(建议调试时关掉,生产时开启)
cookie_path: Cookie 文件路径(可选,传入可免登录)
"""
self.headless = headless
self.cookie_path = cookie_path
self.pw = None
self.browser = None
self.context = None
self.page = None
def start(self):
"""启动浏览器"""
self.pw = sync_playwright().start()
# 启动 Chromium,带反检测参数
self.browser = self.pw.chromium.launch(
headless=self.headless,
args=[
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
'--disable-dev-shm-usage',
]
)
# 创建上下文,模拟真实浏览器环境
self.context = self.browser.new_context(
viewport={'width': 1440, 'height': 900},
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
locale='zh-CN',
timezone_id='Asia/Shanghai',
)
# 注入反检测脚本
self.context.add_init_script("""
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
""")
# 加载 Cookie(如果有)
if self.cookie_path and os.path.exists(self.cookie_path):
self._load_cookies()
self.page = self.context.new_page()
return self
def _load_cookies(self):
"""从文件加载 Cookie"""
with open(self.cookie_path, 'r') as f:
cookies = json.load(f)
self.context.add_cookies(cookies)
print(f"✅ Cookie 已加载: {len(cookies)} 条")
def save_cookies(self):
"""保存当前 Cookie 到文件"""
cookies = self.context.cookies()
os.makedirs('cookies', exist_ok=True)
with open(self.cookie_path or 'cookies/xhs_cookies.json', 'w') as f:
json.dump(cookies, f, ensure_ascii=False, indent=2)
print("✅ Cookie 已保存")
def navigate(self, url: str, wait_for: str = None):
"""
导航到目标页面
Args:
url: 目标 URL
wait_for: 等待某个选择器出现(确保内容加载完毕)
"""
self.page.goto(url, timeout=30000, wait_until='domcontentloaded')
# 随机延迟,模拟人类打开页面的行为
time.sleep(random.uniform(2, 4))
if wait_for:
try:
self.page.wait_for_selector(wait_for, timeout=15000)
except Exception:
print(f"⚠️ 等待选择器超时: {wait_for}")
def scroll_down(self, times: int = 3, interval: float = 2.0):
"""
向下滚动页面,触发懒加载
Args:
times: 滚动次数
interval: 每次滚动的等待时间(秒)
"""
for _ in range(times):
self.page.evaluate("window.scrollBy(0, window.innerHeight * 0.8)")
time.sleep(interval + random.uniform(0, 1))
def get_page_content(self) -> str:
"""获取当前页面的 HTML"""
return self.page.content()
def close(self):
"""关闭浏览器"""
if self.browser:
self.browser.close()
if self.pw:
self.pw.stop()
def __enter__(self):
return self.start()
def __exit__(self, *args):
self.close()
几个关键决策的说明:
headless=False 在调试阶段建议保持关闭,能直观地看到浏览器的行为,方便排查问题。等逻辑调通后再切到无头模式。
反检测脚本的核心是覆盖 navigator.webdriver 属性------这是 Selenium/Playwright 最容易被检测到的特征之一。这个方法不是万能的,小红书有更复杂的指纹检测,但能过滤掉基础的机器人判断。
scroll_down 方法模拟用户滚动的原因:小红书的搜索结果采用无限滚动加载(瀑布流),只有滚动到底部才会触发下一批数据请求。
7️⃣ 核心实现:解析层(Parser)
HTML 结构分析
用 Playwright 渲染完页面后,打开开发者工具,观察笔记卡片的 DOM 结构(实际结构以页面为准,以下为典型结构):
html
<section class="note-item">
<a class="cover ..." href="/explore/NOTEID">
<img src="https://sns-img-qc.xhscdn.com/..." alt="封面图">
</a>
<footer class="footer">
<a class="title" href="/explore/NOTEID">
<span>去年花3000买的帐篷,今天终于用上了</span>
</a>
<div class="author-wrapper">
<span class="name">户外老王</span>
</div>
<div class="interact-info">
<span class="like-wrapper">
<span class="count">1.2万</span>
</span>
</div>
</footer>
</section>
解析器实现
python
# parser.py
from bs4 import BeautifulSoup
from typing import List, Dict
import re
class XhsParser:
"""小红书笔记卡片解析器"""
BASE_URL = "https://www.xiaohongshu.com"
@staticmethod
def parse_note_list(html: str, keyword: str = '') -> List[Dict]:
"""
解析搜索结果页的笔记列表
Args:
html: 渲染后的完整 HTML
keyword: 当前搜索的关键词
Returns:
笔记数据列表
"""
soup = BeautifulSoup(html, 'lxml')
notes = []
# 定位所有笔记卡片(选择器需根据实际页面调整)
note_cards = soup.select('section.note-item')
if not note_cards:
# 备用选择器
note_cards = soup.select('[class*="note-item"]')
if not note_cards:
print("⚠️ 未找到笔记卡片,请检查选择器或页面是否正确渲染")
return []
print(f"🔍 当前页找到 {len(note_cards)} 个笔记卡片")
for card in note_cards:
try:
note = XhsParser._parse_single_card(card, keyword)
if note:
notes.append(note)
except Exception as e:
print(f"⚠️ 解析单个卡片出错: {e}")
continue
return notes
@staticmethod
def _parse_single_card(card, keyword: str) -> Dict:
"""解析单个笔记卡片"""
# 封面图与笔记链接(通常在同一个 <a> 标签上)
cover_link = card.select_one('a.cover')
note_url = ''
cover_url = ''
if cover_link:
href = cover_link.get('href', '')
if href:
note_url = XhsParser.BASE_URL + href if href.startswith('/') else href
img = cover_link.find('img')
if img:
# 优先取 src,懒加载的情况取 data-src
cover_url = img.get('src') or img.get('data-src') or ''
# 笔记标题
title_tag = card.select_one('a.title span, .title span, span.title')
note_title = title_tag.text.strip() if title_tag else ''
# 作者昵称
author_tag = card.select_one('.name, [class*="author"] span, .author-wrapper span')
author = author_tag.text.strip() if author_tag else '未知作者'
# 点赞数(可能是"1.2万"这样的格式,需要转换)
likes_tag = card.select_one('[class*="like"] .count, .like-wrapper .count')
likes_raw = likes_tag.text.strip() if likes_tag else '0'
likes = XhsParser._parse_count(likes_raw)
# 收藏数(小红书网页端有时不显示单独的收藏数,和点赞数合并展示)
collects_tag = card.select_one('[class*="collect"] .count, .collect-wrapper .count')
collects_raw = collects_tag.text.strip() if collects_tag else '0'
collects = XhsParser._parse_count(collects_raw)
# 至少要有标题或链接才算有效数据
if not note_title and not note_url:
return None
from datetime import datetime
return {
'keyword': keyword,
'note_title': note_title,
'author': author,
'likes': likes,
'collects': collects,
'cover_url': cover_url,
'note_url': note_url,
'crawl_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
}
@staticmethod
def _parse_count(count_str: str) -> int:
"""
将"1.2万"、"8300"、"3.5k"等格式转换为整数
Args:
count_str: 原始数字字符串
Returns:
整数
"""
if not count_str or count_str == '-':
return 0
count_str = count_str.strip().replace(',', '')
try:
if '万' in count_str:
num = float(count_str.replace('万', ''))
return int(num * 10000)
elif 'k' in count_str.lower():
num = float(count_str.lower().replace('k', ''))
return int(num * 1000)
else:
return int(float(count_str))
except (ValueError, AttributeError):
return 0
几个细节值得单独说一下:
_parse_count 方法是这个解析器里我花时间最多的部分。小红书显示互动数据时,超过 1 万的会显示"1.2万",这种中文混合格式如果不单独处理,直接转 int 会报错。把它做成独立方法,后续如果格式变化只需要改一处。
选择器我用了多个备选(a.title span, .title span, span.title),因为小红书的 CSS 类名可能随版本变化。遇到解析为空时,第一反应不是改代码,而是先 print(card.prettify()) 看看实际的 DOM 结构,再对应调整。
8️⃣ 数据存储与导出(Storage)
存储策略
对于搜索关键词驱动的采集场景,我倾向于用 CSV + 增量追加 的策略:
- 每个关键词对应一个 CSV 文件
- 已存在的 CSV 在新一轮采集时追加写入,不覆盖历史数据
- 去重在写入前完成(基于 note_url)
存储层实现
python
# storage.py
import pandas as pd
import os
import hashlib
from typing import List, Dict
from datetime import datetime
class XhsStorage:
"""小红书数据存储管理"""
def __init__(self, output_dir: str = 'data'):
self.output_dir = output_dir
os.makedirs(output_dir, exist_ok=True)
def _get_filepath(self, keyword: str) -> str:
"""根据关键词生成文件路径"""
safe_keyword = re.sub(r'[\\/:*?"<>|]', '_', keyword)
date_str = datetime.now().strftime('%Y%m%d')
return os.path.join(self.output_dir, f'notes_{safe_keyword}_{date_str}.csv')
def save(self, notes: List[Dict], keyword: str):
"""
保存笔记数据(增量模式)
Args:
notes: 笔记列表
keyword: 关键词(用于文件命名)
"""
if not notes:
print("⚠️ 无数据可保存")
return
import re
filepath = self._get_filepath(keyword)
new_df = pd.DataFrame(notes)
# 如果文件已存在,合并并去重
if os.path.exists(filepath):
existing_df = pd.read_csv(filepath, encoding='utf-8-sig')
combined_df = pd.concat([existing_df, new_df], ignore_index=True)
else:
combined_df = new_df
# 去重(基于笔记 URL)
before = len(combined_df)
combined_df = combined_df.drop_duplicates(subset=['note_url'], keep='last')
after = len(combined_df)
if before > after:
print(f"🔄 去重:删除 {before - after} 条重复记录")
# 按点赞数排序
if 'likes' in combined_df.columns:
combined_df = combined_df.sort_values('likes', ascending=False)
combined_df.to_csv(filepath, index=False, encoding='utf-8-sig')
print(f"✅ 数据已保存: {filepath}")
print(f"📊 当前共 {len(combined_df)} 条笔记")
return filepath
def get_existing_urls(self, keyword: str) -> set:
"""获取已采集的笔记 URL 集合(用于断点续爬)"""
import re
filepath = self._get_filepath(keyword)
if not os.path.exists(filepath):
return set()
df = pd.read_csv(filepath, encoding='utf-8-sig')
return set(df['note_url'].dropna().tolist())
字段映射表:
| 字段名 | 数据类型 | 示例值 | 备注 |
|---|---|---|---|
| keyword | string | "露营装备" | 搜索关键词 |
| note_title | string | "去年花3000买的帐篷..." | 笔记标题 |
| author | string | "户外老王" | 作者昵称 |
| likes | integer | 12500 | 点赞数(已转换为整数) |
| collects | integer | 8300 | 收藏数(可能为0) |
| cover_url | string | "https://sns-img-qc..." | 封面图 URL |
| note_url | string | "https://www.xiaohongshu.com/explore/..." | 笔记链接(去重主键) |
| crawl_time | string | "2025-02-11 16:30:00" | 采集时间戳 |
9️⃣ 运行方式与结果展示
主程序入口
python
# main.py
from browser import XhsBrowser
from parser import XhsParser
from storage import XhsStorage
import time
import random
# ========= 配置区域 =========
KEYWORDS = ['露营装备', '徒步穿搭'] # 要搜索的关键词列表
MAX_SCROLL = 5 # 每个关键词最多滚动几次(每次加载约10条)
HEADLESS = False # 调试时建议 False
COOKIE_PATH = 'cookies/xhs_cookies.json' # Cookie 文件路径(可选)
# ============================
def crawl_keyword(keyword: str, storage: XhsStorage) -> int:
"""
采集单个关键词的笔记
Returns:
采集到的笔记数量
"""
search_url = f'https://www.xiaohongshu.com/search_result?keyword={keyword}&type=51'
print(f"\n🔍 开始采集关键词: 「{keyword}」")
print(f" URL: {search_url}")
# 已采集的 URL(用于跳过重复)
existing_urls = storage.get_existing_urls(keyword)
total_notes = []
with XhsBrowser(headless=HEADLESS, cookie_path=COOKIE_PATH) as browser:
# 访问搜索页
browser.navigate(
search_url,
wait_for='section.note-item' # 等待至少一个笔记卡片出现
)
# 滚动翻页
for scroll_idx in range(MAX_SCROLL):
print(f"\n 第 {scroll_idx + 1}/{MAX_SCROLL} 次滚动...")
# 获取当前页 HTML
html = browser.get_page_content()
# 解析笔记
notes = XhsParser.parse_note_list(html, keyword)
# 过滤已采集的
new_notes = [n for n in notes if n['note_url'] not in existing_urls]
total_notes.extend(new_notes)
for n in new_notes:
existing_urls.add(n['note_url'])
print(f" 本次新增: {len(new_notes)} 条笔记")
# 滚动到下方,触发下一批加载
browser.scroll_down(times=2, interval=1.5)
# 每次滚动后等待一段时间
time.sleep(random.uniform(2, 4))
# 保存数据
if total_notes:
storage.save(total_notes, keyword)
else:
print(f" ⚠️ 未采集到新数据")
return len(total_notes)
def main():
"""主流程"""
print("🌸 小红书笔记爬虫启动...\n")
storage = XhsStorage()
total = 0
for keyword in KEYWORDS:
count = crawl_keyword(keyword, storage)
total += count
# 关键词之间随机等待
if keyword != KEYWORDS[-1]:
wait_time = random.uniform(5, 10)
print(f"\n⏳ 等待 {wait_time:.1f} 秒后继续下一个关键词...")
time.sleep(wait_time)
print(f"\n🎉 全部完成!共采集 {total} 条新笔记")
if __name__ == '__main__':
main()
启动命令
json
# 进入项目目录
cd xhs_spider
# 运行爬虫
python main.py
运行日志示例
json
🌸 小红书笔记爬虫启动...
🔍 开始采集关键词: 「露营装备」
URL: https://www.xiaohongshu.com/search_result?keyword=露营装备&type=51
第 1/5 次滚动...
🔍 当前页找到 20 个笔记卡片
本次新增: 20 条笔记
第 2/5 次滚动...
🔍 当前页找到 30 个笔记卡片
本次新增: 10 条笔记
第 3/5 次滚动...
🔍 当前页找到 40 个笔记卡片
本次新增: 10 条笔记
...
✅ 数据已保存: data/notes_露营装备_20250211.csv
📊 当前共 45 条笔记
⏳ 等待 7.3 秒后继续下一个关键词...
🔍 开始采集关键词: 「徒步穿搭」
...
🎉 全部完成!共采集 89 条新笔记
结果展示(CSV 前5行)
| keyword | note_title | author | likes | collects | note_url |
|---|---|---|---|---|---|
| 露营装备 | 去年花3000买的帐篷,今天终于用上了 | 户外老王 | 12500 | 8300 | https://www.xiaohongshu.com/explore/... |
| 露营装备 | 露营必备清单|新手入坑不踩雷 | 露营日记本 | 9800 | 6200 | https://www.xiaohongshu.com/explore/... |
| 露营装备 | 500元预算怎么配置露营装备? | 穷游快乐星 | 7300 | 4100 | https://www.xiaohongshu.com/explore/... |
| 徒步穿搭 | 女生徒步穿搭分享,实用又显瘦 | 山野小鹿 | 15600 | 11200 | https://www.xiaohongshu.com/explore/... |
| 徒步穿搭 | 徒步3天2夜,装备清单附购买链接 | 背包客小明 | 8900 | 5700 | https://www.xiaohongshu.com/explore/... |
🔟 常见问题与排错
Q1: 遇到滑块验证码/人机验证怎么办?
原因分析:
这是小红书最常见的反爬措施。触发条件包括:短时间内多次访问、IP 异常、浏览器指纹被识别。
解决思路:
python
# 检测到验证码后暂停,等待人工处理(半自动模式)
page.wait_for_selector('[class*="verify"], [class*="captcha"]', timeout=3000)
# 如果检测到,暂停程序,提示人工滑动
input("⚠️ 检测到验证码,请手动完成验证后按回车继续...")
更根本的解决方案是降低频率,或者使用带登录态的 Cookie------登录用户触发验证码的概率比匿名用户低得多。
Q2: 笔记卡片解析为空(notes = [])?
原因分析:
- 页面还没渲染完,就开始解析了
- 选择器对应的 class 名发生了变化
- 遇到了错误页面(搜索结果为空、网络错误)
排查步骤:
python
# 先把 HTML 保存下来,手动用浏览器打开看看
html = browser.get_page_content()
with open('debug_page.html', 'w', encoding='utf-8') as f:
f.write(html)
print("HTML 已保存,请用浏览器打开 debug_page.html 检查")
保存下来的文件用浏览器打开,然后 F12 找到笔记卡片的真实选择器,更新 parser.py 里的选择器。
Q3: 点赞数、收藏数都是 0?
原因分析:
小红书在未登录状态下可能不展示完整的互动数据,或者互动数据通过另一个 XHR 请求异步加载,HTML 里并没有这些数字。
解决方案:
python
# 等待互动数据加载(增加等待时间)
browser.page.wait_for_timeout(3000)
# 或者尝试用 Playwright 直接读取元素文本,绕过 BS4 解析
likes_text = browser.page.query_selector('[class*="like"] .count')
if likes_text:
likes = XhsParser._parse_count(likes_text.inner_text())
Q4: CSV 打开后中文乱码?
原因分析:
Windows 的 Excel 默认用 GBK 编码打开 CSV,而文件是 UTF-8 存的。
解决方案:
代码里已经用了 encoding='utf-8-sig'(带 BOM 的 UTF-8),这是专门解决 Windows Excel 乱码问题的标准做法。如果还是乱码,尝试:
json
Excel → 数据 → 从文本/CSV → 选文件 → 编码选 UTF-8 → 导入
Q5: 每次运行都需要重新登录验证?
解决方案(Cookie 持久化):
python
# 第一次手动登录后,把 Cookie 保存下来
browser = XhsBrowser(headless=False, cookie_path='cookies/xhs_cookies.json')
browser.start()
browser.navigate('https://www.xiaohongshu.com')
input("请手动登录,完成后按回车保存 Cookie...")
browser.save_cookies()
browser.close()
# 之后每次运行都会自动加载 Cookie,无需重新登录
1️⃣1️⃣ 进阶优化(加分项)
网络拦截:直接捕获 XHR 数据
不用解析 HTML,直接拦截 Playwright 的网络请求,拿到原始 JSON:
python
# 比解析 HTML 更稳定,不受页面改版影响
notes_data = []
def handle_response(response):
"""拦截 API 响应"""
if 'search_result' in response.url or 'homefeed' in response.url:
try:
data = response.json()
# 解析 JSON 数据,提取笔记信息
items = data.get('data', {}).get('items', [])
for item in items:
note = item.get('note_card', {})
notes_data.append({
'note_title': note.get('title', ''),
'author': note.get('user', {}).get('nickname', ''),
'likes': note.get('interact_info', {}).get('liked_count', 0),
# ... 其他字段
})
except Exception:
pass
# 注册拦截器
page.on('response', handle_response)
page.goto(search_url)
这个方案的优点是完全脱离了 CSS 选择器,不受页面重构影响。缺点是需要先摸清楚 API 的响应结构。
断点续爬
python
def crawl_keyword_with_checkpoint(keyword, storage, max_scroll=10):
"""带断点续爬的采集函数"""
# 读取上次进度
progress_file = f'cache/{keyword}_progress.json'
start_scroll = 0
if os.path.exists(progress_file):
with open(progress_file) as f:
progress = json.load(f)
start_scroll = progress.get('last_scroll', 0)
print(f" 📌 从第 {start_scroll + 1} 次滚动继续")
for scroll_idx in range(start_scroll, max_scroll):
# 采集逻辑...
# 保存进度
with open(progress_file, 'w') as f:
json.dump({'last_scroll': scroll_idx}, f)
多关键词并行(谨慎使用)
python
from concurrent.futures import ThreadPoolExecutor
import time
def crawl_keywords_parallel(keywords, max_workers=2):
"""多关键词并行采集(每个关键词开一个独立浏览器)"""
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [
executor.submit(crawl_keyword, kw, XhsStorage())
for kw in keywords
]
for future in futures:
future.result()
特别提醒: max_workers 建议不超过 2,同时开多个浏览器对本机内存压力很大,对服务器也会有较明显的并发压力,更容易触发封禁。
数据分析:找出高潜力选题
python
# analyze.py
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv('data/notes_露营装备_20250211.csv')
# 1. 点赞数分布
print(df['likes'].describe())
# 2. 筛选高赞笔记(点赞 > 1万)
top_notes = df[df['likes'] > 10000].sort_values('likes', ascending=False)
print(f"\n高赞笔记(>{10000}赞): {len(top_notes)} 篇")
print(top_notes[['note_title', 'author', 'likes']].to_string())
# 3. 标题词频分析
from collections import Counter
import jieba
all_titles = ' '.join(df['note_title'].dropna())
words = jieba.cut(all_titles)
word_count = Counter(w for w in words if len(w) > 1)
print("\n标题高频词 Top10:")
for word, count in word_count.most_common(10):
print(f" {word}: {count}次")
1️⃣2️⃣ 总结与延伸阅读
我们完成了什么?
通过这个项目:
- ✅ 搞清楚了 SPA 类网站与静态网站的本质区别,理解了"直接 requests 拿不到数据"的根本原因
- ✅ 掌握了 Playwright 驱动真实浏览器的核心技巧(反检测、等待策略、滚动翻页)
- ✅ 实现了一套稳定的笔记元数据采集框架,支持多关键词、增量去重、断点续爬
- ✅ 了解了网络拦截这个更优雅的进阶方案
这个项目让我意识到的几点
爬虫开发里最耗时的往往不是写代码,而是分析页面结构和调试选择器。建议养成一个习惯:在写任何解析代码之前,先花 10 分钟把页面 HTML 保存下来慢慢看,不要一边运行一边猜。
另外,Playwright 方案虽然稳,但运行成本确实高。如果你的采集需求是一次性的,跑完就好,Playwright 够用了。如果需要每天定时运行、长期维护,最终还是要投入时间研究 API 逆向,因为浏览器自动化方案在服务器上部署成本较高,且对内存和 CPU 的要求也更高。
下一步可以做什么?
技术方向:
- 研究小红书网页端的 API 签名机制(
X-S参数),写一个纯 requests 版本 - 把 Playwright 采集改为 CDP(Chrome DevTools Protocol)直接协议,速度更快
- 接入 Scrapy-Playwright 中间件,利用 Scrapy 的管道和调度器
数据分析方向:
- 结合 jieba 分词,分析热门笔记的标题模式(数字、疑问句、情绪词)
- 用封面图 URL 批量下载封面,分析高赞笔记的视觉风格(颜色、构图)
- 建立关键词-笔记热度时序数据库,观察话题生命周期
推荐参考
工具文档:
- Playwright Python 官方文档:https://playwright.dev/python/
- BeautifulSoup 文档:https://www.crummy.com/software/BeautifulSoup/
扩展阅读:
- Chrome DevTools Protocol 文档(了解浏览器底层机制)
- Web Scraping with Playwright(O'Reilly 博客系列)
最后的话
小红书爬虫是我见过的"说起来简单,做起来有门道"的典型案例之一。入门的人以为加个 User-Agent 就能搞定,然后一看 HTML 里什么都没有,一脸茫然。出门的人知道 SPA 渲染、API 签名、反指纹检测是怎么回事,写出来的代码才真的能跑。
这中间的差距,其实就是对"一个网页请求背后到底发生了什么"这件事的理解深度。希望这篇文章能帮你把这个理解再往深推一层。
技术的边界不是你能写什么代码,而是你能看懂什么问题。
🌟 文末
好啦~以上就是本期的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
✅ 专栏持续更新中|建议收藏 + 订阅
墙裂推荐订阅专栏 👉 《Python爬虫实战》,本专栏秉承着以"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一期内容都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴 :强烈建议先订阅专栏 《Python爬虫实战》,再按目录大纲顺序学习,效率十倍上升~

✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】等写成某期实战?
评论区留言告诉我你的需求,我会优先安排实现(更新)哒~
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)
✅ 免责声明
本文爬虫思路、相关技术和代码仅用于学习参考,对阅读本文后的进行爬虫行为的用户本作者不承担任何法律责任。
使用或者参考本项目即表示您已阅读并同意以下条款:
- 合法使用: 不得将本项目用于任何违法、违规或侵犯他人权益的行为,包括但不限于网络攻击、诈骗、绕过身份验证、未经授权的数据抓取等。
- 风险自负: 任何因使用本项目而产生的法律责任、技术风险或经济损失,由使用者自行承担,项目作者不承担任何形式的责任。
- 禁止滥用: 不得将本项目用于违法牟利、黑产活动或其他不当商业用途。
- 使用或者参考本项目即视为同意上述条款,即 "谁使用,谁负责" 。如不同意,请立即停止使用并删除本项目。!!!
