复杂网页爬虫实战:电商数据采集与反爬策略解析
2026/9/9 10:09:59 网站建设 项目流程

以前帮朋友做电商选品分析的时候,最先遇到的一个拦路虎就是怎么把商品数据快速拿到手。电商页面看起来简单,真下手去写爬虫的时候,你会发现它跟普通的静态博客完全是两个物种——商品详情、价格、SKU、评论,这些信息往往不是一次性渲染出来的,而是通过异步接口分多次加载,甚至整张价格表都做了字体反爬。你拿 requests 直接请求页面,拿到的只是一堆接口占位符和空标签。

这篇文章整理的是我在这类“复杂网页数据采集”项目里的完整落地经验,会从需求拆解讲到工具选型,再一路拆到具体的处理策略、数据清洗思路和反爬应对方案。适合那些遇到“页面能打开、代码却采集不到有效字段”这类问题的开发者,也适合准备入行爬虫、对动态网页和信息提取有好奇心的新手。我会尽量少讲空泛的概念,多给可直接抄走的代码和思路。

1. 项目拆解与整体设计思路

1.1 先把“复杂页面”这件事说透

电商复杂网页和普通网页最大的区别在于,它不是一个文档,而是一个应用。你看到的每一个商品卡、每一段价格、每一格库存状态,背后可能对应着好几个独立接口。前端为了追求加载速度和交互体验,会把首屏内容先渲染出来,剩下的数据等用户滚动、点击再请求。这就导致你直接用 requests 去拉 HTML 源码,经常看到的是 “加载中” 之类的水印值,转头发现数据并不在源代码里。

我通常会把复杂页面分成三类来理解:

  • 接口动态加载型:数据在 XHR 请求里,页面 HTML 只是空壳,需要找接口规律。
  • 浏览器渲染型:数据由 JavaScript 渲染生成,接口可能加密,必须依赖真浏览器环境。
  • 混合加固型:既有接口加密又有前端脚本检测,比如计算环境指纹、检测 webdriver 标识。

电商网站大多数都处于第二种和第三种之间。明白了这一点,你就不会再用“发个包拿 HTML”的老思路去硬碰硬,而是会提前把工具链锁定在浏览器自动化方向。

1.2 需求拆解:先定目标再动手

很多爬虫项目翻车,不是技术不行,而是需求根本没有拆清楚。做电商数据采集,第一步不是写代码,而是把“我要什么”拆到直接变成字段级需求。

比如一个商品采集任务,至少要确认四件事:

  1. 采集对象具体是哪个平台、哪个类目、哪些关键词结果页。
  2. 需要哪些字段:标题、价格、销量、店铺名、评论数,还是详情页里的规格参数。
  3. 数据深度:只抓列表页,还是需要点进每个商品的详情页。
  4. 数据更新频率:是一次性存量采集,还是每周定时增量。

把这些写清楚之后,再去设计技术方案。很多人上来就写大而全的爬虫框架,最后发现某个关键字段根本拿不到,等于白干。我自己习惯先人工打开目标页面,打开浏览器开发者工具,先把接口和页面结构摸一遍,再去动键盘。

提示:一开始不要急着上 Scrapy 这类重型框架。复杂网页项目建议先用脚本把单页面跑通,确认字段都能稳定取到之后,再考虑框架化和并发加速。

1.3 技术路线选型:从单页脚本到分布式扩展

技术路线没有银弹,只有匹配需求的选择。单页采集、中小规模数据量,用 Playwright 或 Selenium 脚本就够了。需要每天跑成千上万条数据,再考虑 Scrapy 加 Playwright 集成,或者引入消息队列做分布式调度。

我做第一版的时候只用了 Playwright 加 pandas,流程非常简单:访问列表页 → 翻页 → 解析所有商品节点 → 进入详情页补充字段 → 存 CSV。后来数据量上来,才把其中比较稳定的部分抽出来放到 Scrapy 里,用 Item Pipeline 做入库和去重。方案演进的顺序很重要,不要一上来就搭 Mesos、Kafka 那套,小项目撑不起来,维护成本还高。

2. 工具选型与环境准备

2.1 浏览器自动化的老面孔与新选择

