拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Selenium模拟浏览器爬取小红书:从环境搭建到反爬避坑实战

Selenium模拟浏览器爬取小红书:从环境搭建到反爬避坑实战

简介:这是一份基于Selenium模拟浏览器行为的小红书关键词搜索与笔记爬取项目源码,附带完整文档说明,特别适合作为Python期末大作业、课程设计或毕业设计的参考。项目实现了关键词搜索、笔记详情与作者维度数据的采集,覆盖作者获赞收藏数、粉丝数、笔记数、评论数、正文内容、发布日期、封面图、点赞收藏数、笔记URL与用户URL等十余个关键字段,并在文档中逐一说明,便于理解字段含义和二次扩展。压缩包共四个文件,包括ipynb格式的爬虫主程序、md格式的使用说明,以及json、csv两种爬取结果样例,能够直观对比不同格式下的字段组织方式,整体体积仅52KB,轻量易读。目前已有135人学习下载。对希望掌握Selenium模拟操作、熟悉小红书热门笔记数据采集流程的Python开发者来说,这是一份高性价比的入门与实战参考。

1. 用 Selenium 模拟浏览器爬小红书:为什么绕不开这条慢路子

小红书的关键词搜索和笔记详情页,几乎全部由 JavaScript 动态渲染,直接拿 requests 请求 HTML 拿不到笔记列表,拿到的也是空壳。Selenium 模拟浏览器行为等于把浏览器整个搬过来,JS 照常执行、接口照常请求,能看到什么就能爬到什么,这是它在这个场景下最核心的价值——把反爬问题从「破解加密参数」降维成「伪装成真人操作」。项目标题里「Selenium 模拟浏览器行为」这几个字,本质上就是在选型上替你做了决定:不碰逆向、不走协议,用真实浏览器跑真实点击和滚动。

这个方案最适合三类人:一是做竞品调研和选题分析的内容运营,想按关键词批量收集笔记标题、正文、标签和互动数据;二是刚入门爬虫的 Python 开发者,想找一个能完整跑通「搜索—翻页—进详情—解析—落地」全流程的练手项目;三是需要给团队做小红书数据监测工具的产品或后端工程师。它的代价也很明显——慢,单个关键词翻几十页可能要几分钟,比接口直爬慢一个数量级。但只要把登录态、滚动节奏和选择器维护好,它比任何协议模拟方案都稳,因为你不是在跟风控对着干,是在模拟一个正常用户。

2. 搭建 Selenium 采集环境:浏览器参数和登录态是两张入场券

2.1 安装三件套:selenium、webdriver-manager、Chrome 本体

设备上先确认有 Python 3.8+ 和 Chrome 浏览器。常见做法是用 webdriver-manager 自动匹配驱动版本,省去手动下载 chromedriver 对不上版本的麻烦。安装命令如下:

pip install selenium webdriver-manager

webdriver-manager 的核心作用是自动检测本机 Chrome 的版本号,并按版本下载对应的 chromedriver 二进制文件。这个过程只需要执行一次,之后驱动文件会缓存在用户目录下,不需要每次启动都重新下载。

只有 selenium 和 webdriver-manager 还不够,浏览器本身要能跑起来。Windows 和 macOS 直接装官方 Chrome 即可,Linux 服务器如果是精简环境,需要额外安装 chromium,这里不多展开。装好之后可以跑一个最简脚本验证驱动能不能正常拉起浏览器:

from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager options = webdriver.ChromeOptions() options.add_argument("--start-maximized") driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options) driver.get("https://www.xiaohongshu.com") print(driver.title) driver.quit()

这段代码里,ChromeDriverManager().install() 负责定位或下载匹配当前 Chrome 的驱动,Service 把驱动路径交给 Selenium 去启动。如果直接跑通并打印出页面的 title,说明环境没问题。接下来要做的是给浏览器加反检测参数,这是后面能不能稳定采集的分水岭。

2.2 ChromeOptions 里的关键参数:为什么裸启动必被风控

直接裸启动 Chrome 访问小红书,大概率会卡在登录墙或者滑块验证上。原因是无头浏览器和默认 WebDriver 模式会暴露自动化特征,服务端检测到这些特征直接判为机器。我一般会在启动时传入这一组参数:

