☰
Selenium模拟浏览器抓取小红书:从环境搭建到风控实战
2026/10/2 10:55:03 网站建设 项目流程

简介:这是一份基于Selenium模拟浏览器行为的小红书关键词搜索与笔记爬取项目源码及配套文档说明,定位为Python课程期末大作业、毕业设计或新手进阶爬虫练习的参考工程。资源包共4个文件,压缩包约52KB,包含一个可直接运行的Jupyter Notebook爬虫主程序(.ipynb)、一份说明数据字段与运行思路的README文档(.md)、以及爬取结果导出的JSON和CSV样例(.json、.csv),既能看到抓取与解析过程,也能直接观察最终数据结构。项目采集小红书笔记的常见字段,包括作者昵称、粉丝数、获赞与收藏数、笔记内容、发布日期、封面图片、评论数、收藏数、笔记URL、用户URL等十余项信息,并在文档中逐项说明字段含义,便于二次开发或迁移改写。该资源已有135人学习下载,适合想掌握Selenium动态页面抓取、关键词搜索、翻页处理及结构化数据落地的学习者参考。

1. Selenium模拟浏览器行为抓小红书:为什么专业人士选择它

当你想监控某个关键词在小红书的笔记趋势,或者分析竞品笔记的互动结构,直接用 requests 抓取会发现返回的 HTML 几乎没有笔记数据。小红书把数据放在异步接口里,请求头还加了加密签名,光靠模拟 HTTP 请求需要逆向风控算法,维护成本很高。Selenium 模拟浏览器行为则绕开这一层:它驱动真实浏览器打开搜索页,执行滚动、点击、读取渲染后的 DOM,每次都把小红书当成“人工访问”来处理。这个方案特别适合营销分析、内容运营、学术研究场景里需要结构化采集笔记的人,也是新手能最快跑通的一条小红书爬虫路径。下面的步骤我都以 Python + Selenium 4.x 为例,并会说明整套项目源码通常需要包含哪些模块。

2. 环境选型和最小可用脚本:先让浏览器自己搜一次关键词

2.1 为什么选 Selenium 而不是 requests

很多第一篇小红书爬虫教程是拿 requests 直接打搜索页 URL,然后正则匹配 HTML。这套路在小红书改版前可行,改版后基本翻车。原因有两点。

第一,搜索结果的笔记列表是异步加载的,初始 HTML 里只有页面框架,真正的笔记卡片要等浏览器执行 JavaScript 后才出现在 DOM 里。requests 拿到的是一张空壳。

第二,小红书大部分数据接口的请求参数需要签名,签名算法随版本更新,社区里公开的算法常常过段时间就失效。Selenium 模拟浏览器行为等于把这块黑匣子交给浏览器自己处理,我们不关心接口怎么签名,只关心最终渲染出来的内容。

有人会问:能不能用 requests 去 hook 页面发起的 XHR 请求,直接从 JSON 里拿数据?这个思路确实更轻量,很多实际项目也在用,比如社区里常见的 mediacrawler 类项目,就是抓 HTML 里内嵌的 JSON 结构。但它的维护成本高于 Selenium:接口路径一变、字段结构一变,解析代码就要跟着改。Selenium 的优势在于它模拟的是用户视角,只要页面能看,爬虫就能跑,页面结构变化的影响范围被限制在 CSS 选择器这一层。

当然,Selenium 不是银弹。它慢,单实例每秒只能处理几篇笔记;它也不等于能在风控面前隐身,频繁操作一样会触发滑块验证。但它是把“能不能可靠拿到数据”这个问题先解决掉的路径,适合中小批量采集和低频率监控。对新手来说,Selenium 的资料密度也远高于其他方案,出了问题在网上搜“selenium 小红书 爬虫”基本能看到前人的血泪经验;项目源码和文档说明的重点,恰恰应放在选择器维护和风控规避这两块,而不是把时间耗在加密参数逆向。另外,这套方案也常被用来做只抓笔记不抓图的轻量采集——这是很多课程项目源码选择的边界。不要把详情页图集解析塞进搜索采集主流程,搜索模块专注拿列表字段,图集单独开一个详情页采集函数,这样任何一块出问题都能单独重跑。