传统做动态页面爬取,最常用的是 Selenium。它确实是工程化的老牌方案,社区资料全,遇到问题基本都能搜到答案。但它的缺点也很明显:每次都要拉起一个完整的浏览器实例,资源占用高;启动速度慢;而且自动化特征比较明显,容易被网站的风控系统识别。

后来我接触了 Playwright,它由微软维护,API 设计更现代化,支持自动等待、拦截请求、直接操作浏览器上下文。拿它来采集电商页面,最舒服的一点是它把“等待元素出现”这个动作做成了内置行为,不用像 Selenium 那样频繁自己写WebDriverWait配合expected_conditions。再加上它自带无头模式、UA 伪装和浏览器指纹处理能力,一次性把很多反爬问题解决在源头。

除了这两个,Python 生态里还有几个新势力的选择值得关注。

  • DrissionPage:把 requests 的快速和浏览器 automa 的稳定性结合在一起,适合需要在普通请求和浏览器渲染之间切换的场景,代码写法也更贴近 Python 习惯。
  • curl_cffi:擅长模拟浏览器 TLS 指纹,对某些从 TLS 层做校验的网站有奇效。
  • Playwright-Stealth:给 Playwright 加了一层反检测补丁,专门处理 webdriver 标识、自动化相关属性暴露这类问题。

2.2 我最终选择的组合方案

这个项目里我用的核心组合是 Playwright + Python,配套 requests 处理纯接口场景。为什么不用 Scrapy?因为当时首版目标只需要采集约几万条商品数据,而且页面动态渲染比重高,Scrapy 的优势发挥不出来。用小脚本快速验证,才是最关键的目标。

实际环境大体如下:

  • Python 3.10
  • Playwright 1.40 左右版本
  • pandas 做表格输出
  • openpyxl 用来写 Excel
  • 平时调试用 Jupyter,正式跑用命令行脚本

环境安装可以用下面这组命令完成:

pip install playwright pandas openpyxl playwright install chromium

安装后建议先用下面的代码验证浏览器能正常启动:

from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() page.goto("https://example.com") print(page.title()) browser.close()

headless 参数建议在调试阶段设成 False,方便肉眼看页面表现;确认一切稳定再切回 True。

2.3 为什么我不建议每个项目都上分布式

有些文章特别喜欢渲染“分布式爬虫”这种词,好像不搞一套分布式架构就不叫专业爬虫。这里我说句大实话:电商复杂页面的瓶颈往往不在并发不够,而在反爬触发概率。你单机跑 100 个并发请求,很可能跑到第 30 个就触发验证码;但你把并发控制在 3 到 5,可能稳稳跑一整天也没事。分布式解决的是规模问题,不是绕过问题。先把单机的频率控制、请求伪装、重试策略做好,永远比堆机器更重要。

3. 核心实现:从请求到结构化数据的完整流水线

3.1 请求上下文管理与cookies预热

电商网站的数据采集,第一步不是写提取逻辑,而是先把浏览器的 “人味” 做足。一个刚启动的全新浏览器实例,cookie 是空的,localStorage 是空的,连字体列表都和正常用户不一样。这种干净环境很容易被识别。

我习惯先创建一个持久化上下文目录,把登录后的 cookie 和缓存存下来,后续每次启动都复用。Playwright 里可以直接指定user_data_dir

from playwright.sync_api import sync_playwright with sync_playwright() as p: context = p.chromium.launch_persistent_context( user_data_dir="./user_data", headless=False, viewport={"width": 1366, "height": 768}, user_agent="Mozilla/5.0 ... Chrome/120.0 Safari/537.36", locale="zh-CN", ) page = context.new_page() page.goto("https://example.com")

首次运行时,我手动在页面里完成一次登录,甚至模拟几次正常的浏览行为,让网站给这个上下文打上“可信用户”的标签。之后的采集任务就复用这个 context,能有效降低被强制验证的概率。这个操作做起来非常简单,却经常被忽视。

提示:重试机制里要引入随机等待,不要用固定time.sleep(2)。更符合人体操作习惯的是在 1 到 3 秒之间随机分布,间隔太规律反而像机器。

3.2 请求拦截与响应等待策略