options = webdriver.ChromeOptions() options.add_argument("--disable-blink-features=AutomationControlled") options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option("useAutomationExtension", False) options.add_argument("--window-size=1400,900") options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36") options.add_argument("--lang=zh-CN") options.add_argument("--disable-gpu") options.add_argument("--no-sandbox")

逐个说明:--disable-blink-features=AutomationControlled用来关掉 Chrome 的自动化控制标记,这是反检测的第一道门槛;excludeSwitches去掉页面里navigator.webdriver标志,否则你在控制台执行window.navigator.webdriver返回的是 true,很容易暴露;window-size不要省略,窗口太小会影响懒加载;user-agent 建议用当前 Chrome 版本对应的 UA,别用默认的 HeadlessChrome;--no-sandbox一般只在 root 用户或容器环境下需要,本地开发可加可不加。

提示:这些参数只是降低被识别概率,不是免死金牌。小红书的运行商风控算法会综合评估浏览器指纹、行为轨迹和频率,参数只能去除明显的自动化特征,真正的安全靠后文要讲的操作节奏。

2.3 登录态复用:扫码一次,Cookie 管一周

小红书不登录时搜索会被限制得很厉害,翻几页就弹验证码。最省事的办法是手动扫码登录一次,把 Cookie 序列化到本地,之后每次启动直接加载,不需要重复扫码。核心逻辑分两段:先登录并存 Cookie,再加载 Cookie 进首页。

存 Cookie 的代码:

import json import time from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By from webdriver_manager.chrome import ChromeDriverManager options = webdriver.ChromeOptions() options.add_argument("--disable-blink-features=AutomationControlled") options.add_experimental_option("excludeSwitches", ["enable-automation"]) driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options) driver.get("https://www.xiaohongshu.com") time.sleep(30) # 这 30 秒是留给你手动扫码的窗口 with open("xhs_cookies.json", "w", encoding="utf-8") as f: json.dump(driver.get_cookies(), f, ensure_ascii=False, indent=2) print("cookie saved:", len(driver.get_cookies())) driver.quit()

30 秒的 time.sleep 不能写成死等,更好的做法是循环轮询检测当前 URL 是否从登录页跳转到了首页,或者检测页面里是否出现用户头像元素。不过对于教学项目,sleep 足够直观。保存下来的 cookies 会在后续每次启动时被重新注入。

加载 Cookie 的代码:

import json from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager options = webdriver.ChromeOptions() options.add_argument("--disable-blink-features=AutomationControlled") options.add_experimental_option("excludeSwitches", ["enable-automation"]) driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options) driver.get("https://www.xiaohongshu.com") # 先打开主域,确保 Cookie 作用域正确 with open("xhs_cookies.json", "r", encoding="utf-8") as f: cookies = json.load(f) for cookie in cookies: driver.add_cookie(cookie) driver.refresh() time.sleep(3) print("当前页面:", driver.current_url)

这里有一个关键顺序:必须先访问一次小红书主页面拿到页面上下文,再 add_cookie,最后 refresh。如果直接往空页面塞 Cookie,浏览器会因缺少域名上下文而抛异常。加载成功后,driver 就处于登录态,后续所有搜索请求都带身份信息,被弹验证码的概率大幅下降。

3. 关键词搜索与笔记列表:从输入关键词到拿到全部笔记链接

3.1 搜索 URL 的构造方式与参数含义

小红书网页版的搜索结果是 URL 驱动的,不需要模拟输入框打字再回车。直接在地址栏访问下面这个格式即可:

https://www.xiaohongshu.com/search_result?keyword=露营装备&type=51

keyword是搜索词,需要做 URL 编码;type=51表示只搜笔记,不搜用户和商品。type=1综合、type=61搜索用户、type=57搜商品,实际采集用 51 最干净。用 driver.get 直接跳转到这个 URL,比在输入框里 send_keys 再按回车少一步交互,也少一个被检测的点。

from urllib.parse import quote keyword = "露营装备" search_url = f"https://www.xiaohongshu.com/search_result?keyword={quote(keyword)}&type=51" driver.get(search_url)

用 quote 对中文关键词做编码的目的是避免 URL 里出现非 ASCII 字符,有些代理环境和服务端会因此拒绝请求。这个 URL 是列表爬取的总入口,后续所有翻页行为都以这个页面为基础。

3.2 等待策略:显式等待是爬虫的救命稻草