2.2 安装 Selenium 与驱动:用 webdriver-manager 保证版本匹配

常见做法是先用 pip 装两个包:selenium 和 webdriver-manager。Selenium 负责控制浏览器,webdriver-manager 负责自动下载与本地 Chrome 版本匹配的驱动,省去手动找 chromedriver 的麻烦。

pip install selenium webdriver-manager

装完后先确认浏览器版本。Selenium 4.6 以上自带 Selenium Manager,直接调用webdriver.Chrome()也能自动匹配驱动;但网络环境下自动下载偶尔会失败,所以我更习惯显式指定 Service,失败时能看到更明确的报错。

from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager options = Options() options.add_argument('--start-maximized') options.add_experimental_option('excludeSwitches', ['enable-automation']) options.add_argument('--disable-blink-features=AutomationControlled') service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service, options=options) driver.get('https://www.xiaohongshu.com') print(driver.title) driver.quit()

这段代码里,--disable-blink-features=AutomationControlled是用来隐藏“浏览器正被自动化控制”的标记,降低被识别的概率;excludeSwitches去掉页面右上角那个“Chrome 正受到自动测试软件控制”的黄色提示条。ChromeDriverManager().install()首次运行会下载匹配的驱动,之后走本地缓存,启动速度会快很多。

如果运行时报session not created: This version of ChromeDriver only supports Chrome version X,说明驱动和浏览器版本不匹配。虽然 webdriver-manager 一般能自动对齐,但浏览器刚升级完大版本时,驱动缓存可能还没刷新。解决办法是删掉 webdriver-manager 的缓存目录,Linux 下通常在~/.wdm,Windows 在%USERPROFILE%\.wdm,再重跑一次;或者手动下载对应版本驱动,写死到 Service 的路径参数里。

提示:如果公司网络有限制,装驱动会一直超时。解决办法是手动从 chromedriver 镜像站下载对应版本,把路径填到Service('完整路径')里。

还有一个常见版本坑:老教程里写webdriver.Chrome(executable_path='...'),这在 Selenium 4.10 之后已经废弃,会直接抛异常。新写法是 Service 传路径。项目文档说明里如果要用旧例子,一定要标注 Selenium 版本,否则新手会在这一步卡很久。

2.3 打开小红书搜索页并提取第一屏笔记

别急着写完整爬虫。最小验证目标只有一个:让浏览器自己打开搜索页,并拿到第一屏笔记卡片的数量。

from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from urllib.parse import quote keyword = "秋冬面霜" url = f"https://www.xiaohongshu.com/search_result?keyword={quote(keyword)}" driver.get(url) wait = WebDriverWait(driver, 10) cards = wait.until( EC.presence_of_all_elements_located( (By.CSS_SELECTOR, "section.note-item") ) ) print(f"第一屏抓到 {len(cards)} 篇笔记")

quote(keyword)负责把中文关键词做 URL 编码,不编码的话地址栏中文会被浏览器自动转义,虽然也能访问,但一旦混入空格和特殊符号就容易出问题。WebDriverWait比固定time.sleep(5)更可靠:页面加载慢就多等,加载快就立刻继续,后面所有章节我都优先用显式等待。

这里选择器section.note-item是当前版本搜索结果页的笔记卡片容器。注意,小红书的类名不是稳定的接口,可能某次改版后变成div.note-item,所以 3.2 节我会给出更稳的定位策略。如果你的脚本需要无人值守,可以加上options.add_argument('--headless=new')开启无头模式;但我建议第一次调试始终开着界面,等选择器和流程都稳定后再切无头,否则风控和定位问题会叠加在一起,很难排查。Selenium 4 里还有page_load_strategy参数可选none或eager,对搜索页这种重前端页面,用默认的normal最稳。第一屏数量不能作为采集完成标准,因为搜索结果是滚动加载的,第一屏通常只有 18 到 20 篇,后面要处理完整的笔记列表。