采集动态网页时,页面加载并不等于数据加载完成。你打开商品列表,页面框架先出现,接口可能几百毫秒后才返回数据,图片可能更慢。如果不等数据就解析,拿到手的就是空值。

Playwright 的page.expect_response方法特别适合处理这种场景。我可以在触发滚动或者点击“加载更多”之前,先注册一个等待条件,等指定接口返回了再继续执行。这个方法比固定等几秒可靠得多,因为网络波动大的时候,固定等待不是早了就是晚了。

示例:

with page.expect_response(lambda resp: "api/item/list" in resp.url and resp.request.method == "GET"): page.click("text=加载更多") response = page.expect_response(...) data = response.json()

还有一种方式是直接拦截响应体,把原本要由页面处理的 JSON 内容提前拿住。我在某些需要直接拿价格、库存的场景里,会在页面脚本执行前就拦截接口响应,用响应 JSON 作为主要数据源,用页面上的 DOM 文本做二次校验。这样两头保险,能明显减少字段丢失。

3.3 动态加载与无限滚动处理

电商列表页很多都是无限滚动设计。它们不在 URL 上翻页,而是靠你不断往下滚,触发接口去加载下一页数据。这种情况下,最直接的方式是用循环模拟滚动,然后监听接口返回,直到满足结束条件。

核心逻辑可以是这样:

  1. 获取当前页面的 DOM 节点数量。
  2. 执行鼠标滚轮滚动到页面底部。
  3. 等待新节点出现或者接口返回。
  4. 判断是否还有“加载更多”按钮。
  5. 重复直到没有新数据或者达到最大页数。

用小步滚动替代一次滚到底,是因为有些网站对极端快速的滚动会有行为检测。正常用户的滚动速度是渐进的,所以我在代码里用mouse.wheel(0, 600)每次滚 600 像素,中间间隔 0.3 秒。这个方法跑下来,触发反爬的频率要比一次滚动到底低很多。

3.4 数据提取:从页面或接口中拿字段的三种思路

数据提取部分,我通常按照页面复杂度分成三层思路。

第一层,静态页面字段,直接走 XPath 或 CSS 选择器。这一层最简单,适合标题、店铺名、销量这类稳定结构。XPath 的容错能力比 CSS 更强,比如包含匹配、按文本定位元素,这些在电商页面里非常常用。

title = page.locator('xpath=//div[@class="item-title"]/a').inner_text()

第二层,动态字段在接口里。打开开发者工具,切到 Network 面板,刷新页面后看 XHR 或 Fetch 请求,找到返回 JSON 的那个接口,直接请求参数,解析 JSON。速度上远比浏览器渲染快。价格、库存、SKU 这类数据,接口一般会返回结构化字段,拿过来就是干净的 JavaScript 对象,不用做字符串清洗。

第三层,页面渲染和接口都对不上的情况。这种只能用 OCR 或图像识别,上衣场景主要是滑块验证码和字体反爬。我建议优先考虑绕开,比如寻找对应的内部接口、分析字体文件映射关系。真正落到 OCR 的部分尽量少,识别率不稳定,后期维护成本极高。

3.5 数据清洗与落库

采集不是终点,拿到数据之后还要清洗。我在项目里经历了几个典型坑:

  • 价格字段经常是 “¥ 199.00”,前面有货币符号,中间有空格。
  • 销量字段可能是 “5000+ 人付款”,我需要把“人付款”剥离掉。
  • 评论数可能是 “1.2万”,要把中文单位转成整数。
  • 部分字段在页面显示为“暂无”,解析后是空字符串。

所以我在 pipeline 阶段专门写了一个清洗函数,用正则表达式标准化数字和单位。清洗后的数据再转成 pandas DataFrame 做初步统计,最后写入 Excel 或者 MySQL。

import re def clean_price(raw): if not raw: return None return float(re.sub(r"[^0-9.]", "", raw)) def clean_sales(raw): if not raw: return 0 if "万" in raw: return int(float(re.sub(r"[^0-9.]", "", raw)) * 10000) return int(re.sub(r"[^0-9]", "", raw))