页面跳转到搜索结果后,笔记列表是异步渲染的,不是立刻出现在 DOM 里。这时候如果直接找笔记卡片元素,大概率抛 NoSuchElementException。两种常见的等待方式区别很大:

隐式等待 poll_frequency 固定、影响全局,但遇到特定元素加载慢或不存在时会浪费固定的等待时间。显式等待可以精确等到「笔记卡片出现」这个条件成立才继续执行,效率高且可控。项目里我全部用显式等待,不碰隐式等待。

from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By wait = WebDriverWait(driver, 15) cards = wait.until( EC.presence_of_all_elements_located((By.CSS_SELECTOR, "section.note-item")) ) print("本页笔记卡片数:", len(cards))

这里的section.note-item是搜索结果页里笔记卡片的外层容器,小红书改版后一直沿用这个类名。如果这个选择器在你的浏览器里失效——小红书改版频繁是出了名的——可以在页面里右键检查,找到包含笔记标题和链接的区块元素,把选择器替换成新的。写成独立变量存起来,后续维护只改一处。

3.3 滚动加载翻页:模拟人手滚动,不点「下一页」

小红书搜索结果页不像传统 PC 端网页有点击翻页按钮,而是无限滚动加载。滚到底部会自动加载下一批笔记,直到没有更多内容。模拟滚动需要循环执行一段固定脚本:

import time import random def scroll_to_bottom(driver): """模拟真人滚动,分步向下滚动直到出现'没有更多'提示。""" scroll_pause_time = random.uniform(1.5, 2.5) current_scroll = 0 step = random.randint(400, 700) while True: driver.execute_script(f"window.scrollBy(0, {step});") current_scroll += step time.sleep(scroll_pause_time) page_text = driver.find_element(By.TAG_NAME, "body").text if "没有更多" in page_text or "已到达底部" in page_text: print("滚动结束,已翻完所有笔记") break if current_scroll > 80000: # 安全上限,防止死循环 break time.sleep(2)

几个参数的设计逻辑:滚动步长 400 到 700 像素是随机取的,模拟真实鼠标滚轮或触控板滚动的变速感;每次滚动后停顿 1.5 到 2.5 秒,给浏览器留出发送请求和渲染新内容的时间,不要滚完立刻找元素。安全上限 80000 像素对应大概 100 多篇笔记的量级,超过这个值直接跳出,防止页面异常时 while 循环卡死。

滚动结束后,页面上所有已加载的笔记卡片都在 DOM 里,这时统一提取链接最稳妥:

cards = driver.find_elements(By.CSS_SELECTOR, "section.note-item") note_links = [] for card in cards: a_tag = card.find_element(By.CSS_SELECTOR, "a[href*='/explore/']") href = a_tag.get_attribute("href") note_links.append(href) print("共获取笔记链接:", len(note_links))

注意 href 里包含的是/explore/路径,这不是笔记详情页的直接地址。正常情况下点击这个链接会跳转到详情页,Selenium 采集时直接 driver.get 这个链接也能打开,但页面内部会做一层跳转。更好的做法是先拿到这些链接,等进入详情页后再逐步解析。

4. 笔记详情解析与结构化落地:把渲染后的页面变成可用的数据

4.1 详情页信息提取:标题、正文、点赞、收藏、标签

拿到笔记链接列表之后,逐条打开并提取信息。详情页的结构相对固定,但内容区域是动态渲染的,需要等核心 DOM 出现再操作。解析时优先提取以下字段:笔记标题、正文文本、发布时间、点赞数、收藏数、评论数、标签列表、图片地址。

driver.get(note_url) time.sleep(random.uniform(2, 4)) title_el = driver.find_elements(By.CSS_SELECTOR, "#detail-title") desc_el = driver.find_elements(By.CSS_SELECTOR, "#detail-desc, .desc") like_el = driver.find_elements(By.CSS_SELECTOR, ".like-wrapper .count") fav_el = driver.find_elements(By.CSS_SELECTOR, ".collect-wrapper .count") title = title_el[0].text.strip() if title_el else "" desc = desc_el[0].text.strip() if desc_el else "" like_count = like_el[0].text.strip() if like_el else "0" fav_count = fav_el[0].text.strip() if fav_el else "0"

#detail-title是详情页标题的 ID,#detail-desc是正文容器的 ID,这两个 ID 在小红书 PC 端页面结构里非常稳定,属于详情页的标志性元素。.like-wrapper .count和.collect-wrapper .count是互动数据区,如果页面改版导致选择器失效,可以在页面内搜索「点赞」「收藏」这些文字找到包裹元素,再往上层找 class。