3. 搜索结果页解析:笔记字段提取与结构化存储

3.1 搜索 URL 参数与滚动加载规律

小红书搜索页的 URL 参数比较多,大部分爬虫脚本只需要关心四个:keyword、source、scope。

参数取值示例作用
keywordurlencode 后的中文搜索关键词,必填
sourceweb_search_result_notes限定结果类型为笔记
scopeall / notesall 表示含用户与话题,notes 只看笔记
page无搜索页不通过参数翻页,而是滚动加载

常见做法是拼成https://www.xiaohongshu.com/search_result?keyword=秋冬面霜&source=web_search_result_notes。如果不加source,页面上会出现“用户”“话题”“笔记”多个 tab,解析逻辑就要多写好几套。需要注意 URL 参数顺序在多数情况下不影响后端解析,但keyword中间如果有特殊符号,比如空格或#,必须用quote处理。之前我看到有人直接把keyword=秋冬 面霜拼进去,结果#之后的内容全被浏览器当成锚点,搜索词被截断;这类问题在文档说明里写清楚,能帮接手的人省很多排查时间。

翻页不是“下一页”按钮,而是滚动触底加载。模拟浏览器行为的价值就在这里:真实用户往下翻,浏览器不断发请求,新的笔记卡片被追加进 DOM。

import time, random for _ in range(5): driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") time.sleep(random.uniform(1.5, 2.5))

每次滚动到页面底部触发新的数据加载。time.sleep用随机区间而不是固定值,避免被风控算法捕捉到“机械节奏”。滚动 5 次大概能加载 40 到 60 篇笔记,具体数量取决于屏幕高度和网络速度。判断是否加载完毕,可以监听每个section.note-item的数量:连续两次滚动后数量没变化,基本可以认为到底了。为了防止极端情况下页面无限加载,循环里加一个计数上限,比如最多滚动 10 次,超过就强制结束并导出当前数据。这样即使风控让你停在某一屏,脚本也不会空跑一整晚。

3.2 笔记卡片的 DOM 定位:CSS 选择器与 XPath 怎么选

搜索页里每张笔记卡片的结构大致是:外层容器、封面链接、标题、作者昵称、互动数据。我一般用 XPath 定位外层容器,因为contains(@class, "note-item")可以容忍类名变动。

cards = driver.find_elements( By.XPATH, "//section[contains(@class, 'note-item')]" )

拿到卡片列表后,每张卡片内部的标题、作者、链接、点赞数需要二次定位。这个环节最容易踩的坑是:标题元素可能同时存在a.title和span.title两种标签;互动数的 class 改版频率更高。稳妥的办法是对每个字段做 try 捕获,获取不到就填空字符串。

def _text(driver, css): try: return driver.find_element(By.CSS_SELECTOR, css).text.strip() except Exception: return "" def _attr(driver, css, attr): try: return driver.find_element(By.CSS_SELECTOR, css).get_attribute(attr) except Exception: return "" for idx, card in enumerate(cards[:20], start=1): title = _text(card, "a.title span, span.title") link = _attr(card, "a.cover", "href") print(idx, title, link)

CSS 选择器a.title span, span.title是逗号分隔写法,两个选择器匹配到的元素都算数,find_element返回第一个。封面链接的href属性里包含笔记 ID,这是后面去重的关键。之所以把_text_attr单独抽成函数,是因为页面上空元素也会被find_element命中断言失败,这样做可以让一个字段解析异常不影响整个卡片。