注意:入库一定要做去重。电商商品数据,列表页翻页时经常出现同一商品被多个入口重复收录,我直接按商品ID + 采集时间作为联合主键,避免重复数据污染分析结果。

4. 常见问题与排查技巧实录

4.1 元素定位失败:元素明明存在但取不到值

这个问题的出现频率最高。原因是页面用了 Shadow DOM 或 iframe。两种结构的 DOM 树跟普通页面不一样,常规的page.locator找不到内部元素。

排查方法很简单:去开发者工具里看元素节点,如果看到#shadow-root标签,然后展开里面还有元素结构,那就说明用了 Shadow DOM。此时可以用 Playwright 的 pierce 选择器,直接穿透 Shadow DOM 操作内部元素。

如果是 iframe,则要切换 frame 上下文。Playwright 里可以用page.frame_locator定位到 iframe 内部的元素,不需要手动切换上下文。

frame = page.frame_locator('iframe[src*="comment"]') comment_list = frame.locator(".comment-item")

4.2 网页卡在滑块验证或行为验证

滑块验证是电商平台最常见的反爬手段。遇到这个,我第一反应不是非得“绕过”,而是先看能不能通过调整行为把它触发概率降下来。实践证明,下面几个动作特别有效:

  • 不要用 headless=True,改用 headless=False 配合虚拟显示器。
  • 减少请求频率,调低并发数。
  • 在关键动作之间加入随机鼠标移动轨迹,不用瞬间跳转。
  • 避免固定 UA,要使用和浏览器版本匹配的真实 UA。

如果确实被限制,那就用页面上的点击操作去完成滑块。Playwright 模拟滑块动作的核心在于轨迹模拟,不要用匀速直线,那样反而假。我自己写过一个函数,采用“先慢后快再慢”的加速度轨迹,识别通过的占比有明显提升。

4.3 数据采集过程中页面崩溃或浏览器内存暴涨

长任务跑久了,浏览器实例占用的内存会一直涨。多标签页加上渲染缓存,很容易把本地电脑拖到卡顿。这个问题我在连续采集几万条数据时遇到过,最后靠“定时重启浏览器”解决。具体做法是每采集 500 或 1000 条数据,主动关闭当前 context,重新启动一个新的 context。这样缓存不会无限累积,内存占用能保持在一个稳定水平。

另外,如果采集过程中某个商品详情页跳转异常,不要立刻报错终止。更稳妥的做法是记录当前状态,把该 URL 放进失败队列,跑完一轮之后重新重试。我用一个简单的列表维护失败队列,重试超过三次才丢弃并记录日志。完整跑完的任务,失败率基本能控制在 0.5% 以下。

4.4 反爬识别升级:检测到自动化特征

有些网站会在请求链路或者浏览器对象上做手脚,检测navigator.webdriver是否为 true,或者检查浏览器窗口尺寸、触控支持、语言设置等特征。Playwright 默认的 headless 模式更容易暴露这些特征。

我的应对经验是:

  • 使用 launch_persistent_context 替代普通 launch,视觉上更像普通用户。
  • 手动给 context 设置permissionsgeolocationtimezone_idlocale这些参数,让环境一致性更强。
  • 引入 stealth.js 一类的脚本,在页面加载前先执行,覆盖掉典型的自动化属性。
  • 尽可能复用正常用户的 cookie 和指纹信息。

另外还有一个经验特别想分享:不要一上来就做高并发。电商类网站的反爬往往是阶梯式触发的,一开始可能没反应,并发一大就直接封你账号或 IP。我现在的习惯是,无论是模拟浏览器脚本还是纯接口采集,都从 1 个并发开始,逐步加到 2、3、5,一旦发现异常立刻退回安全水位。

4.5 常见问题速查表

现象可能原因处理建议
取到的字段为空数据由接口异步加载用 expect_response 等待接口返回再解析
同一条数据重复入库列表页重复收录按商品唯一 ID 去重
页面频繁弹验证码并发过高或指纹暴露降低并发,模拟真实滚动,复用持久化上下文
浏览器内存持续上涨长时间运行未重启每 N 条任务完成后重启 context
某商品详情页抓取失败页面结构特殊放入失败队列,统一重试
价格拿下来单位不对页面做了字体反爬解析字体文件映射或寻找内部接口
CSS 选择器定位不到组件在 Shadow DOM 内改用 pierce 选择器