用 find_elements 而不是 find_element 的好处是:元素缺失时不会立刻抛异常,而是返回空列表,配合 if 判断让程序继续跑。小红书的互动数据有时会出现「10万+」这种格式化文本,落到数据表后统一处理成数字。

4.2 图片提取与格式转换:webp 转 jpg 的真实原因

小红书图片地址默认带格式转换参数,直接拿到的 src 是.webp格式。本地存储或后续做数据分析时,webp 兼容性不如 jpg,而且小红书图片 CDN 支持裸地址回源,把 URL 里的格式后缀改掉就能拿到原始 jpg。

import re import requests def parse_image_urls(driver): imgs = driver.find_elements(By.CSS_SELECTOR, "div.swiper-slide img, div.img-container img") urls = [] for img in imgs: src = img.get_attribute("src") if src and src.startswith("http"): clean_url = re.sub(r"!.*$", "", src) clean_url = clean_url.replace(".webp", ".jpg") urls.append(clean_url) return urls

正则里去掉!及其后面的尾参,这条逻辑处理的是小红书图片 CDN 上类似xxx.jpg!webp的带参 URL。去掉参数后,能拿到原始图片;再把.webp后缀换成.jpg,就是浏览器能直接渲染的常规格式。图片是否需要真下载,取决于项目需求。如果只是做文本分析,保留 URL 就够了;如果要存档,再走 requests 流式写入本地。

4.3 数据落地:按笔记维度写 Markdown 文件和索引表

项目要求「笔记爬取」,落地格式我建议两件套:每篇笔记生成一个独立 Markdown 文件,保留完整正文和图片引用;同时维护一份全局索引 CSV,记录笔记 ID、标题、链接、点赞数、收藏数、抓取时间,方便后面对数据做筛选排序。

import csv import os from datetime import datetime def save_note(note_id, title, desc, tags, img_urls, note_url): # 保存 Markdown 正文 md_path = f"notes/{note_id}.md" os.makedirs("notes", exist_ok=True) with open(md_path, "w", encoding="utf-8") as f: f.write(f"# {title}\n\n") f.write(f"链接:{note_url}\n\n") f.write(f"标签:{', '.join(tags)}\n\n") f.write(desc + "\n\n") for i, url in enumerate(img_urls): f.write(f"![image_{i}]({url})\n\n") # 追加写入全局索引 CSV with open("index.csv", "a", encoding="utf-8", newline="") as f: writer = csv.writer(f) writer.writerow([note_id, title, note_url, like_count, fav_count, datetime.now().isoformat()])

newline="" 是 Windows 下防止 CSV 写出时出现空白行的常见坑。note_id 可以从 URL 里提取,小红书笔记 URL 通常是/explore/{id}或/discovery/item/{id}格式。如果解析失败,直接用 md5 对 URL 取哈希作为笔记 ID,保证唯一。

5. 小红书反爬与 Selenium 常见踩坑排查

5.1 现象:启动后浏览器窗口一闪而过就退出

驱动刚启动,窗口闪现一下立刻关闭,没有任何报错输出。最常见的原因是 chromedriver 与 Chrome 主版本不匹配。webdriver-manager 只在网络正常且能访问驱动下载源时才能拉对版本,某些内网环境它拿不到匹配驱动,就会启动失败。

解决:先确认本机 Chrome 版本,再手动指定驱动版本安装。Chrome 地址栏输入 chrome://version 查看版本号,然后到对应的驱动镜像源下匹配版本。如果 webdriver-manager 缓存了旧驱动,删掉用户目录下.wdm文件夹再重试,强制重新拉取。

5.2 现象:搜索页能打开,但笔记卡片列表始终为空

URL 没问题、页面能加载、滚动也执行了,但查 DOM 节点就是 0 个。先排除选择器失效——很多小红书页面改版后 class 名换掉。但更隐蔽的问题是:滚动脚本执行太快,JS 渲染还没完全挂载,presence_of_all_elements_located只查了当时已存在的节点,后续加载出来的卡片没被捕捉。