CSS 选择器和 XPath 的区别在这里很实际:CSS 简洁,适合定位单层结构;XPath 的contains部分匹配适合应对前端频繁改 class 的情况。对于整页只有一个容器的场景,两者差别不大;对于要在卡片里钻取作者、点赞这类多级节点的场景,XPath 的兜底能力更强。如果cards为空,先确认两件事:第一,是不是被验证码拦截了,页面标题是否变成了安全验证;第二,是不是 class 名调整了,打开开发者工具用Ctrl+F搜索note-item看看还在不在。顺序一定是先看验证码再看选择器,因为验证码页面同样会出现选择器匹配为空。类名变化的另一个常见点是a.cover:老版本是a.cover,新版可能带a.cover.ld这类附加标记,_attr里用 XPath 判contains(@class, 'cover')更稳。

3.3 标题、作者、互动数提取与 JSON 落盘

互动数在小红书页面里不是一开始就渲染出来的,很多卡片要滚动到可视区后数字才会出现。所以正确顺序是:先滚动到底部加载全部卡片,再逐张滚动到卡片位置取数字,最后统一提取。

def _count_to_int(text): t = text.strip() if not t: return 0 if t.endswith("万"): return int(float(t[:-1]) * 10000) if t.endswith("亿"): return int(float(t[:-1]) * 100000000) return int(t.replace(",", "")) notes = [] for card in cards: driver.execute_script( "arguments[0].scrollIntoView({block:'center'});", card ) time.sleep(random.uniform(0.5, 1.0)) notes.append({ "title": _text(card, "a.title span, span.title"), "author": _text(card, "a.author .name, .author .name"), "url": _attr(card, "a.cover", "href"), "likes": _count_to_int(_text(card, ".like-wrapper .count")), "collects": _count_to_int(_text(card, ".collect-wrapper .count")), "comments": _count_to_int(_text(card, ".comment-wrapper .count")), }) with open("notes.json", "w", encoding="utf-8") as f: json.dump(notes, f, ensure_ascii=False, indent=2)

scrollIntoView把卡片拉到屏幕中央,触发互动数渲染。_count_to_int处理“1.2万”这类展示文案:先判断结尾单位,再转成整数。这里要注意点赞数偶尔会显示成“1.2万”,也偶尔是纯数字,不处理的话后面排序分析会出错。

落盘用ensure_ascii=False保证中文可读,indent=2方便人工复核。轻量采集阶段 JSON 文件足够用了,等字段变多、需要按关键词和日期查询时再迁到 SQLite。落盘时建议顺手加一个crawled_at字段写入当前时间戳,这个字段在后面做增量对比时非常有用——同一个关键词,今天抓到的笔记数和昨天对比,就知道内容更新速度;也能在数据清洗时快速剔除上周的脏数据。

一个小红书图片提取的常见需求也在这个阶段处理:a.cover的href是笔记详情页 URL,拆出笔记 ID 后,可以拼一个详情页采集函数,从详情页里收集图集地址,存成images.json。这个函数应当在主流程之外,避免一次搜索把几百张图片地址都带回列表解析里,拖慢整体速度。互动数抓全为 0 时别怀疑小红书数据造假,先检查是不是加了headless模式导致懒加载不触发。无头模式下,Chrome 会跳过部分可见性计算,scrollIntoView的触发效果和真实窗口有差异,把 headless 关掉再跑一轮,数字就会出来。

4. 登录态、风控与参数调优:把反爬概率压到最低

4.1 扫码登录态保持与 Cookie 复用

小红书不登录时能看搜索结果,但笔记正文、完整图集、作者主页信息都会被打折扣。想要长期可靠抓取,第一步是先登录一次,把 Cookie 保存下来,后续脚本直接复用。

import json driver.get("https://www.xiaohongshu.com") input("扫码登录完成后按回车继续...") cookies = driver.get_cookies() with open("cookies.json", "w", encoding="utf-8") as f: json.dump(cookies, f, ensure_ascii=False, indent=2)