4.6 合规提醒与数据使用边界

谈到爬虫,一定避不开合规问题。虽然我不打算在这里展开讲具体法律条款,但有几条底线遵守了能少走很多弯路。

第一,确保你的采集行为不违反目标网站的 robots 协议和用户协议,尤其是登录后才能看到的非公开数据。第二,不要采集个人敏感信息,比如用户手机号、收货地址、真实姓名这类内容。第三,采集频率要克制,不要影响目标网站正常服务。数据使用的边界要搞清楚,采集公开数据做分析是一回事,把数据重新包装后对外提供商业服务是另一回事。

这个原则贯穿整个项目始终。我每轮全量采集前,都会先做一个小范围测试,确认目标网站的响应速度没有被明显拖慢。这不只是讲技术道德的问题,也是保证采集任务能长期稳定跑下去的基础。

5. 从单机脚本到工程化落地的扩展建议

5.1 数据存储选型

如果数据量级从几万条涨到几百万条,pandas 那套方案就不够看了。CSV 文件读写会越来越慢,内存占用也会变大。这个阶段我会迁移到数据库存储。MySQL 适合结构化程度高、需要多人查询的业务数据;MongoDB 更适合商品快照、详情页 JSON 这类半结构化数据。

我自己比较喜欢用 MySQL 存商品主表,用 MongoDB 存每次采集的原始快照。原始快照的意义在于,即使后续分析和清洗逻辑变了,你还能从最原始的数据里重新提取字段,不至于后悔当初存少了。

5.2 定时调度与监控

工程化落地之后,定时任务、日志和监控就变得很重要。我每天固定凌晨跑增量采集,用 cron 或计划任务触发脚本。采集完成之后把结果发送到企业微信或钉钉机器人,失败率达到阈值就触发告警。

日志要记录几个关键信息:任务开始时间、结束时间、采集成功数、失败数、失败 URL 样例、执行过程中的异常堆栈。有日志才有排查依据。

5.3 数据采集与 AI 的结合点

最后聊一聊“AI 是爬虫技术的更深层次运用吗”这个话题。这两年大模型火起来之后,很多团队会把大模型放进数据采集链路里。比较常见的做法是让大模型充当“信息抽取器”,你不再需要为每种页面结构单独写解析规则,直接把 HTML 文本或者截图扔给模型,让它输出需要的 JSON 字段。这种方法在处理长尾网站、经常改版的页面时,能省很多维护精力。

但也要有心理准备:大模型抽取的准确率做不到 100%,而且推理成本比正则要高得多。我的建议是两条腿走路——结构稳定的重点页面用传统解析,保证速度和精度;结构经常变化或者没有规律的页面用大模型兜底。把它当作一个灵活组件,而不是全盘替代。

我再顺手分享一个自己用到比较好的流程:用 Playwright 把页面渲染成截图和 HTML 摘要,然后把摘要发给大模型,让它识别页面里面哪些区块对应商品标题、价格、评论。这个流程特别适合页面结构频繁变化、维护成本失控的项目。

写在最后的几点体会

做了这么多采集项目,最大感受是爬虫工程师的核心竞争力不在于会写多少解析规则,而在于具备“结构拆解”和“问题预判”的能力。拿到一个复杂电商页面,你能不能在五分钟内判断出它属于哪种加载方式、关键字段藏在 DOM 里还是接口里、风险点在哪里,这决定了整个项目的推进速度。

踩过的坑多了之后,我现在做任何采集任务都会遵守三条底线:一是策略克制,永远保证对目标站点友好;二是数据清洗仔细,不要拿到脏数据就往下游送;三是日志完善,出现问题能当天定位而不是靠猜。这些看起来不酷,但真正跑数据的人会明白,稳定比炫酷重要得多。

如果你正在做一个电商数据采集项目,我的建议是先拿一天时间把目标页面结构摸清楚,把接口列表列出来,再开始写脚本。别急着抄代码,这个准备工作做得越扎实,后面踩的坑就越少。

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

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

立即咨询