解决:把滚动和等待循环拆分,滚动一次、等待一次、提取一次,不要在滚动结束后才统一提取。如果页面字面上已经显示笔记数量但 DOM 里查不到,说明页面结构可能进入了「防爬假页面」,某些反爬策略会给可疑客户端返回一个空壳页面,此时检查当前页面截图和driver.page_source长度,确认是不是被重定向到了验证页。

5.3 现象:翻页翻到一半突然弹出登录墙或滑块验证

前面几十篇笔记都正常,突然出现滑块或者扫码登录提示。这不是 IP 被封,也不是账号异常,而是行为频率太高,风控算法判定操作速度不像真人。短视频平台的内容页加载一般有 500 毫秒到 2 秒的间隔,真实用户翻页不会每 1 秒就滚一次屏。

解决:把固定 sleep 全部改成随机区间延时,滚动步长也随机化。我一般用random.uniform(2, 5)作为页面切换的等待时间,滚动步长在 400 到 800 像素之间随机。另外每次搜索词采集完,强制休息 20 到 40 秒再做下一组搜索。

5.4 现象:连续采集 30 条笔记后被提示「操作过于频繁」

即使延时做得足够随机,也会出现操作频率限制的报错,页面提示「操作过于频繁,请稍后再试」。这种情况通常是账号维度的采集频控,跟浏览器指纹关系不大。

解决:先停 10 分钟以上再继续,短时间重试没有意义。更根本的做法是多账号轮询——给每个账号单独建一个 user-data-dir 目录,启动时通过options.add_argument("--user-data-dir=./profile_01")给每个 Chrome 进程分配独立缓存目录,多个账号轮流切换登录状态。注意:这是工作量较大的运维操作,个人学习项目不必加,但做正式数据服务这是一个绕不开的架构选择。

5.5 现象:详情页图片地址解析出来 404 或打不开

解析出来的图片地址在浏览器里直接打开报 404。原因通常是图片 URL 里的签名参数在跳转后被剥离,或者用 requests 请求时没有带 Referer 头,小红书 CDN 会拒绝无来源请求。

解决:requests 下载图片时补上Referer: https://www.xiaohongshu.com请求头,同时带上笔记页 URL 的 Origin。图片链接的完整度问题则需要在解析时判断 URL 是否以http开头,并用正则清理尾参,两处同时做才能保证图片下载稳定。

6. 并发提速与数据校验:从单线程脚本到可用的采集工具

单线程逐条打开详情页确实慢,但小红书风控对并发也格外敏感。常见的提速做法是 multiprocessing 多进程,每个进程单独启动一个 Chrome 实例,用不同的--user-data-dir,进程间互不共享 Cookie 和浏览器缓存。核心代码骨架:

from multiprocessing import Pool def worker(keyword_group): # 每个 worker 独立启动 driver、加载独立 profile driver = create_driver_with_profile(f"profile_{keyword_group[0]}") for keyword in keyword_group[1]: collect_keyword(driver, keyword) driver.quit() return len(keyword_group[1]) if __name__ == "__main__": groups = [("keyword_group_1", ["露营装备", "折叠桌", "营地灯"]), ("keyword_group_2", ["钓鱼椅", "天幕", "气垫床"])] with Pool(2) as pool: results = pool.map(worker, groups) print("各组完成关键词数:", results)

并发数控制在 2 个进程以内,开太多会导致 CPU 和内存迅速占用,还会被风控精准打击。每个进程处理一组独立关键词,组间互不干扰,这样即使某一组被限流,其他组的数据照常产出。

采集完成后的校验环节别省。检查每次保存的 index.csv 和 notes 目录里 md 文件数量是否一致;随机抽 3 到 5 篇笔记,打开 md 文件核对标题是否为空、正文是否完整、图片引用是否可达。小红书页面改版后,选择器失效是最容易「静默失败」的环节——程序跑完没报错、但数据一堆空值,这种翻车体验我经历过不止一次。我的习惯是每次正式采集前用小批量测试跑 5 篇,人工核对字段无误后再放开全量跑。

最后一条建议来自我自己的血泪经验:这个项目写到能稳定采集,留给自己的使用边界要想清楚。小红书平台对自动化采集有明确限制,个人学习、学术研究和小范围数据分析是主流用法,产出数据不要用于商业售卖或公开发布,采集频率也要克制到不对平台造成压力。这套代码的架构和排查思路,换个合规数据源同样能复用——Selenium 模拟浏览器的能力和审美,才是项目源码之外真正值钱的东西。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表