第二次运行就不要再弹浏览器了,直接从文件加载 Cookie:

driver.get("https://www.xiaohongshu.com") with open("cookies.json", "r", encoding="utf-8") as f: cookies = json.load(f) for cookie in cookies: driver.add_cookie(cookie) driver.refresh()

这里有个关键细节:add_cookie之前必须先get一次目标域名,否则浏览器会报“不能为无效域设置 Cookie”。另外 Cookie 的有效期是有限的,小红书的风控会定期让旧 Cookie 失效,表现为“页面能打开但内容变成登录提示”。这时候重新扫码一次即可,不用改任何代码。建议每次脚本结束时把最新 Cookie 再落盘一次,覆盖旧文件——因为有些服务端 cookie 是滑动续期的,抓得越久,有效期被续得越长。

扫码登录真正痛苦的是验证环节:二维码图片出现后,如果脚本界面是无头模式,你根本看不到二维码。所以登录过程绝不能在 headless 下做,建议单独跑一个“登录后保存 Cookie”的脚本,采集主脚本不带登录逻辑。这也是项目源码文档说明里常见的设计——把登录、采集、解析拆成三个模块,各自独立维护。搜出来的内容是否完整,也跟账号状态强相关:普通账号和新注册账号能看到的结果字段会有差异,比如有的账号能直接看到评论数,有的只显示“评论>1000”。如果你只做趋势分析,这类字段缺失不影响整体结论;但文档说明里如果承诺了完整字段,就要在登录态检查这里多写一层断言。

4.2 行为模拟三件套:随机延迟、滚动节奏、浏览器指纹

小红书风控算法的核心判断依据,是行为节奏和浏览器指纹。人做不到每次都等完全相同的间隔,也做不到十几秒把页面翻到底。所以你的爬虫要尽量“像人”。延迟方面,time.sleep(random.uniform(1, 3))是底线,稳定运行推荐拉到uniform(2, 5),同一个关键词翻页间隔尤其要大一点。滚动方面,不要一次scrollTo到底,改成步进式滚动:

for i in range(1, 9): driver.execute_script( f"window.scrollTo(0, {i * 800});" ) time.sleep(random.uniform(0.8, 1.5))

800像素和三屏一停是我常用的节奏,屏幕高度不同可以换成window.innerHeight * 0.8。步进滚动的意义是让浏览器产生多次中间态布局,和真实翻页一致;一步到底的滚动会被风控算法判定为机械行为。如果发现某个关键词搜索特别容易触发验证,还可以把步数减半、每步延迟提高到 2 秒以上。页面打开速度与网络环境相关,不用刻意等待页面完全加载,用WebDriverWait等待第一个卡片出现即可开始滚动;如果 20 秒内都没等到,可以跳过一个页面重来,避免无限等待拖垮整体循环。

浏览器指纹方面,除了第二章里那两个参数,还可以设置真实的窗口大小。窗口尺寸要符合常规显示器比例,1920,1080或者1366,768都好过默认的800x600。有条件的话,给配置加一个固定 user-agent,比如用当前 Chrome 版本的 UA,而不是让 Selenium 自动暴露默认值。如果常规参数挡不住风控,社区里普遍采用的办法是换 undetected-chromedriver 这类 patch 版驱动,它可以隐藏更多自动化特征。但这类轮子维护情况参差不齐,新版 Chrome 发布初期可能不可用。我的建议是:先把频率降下来,90% 的滑块问题靠“慢”就能解决。鼠标轨迹要不要模拟?我的观察是,搜索和滚动场景不需要模拟鼠标轨迹,因为真实用户在这些场景里不一定有鼠标动作;但点击翻页或点赞时需要。小红书的滑块验证则相反,自动轨迹几乎必被识别,手动处理反而存活率高。这个判断没有严格依据,属于经验层面,但值得记录在你的项目文档里作为决策参考。人脸识别风控之类的更高阶机制目前不用考虑,小红书主要卡频控和指纹,把这两样做好已经能应对绝大多数采集场景。

4.3 数据去重与增量更新:笔记 ID 是唯一主键

一次搜索跑完,下次想追更新,不能全量重抓然后整体覆盖。小红书笔记 URL 里带唯一 ID,比如/search_result/64b1c2d4000000001903a1b2,最后一段 24 位十六进制字符串就是笔记 ID。已有集合里出现过的 ID,直接跳过。

import os, json seen_path = "seen_ids.json" seen = set(json.load(open(seen_path))) if os.path.exists(seen_path) else set() new_notes = [] for card in cards: link = _attr(card, "a.cover", "href") note_id = _extract_note_id(link) if not note_id or note_id in seen: continue seen.add(note_id) new_notes.append(parse_card(card)) with open(seen_path, "w", encoding="utf-8") as f: json.dump(list(seen), f, ensure_ascii=False)

_extract_note_id常见做法是link.rstrip("/").split("/")[-1],但要注意有的 URL 带查询参数,需要先用urllib.parse.urlparse(link).path把路径部分拆出来再取最后一段。增量保存单独维护一个seen_ids.json,既能做去重,也是后续统计“本次新增多少篇”的依据。这个思路和很多小红书 id 解析工具内部做的其实是同一件事:从分享链接里把核心 ID 提取出来,只是我们把它接进了自己的采集流程。

存储上,如果项目文档说明要求更正式的数据结构,可以把 seen_ids 也同步写进 SQLite:

import sqlite3 conn = sqlite3.connect("xhs.db") conn.execute( """CREATE TABLE IF NOT EXISTS note_seen ( note_id TEXT PRIMARY KEY, crawled_at TEXT DEFAULT CURRENT_TIMESTAMP )""" ) conn.executemany( "INSERT OR IGNORE INTO note_seen(note_id) VALUES (?)", [(nid,) for nid in seen] ) conn.commit()

INSERT OR IGNORE是幂等写入,重复跑同一批数据不会报错。把去重从内存搬到数据库,脚本重启不丢状态,几百个关键词的持续采集也扛得住。网络抖动会导致某一篇笔记的互动数没取到,不要立刻判定失败。常见做法是给提取结果加一个校验:title和url都为空就重试当前卡片,重试两次仍为空才跳过。重试要控制次数,配合time.sleep(random.uniform(1, 2)),否则反而触发风控。

5. 避坑指南:小红书 Selenium 爬取的 5 个常见问题进行排查

前面那么多代码能跑通,不代表能一直跑通。以下 5 个坑是我在 Selenium 爬小红书过程中实际遇到最多的,按“现象 → 原因 → 解决”的顺序写,方便你排查时对照。

5.1 滑动验证码反复弹窗

现象:脚本跑几分钟后页面弹出滑块验证,手动拖一次后,跑不了几页又弹。

原因:访问频率超过小红书风控算法阈值,或者浏览器指纹带有自动化特征。无头模式下弹窗概率更高,因为 headless 的渲染行为和真实 Chrome 差异明显。

解决:优先降频,把连续请求间隔拉到 5 秒以上,连续翻页不超过 10 次就主动休息 60 秒。不去使用无痕模式,无痕浏览器缺少本地用户数据文件,本身就是一个风险特征。如果仍然频繁弹窗,将验证码出现后的逻辑改成人工介入——检测到滑块元素就停下来,用input()等待人工拖完再继续。不要尝试自动执行滑块轨迹,小红书对轨迹特征的校验很严格,包括拖动速度、停顿点和加速度曲线,自动轨迹反而会让账号被重点标记。

5.2navigator.webdriver属性暴露

现象:浏览器能打开,但一进搜索页就被重定向到验证码,在地址栏执行navigator.webdriver返回true。

原因:Selenium 注入的 webdriver 特性会被页面脚本检测到。--disable-blink-features=AutomationControlled能解决部分版本的问题,但 Chrome 升级后可能失效。

解决:先验证这个属性:

print(driver.execute_script("return navigator.webdriver"))

如果输出True,除了添加启动参数,还可以在页面加载前执行一段 JS 把属性覆盖掉:

driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { "source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})" })

execute_cdp_cmd是 Selenium 对 Chrome DevTools 协议的透传,addScriptToEvaluateOnNewDocument保证在页面任何脚本执行前就把属性覆盖。这个手段在大多数 Chrome 版本上有效,但属于对抗性质的操作,建议只在确有需要时使用,不要把它当成常规配置。

5.3 图片懒加载导致图片地址抓不到

现象:提取卡片封面src时拿到一串空字符串,或者拿到data:image/gif占位图。

原因:小红书对图片用了懒加载,src只在图片进入视口后才填充真实地址,之前是占位符。

解决:滚动到卡片位置后再取src,如果还是空,检查有没有>driver = None try: driver = webdriver.Chrome(service=service, options=options) run_crawl(driver) finally: if driver: driver.quit()

日志方面,在创建 Service 时把日志输出重定向到空设备,并关闭日志输出:

import subprocess service = Service( ChromeDriverManager().install(), service_args=["--log-level=OFF"], log_output=subprocess.DEVNULL )

这个是小细节但特别救命。长时间线上跑采集的人都知道,半夜翻车往往不是抓不到数据,而是磁盘被日志写满导致整机卡死。

6. 进阶技巧:多实例并发与数据完整性验证

6.1 并发浏览器实例:两个就够

有人图快,把 selenium 爬虫用线程池开 8 个浏览器实例,结果往往是被风控集体拉黑,所有实例一起弹验证码。比较好的做法是把并发控制在 2 到 3 个,每个实例用独立的用户数据目录:

options.add_argument(f"--user-data-dir=/tmp/xhs-profile-{index}")

这里的index是实例编号,目录不能是多个实例共享的路径,否则 Chrome 会报“Profile 正被其他进程使用”。多实例对比单实例的提升大约在 1.8 倍左右,越往上加收益越小,风险越大。真正的瓶颈不是浏览器数量,而是风控允许的总请求速率。

6.2 用统计分布验证数据完整性

抓完数据别急着交给业务方。先用简单统计看数据有没有明显偏斜:按关键词分组统计笔记数、按作者统计发文数、查看互动数的分布。一套正常数据里,头部笔记数量少但互动极高,长尾笔记互动是个位到两位数。如果发现所有笔记互动数都是 0,大概率是提取选择器没适配改版后的页面,而不是真的没人点赞。抽样人工验证也必须做。随机挑 10 条笔记,打开原链接核对标题和作者,误差率超过 5% 就要回头改选择器。数据质量是爬虫项目能不能被信任的生命线,这步不省。

6.3 把笔记图集 URL 单独落一份存档

小红书图片提取是这个方向被问得最多的需求之一。在笔记详情页收集到的图集 URL,除了写进 notes 主表,我还会再单独存一份images.json,包含 note_id、图片序号、完整 URL。好处是图片下载失败时可以单独重跑下载任务,不用再次打开详情页;后续做内容库迁移、去重删冗余,这份独立存档也方便机器直接处理。这个技巧来源于一次真实教训:一开始把所有字段混在一个表里,图片源站失效后要清洗一张几万行的表,后悔药都没得吃。

最后想说一句:Selenium 模拟浏览器跑小红书,是少数“理论简单但运行时充满变量”的爬虫场景——选择器会变、风控在变、Cookie 生命周期也在变。我的习惯是把登录、搜索、解析、存储拆成独立模块,遇到页面改版只改定位层,业务逻辑不动。第一次跑通这样一套工程后,你会理解为什么课程项目会把它评为高分方向——它兼具完整的工程链路和真实的对抗场景。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询