news 2026/10/21 3:42:05

动态渲染页面爬虫实战:商品名称、价格与评论数采集方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
动态渲染页面爬虫实战:商品名称、价格与评论数采集方案

1. 项目缘起与整体设计思路

电商平台的价格和评论数据,对于做市场分析、竞品监控或者选品调研的人来说,价值不用我多讲。但真正动过手的人都知道,这类页面跟早期的静态网页完全不是一回事——你右键查看源代码,会发现商品列表区域空空如也,价格、评论数这些关键字段压根不在初始HTML里。这就是典型的动态渲染页面,数据是通过JavaScript在浏览器端异步加载后再填充到DOM中的。

我这次要聊的,就是围绕“动态爬取商品名称、评论数、价格”这个需求,把整个方案从选型到落地完整走一遍。核心关键词就三个:动态渲染、字段提取、反爬应对。适合有一定Python基础、做过简单静态爬虫但一碰到JS渲染页面就卡壳的朋友,也适合想系统梳理动态采集思路的从业者。

先说清楚这个项目能解决什么问题。传统用requests直接请求URL拿HTML的方式,在动态页面上会失效,因为你拿到的是“骨架”,不是“血肉”。要拿到真实数据,有两条主流路线:一是逆向分析页面的异步接口,直接请求数据API;二是用浏览器自动化工具模拟真实渲染,等页面加载完再提取。这两条路各有适用场景,我在下面会详细拆。

整体设计上,我的思路是分层处理:先判断目标字段是否来自可逆向的接口,能逆向就优先逆向,因为速度快、资源消耗低;逆向成本太高(比如参数加密复杂、签名算法频繁变动)时,再退回浏览器自动化方案。字段提取层面,商品名称、评论数、价格这三个字段的DOM结构往往不在同一个容器里,需要分别定位,而且评论数经常是异步二次加载的,得单独处理等待逻辑。

提示:动态页面采集的第一原则是“先看网络请求,再动浏览器”。很多人一上来就上自动化工具,结果效率低还容易被识别,其实很多数据接口并没有想象中那么难逆向。

2. 动态页面采集的核心原理与方案选型

2.1 为什么静态请求拿不到数据

要理解动态采集,得先明白浏览器加载一个现代电商页面时发生了什么。你输入URL回车后,服务器返回的初始HTML其实只是个“壳”,里面包含了一堆<script>标签。浏览器解析这些脚本并执行,脚本再去向后端接口请求真正的商品数据,拿到JSON后再通过DOM操作把数据渲染到页面上。所以你在“查看源代码”里看到的是初始壳,而在“审查元素”里看到的是渲染后的完整DOM,两者根本不是一回事。

requests库只做了第一步——拿到初始HTML,它不会执行JavaScript。这就是为什么你用requests.get()拿到的文本里搜不到价格。理解这一点,后面的方案选择就顺理成章了:要么绕过渲染直接找数据接口,要么用能执行JS的工具。

2.2 两条主流路线的取舍

路线一:接口逆向。打开浏览器开发者工具的Network面板,筛选XHR/Fetch请求,刷新页面,观察哪些请求返回了包含商品数据的JSON。找到后,分析请求的URL、请求头、参数规律,然后用requests直接复现这个请求。这条路的优势非常明显:速度快(一个请求拿一页数据)、资源占用低(不需要启动浏览器)、数据干净(JSON直接解析)。缺点是有些平台会对请求参数做签名加密,逆向难度大,而且接口可能随时变动。

路线二:浏览器自动化。用Selenium、Playwright或DrissionPage这类工具驱动真实浏览器,让页面完整渲染后再从DOM里提取数据。优势是所见即所得,不用管接口怎么加密,页面显示什么就能抓什么。缺点是慢、吃内存,而且自动化工具本身有一些特征可能被识别。

我的实际选择是优先逆向,逆向不了再自动化。对于大部分中小型电商页面,接口往往没有强加密,直接请求JSON效率高得多。只有当遇到参数签名复杂、或者数据是服务端渲染混合客户端渲染的情况,才切换到自动化方案。

2.3 字段定位的基本功

不管走哪条路线,字段定位都是核心。商品名称通常在标题链接的文本里,价格往往在一个带特定class的<span>或<em>里,评论数则经常藏在“评价”字样旁边的数字节点中。这里有个经验:不要死记class名,因为class可能带哈希后缀随时变。更稳的做法是结合层级关系和文本特征来定位,比如“找到包含‘评价’文本的节点,取它附近的数字”。

用XPath或CSS选择器时,我习惯先用浏览器控制台验证选择器能不能唯一命中目标,再写进代码。这一步花两分钟,能省掉后面半小时的调试。

3. 接口逆向的实操细节与参数分析

3.1 抓包定位数据接口

打开开发者工具,切到Network面板,勾选XHR和Fetch过滤。刷新商品列表页,你会看到一串请求。逐个点开看Response,找那种返回JSON且里面包含商品名称、价格字段的请求。通常这类接口的URL里会带search、list、product之类的关键词,返回结构里会有items、products、data这类数组字段。

找到目标接口后,重点看三样东西:请求URL、请求方法、请求参数。URL告诉你接口地址,方法通常是GET或POST,参数里往往包含分页、关键词、排序等。把这些记下来,是复现请求的基础。

3.2 请求头的关键字段

直接拿URL去请求往往会失败,因为缺少必要的请求头。最关键的几个是User-Agent(标识客户端)、Referer(来源页)、Cookie(会话状态),有些接口还需要X-Requested-With: XMLHttpRequest来标识这是异步请求。我的做法是把浏览器里这个请求的请求头完整复制出来,先原样复现,跑通了再逐个删减,找出哪些是必需的。

这里有个细节:Cookie里可能包含动态生成的令牌,直接硬编码会过期。如果发现请求一段时间后失效,就要考虑用requests.Session()先访问一次首页拿到Cookie,再带着这个Session去请求接口。

3.3 参数规律与分页处理

分页参数是最常见的。有的用page=1&pageSize=20,有的用offset=0&limit=20,还有的用start=0&count=20。搞清楚每页多少条、页码从几开始,就能写循环批量拉取。我一般会先手动改参数请求两三次,确认分页逻辑,再写进代码。

如果参数里有看不懂的签名字段(比如sign、token、_t),先别急着放弃。有时候这个签名只是时间戳的简单变换,有时候是MD5拼接。可以搜索页面JS里生成这个参数的函数,看逻辑复不复杂。如果实在搞不定,就退回自动化方案,没必要死磕。

import requests import time session = requests.Session() headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://example-mall.com/list", "X-Requested-With": "XMLHttpRequest" } # 先访问首页建立会话 session.get("https://example-mall.com", headers=headers) for page in range(1, 6): params = {"page": page, "pageSize": 20, "keyword": "耳机"} resp = session.get("https://example-mall.com/api/search", headers=headers, params=params) data = resp.json() for item in data.get("items", []): print(item.get("name"), item.get("price"), item.get("commentCount")) time.sleep(1.5)

上面这段是接口逆向的骨架代码。注意time.sleep不能省,请求太密集容易触发风控。间隔设多少合适?我的经验是1到2秒比较稳妥,具体看平台敏感度。

4. 浏览器自动化方案的完整落地

4.1 工具选型:为什么我倾向Playwright

浏览器自动化工具里,Selenium资历最老、资料最多,但配置驱动、等待逻辑写起来比较啰嗦。Playwright是后起之秀,自带智能等待、支持多浏览器、API设计更现代,而且默认就带了一些降低自动化特征的措施。DrissionPage是国内开发者做的,把请求和浏览器控制结合得不错,中文文档友好。

我现在的默认选择是Playwright,主要因为它的wait_for_selector和自动等待机制省心,不用到处写sleep。如果你团队已经在用Selenium,也没必要换,原理是相通的。

4.2 页面等待策略

动态页面最大的坑就是“数据还没加载完就提取”。解决办法是显式等待目标元素出现。Playwright里用page.wait_for_selector("选择器"),Selenium里用WebDriverWait配合expected_conditions。关键是等待的选择器要选对——等那个最终会包含数据的节点,而不是一开始就存在的骨架节点。

评论数这类字段经常是二次加载的,可能商品名称和价格都出来了,评论数还在转圈。这时候要么单独等评论数节点,要么用wait_for_load_state("networkidle")等网络请求基本停歇。后者简单但可能等太久,前者精准但要多写几行。

4.3 字段提取与数据清洗

页面渲染完后,提取就相对直接了。用选择器拿到元素,再取文本。但要注意几点:价格文本可能带货币符号和千分位逗号,评论数可能带“条”“+”这类后缀,提取后要做清洗转成数字。商品名称可能带多余空格或促销标签,也要处理。

from playwright.sync_api import sync_playwright import re def parse_number(text): if not text: return 0 nums = re.findall(r"[\d.]+", text.replace(",", "")) return float(nums[0]) if nums else 0 with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto("https://example-mall.com/list?keyword=耳机") page.wait_for_selector(".product-item", timeout=15000) items = page.query_selector_all(".product-item") for item in items: name = item.query_selector(".title").inner_text().strip() price = parse_number(item.query_selector(".price").inner_text()) comment = parse_number(item.query_selector(".comment").inner_text()) print(name, price, comment) browser.close()

这段代码里,parse_number函数负责把“¥1,299.00”和“2.3万条评价”这类文本转成数字。注意“万”这个单位要单独处理,否则会算错。实际写的时候我会加一个判断,遇到“万”就乘以10000。

4.4 滚动加载的处理

很多商品列表是滚动到底部才加载下一页的,没有分页按钮。这时候需要模拟滚动。做法是循环执行window.scrollTo(0, document.body.scrollHeight),每次滚动后等一小段时间,直到页面高度不再变化,说明加载完了。Playwright里可以用page.mouse.wheel(0, 5000)或者直接执行JS。

注意:滚动加载的页面,去重很重要。因为滚动过程中可能重复渲染同一批数据,提取时要按商品ID或名称去重,否则统计会出错。

5. 反爬应对与稳定性优化

5.1 请求频率控制

不管走哪条路线,频率控制都是第一道防线。接口逆向时,请求间隔设1到3秒随机;自动化时,操作之间加随机延迟。固定间隔容易被识别出机器规律,随机化能降低风险。我一般用random.uniform(1, 3)生成间隔。

5.2 请求头与指纹伪装

接口逆向时,请求头要尽量贴近真实浏览器,User-Agent用常见的Chrome版本,Accept、Accept-Language这些也补上。自动化时,Playwright默认的浏览器指纹已经比较接近真实,但如果平台检测严格,可以考虑用playwright-stealth这类插件进一步降低特征。

5.3 异常重试与断点续爬

网络请求失败是常态,必须加重试机制。我的做法是用tenacity库或者自己写个装饰器,失败后等几秒重试,最多重试三次。对于大批量采集,还要做断点续爬——把已采集的页码或ID记录到文件里,中断后从断点继续,避免重复劳动。

问题类型表现应对策略
请求被拒返回403或验证码页降低频率、更换请求头、建立会话
数据为空接口返回空数组检查参数、Cookie是否过期
元素找不到选择器超时检查是否动态加载、调整等待策略
数据重复同一条记录多次出现按唯一ID去重
字段错位价格和名称对不上检查是否在循环内重新定位元素

5.4 数据存储与去重

采集到的数据我一般先存CSV或SQLite,方便后续分析。存之前按商品ID去重,避免重复采集导致数据膨胀。如果数据量大,可以考虑存MySQL或MongoDB。字段上至少保留商品ID、名称、价格、评论数、采集时间,方便做价格趋势分析。

6. 常见问题排查与实操心得

6.1 接口能跑通但数据不全

这种情况通常是分页没处理完,或者接口有隐藏的筛选参数。先确认总页数,再检查是否有category、sort这类参数影响了返回结果。还有一种可能是接口对未登录用户只返回部分数据,这时候需要带上登录后的Cookie。

6.2 自动化跑着跑着就卡住

多半是等待策略有问题。要么是某个元素一直没出现导致超时,要么是页面弹出了登录框或验证码遮挡了目标元素。我的排查顺序是:先截图看页面当前状态,再检查等待的选择器是否存在,最后看是否有弹窗需要关闭。

6.3 价格和评论数提取为0

先确认选择器命中的元素文本到底是什么。有时候价格在<em>里,你选了父级<span>,拿到的文本可能包含其他内容。评论数如果是“暂无评价”,parse_number会返回0,这是正常的,但要和“提取失败”区分开。我一般会打印原始文本,肉眼确认一下。

6.4 实操心得三条

第一,先小批量验证再全量跑。拿一页数据跑通整个流程,确认字段都对,再放开分页循环。这样出问题容易定位。

第二,日志要打全。每次请求的URL、返回状态码、提取到的条数都记下来,出问题时看日志比猜快得多。

第三,选择器写宽松一点。别用那种一长串带哈希class的选择器,用层级加文本特征的方式,页面小改版也不容易失效。

6.5 关于合规的提醒

采集公开的商品名称、价格、评论数用于个人分析,一般问题不大,但要控制频率,别对目标服务器造成压力。采集到的数据不要用于商业转售,也不要绕过平台的登录和权限限制去拿非公开数据。这是基本的从业底线,也是让这个技术能长期用下去的前提。

7. 字段解析的进阶技巧

7.1 评论数的单位换算

评论数经常以“万”为单位显示,比如“1.2万条评价”。解析时要判断文本里有没有“万”,有就乘以10000。还要注意“+”号,比如“100+”,这种是约数,存的时候可以保留原始文本,也可以取最小值100。我一般两个都存,原始文本用于展示,数值用于计算。

7.2 价格的多种格式

价格可能是“¥1299”、“1299.00”、“1,299”这几种格式。统一处理方式是先去掉货币符号和逗号,再转float。如果遇到“起”字,比如“1299起”,说明是区间最低价,存的时候标记一下。

7.3 商品名称的清洗

名称里常混有促销标签,比如“【限时特惠】”、“包邮”。如果做分析,这些标签会干扰关键词匹配。我的做法是保留原始名称,同时生成一个清洗后的名称字段,去掉方括号内容和常见促销词。

def clean_name(name): name = re.sub(r"【.*?】", "", name) for word in ["包邮", "限时", "特惠", "新品"]: name = name.replace(word, "") return name.strip()

这段清洗逻辑不复杂,但能明显提升后续文本分析的准确度。实际用的时候,促销词表可以根据目标平台的特点自己维护。

7.4 动态class的应对

有些平台的class名带随机哈希,比如price_ab3f2。这种选择器今天能用明天就废。应对办法是用属性包含匹配,比如[class*="price"],或者用XPath的contains(@class, "price")。更稳的是结合结构定位,比如“第三个div下的span”,但结构也可能变。没有一劳永逸的办法,定期检查选择器有效性是必要的维护工作。

8. 批量采集的工程化组织

8.1 配置与代码分离

把目标URL、选择器、请求头、采集页数这些放到配置文件里,代码只负责逻辑。这样换一个采集目标时,改配置就行,不用动代码。我用YAML或JSON存配置,读进来是个字典,用起来方便。

8.2 并发与限速的平衡

单线程采集慢,但并发太高容易被封。我的折中是接口逆向时用3到5个线程,配合每线程独立的请求间隔;自动化时基本单线程,因为浏览器本身资源占用就高。并发不是越高越好,稳定拿到数据才是目的。

8.3 数据校验

采集完一批数据后,做个简单校验:条数是否合理、价格是否在合理区间、评论数是否非负。发现异常数据先标记,别直接入库。我遇到过一次价格提取成了评论数,就是因为选择器写错位了,校验环节及时发现才没污染整个数据集。

8.4 定时任务的考虑

如果需要持续监控价格变化,可以把采集脚本做成定时任务,每天跑一次。用系统的计划任务或者调度框架都行。注意每次跑之前检查一下选择器是否还有效,平台改版是常态,脚本需要定期维护。

9. 我在实际项目中的几点体会

做这类动态采集,技术本身不是最难的,难的是持续稳定。平台会改版、接口会调整、风控会升级,今天能跑的脚本下个月可能就失效了。所以我在设计时会把“易维护”放在很重要的位置:选择器写宽松、配置抽出来、日志打详细、异常有重试。这样出问题时,改一处配置就能恢复,而不是从头重写。

另一个体会是不要追求一次抓全。先抓核心字段,跑通流程,再逐步加字段、加页数、加频率。贪多求快往往导致问题堆在一起,排查起来痛苦。我见过不少人一上来就想把整个平台的商品全抓下来,结果卡在反爬上,连第一页数据都没拿稳。

最后说个具体的:评论数这个字段,很多平台在列表页显示的是总数,但点进详情页才加载具体评论内容。如果只需要评论数,列表页就够了;如果要评论内容,那是另一个量级的工程,涉及详情页采集和翻页,复杂度高不少。搞清楚需求边界,能省很多无用功。

这个方向后续还能扩展的地方不少,比如把采集到的价格数据做成趋势图,或者结合评论数做热度排序。工具是死的,思路是活的,把动态采集这套流程跑通之后,换个目标平台无非是重新定位字段、重新分析接口的事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 3:40:24

edge-tts语音合成总报错?这份完整排查手册帮你锁定根因

edge-tts语音合成总报错&#xff1f;这份完整排查手册帮你锁定根因 【免费下载链接】edge-tts Use Microsoft Edges online text-to-speech service from Python WITHOUT needing Microsoft Edge 项目地址: https://gitcode.com/GitHub_Trending/ed/edge-tts 用 edge-tt…

作者头像 李华
网站建设 2026/10/21 3:40:24

Hive分桶从原理到实践:哈希取模、桶数设计与bucket map join调优

1. 分桶到底解决了什么问题&#xff1a;先忘掉建表语法1.1 一个大join任务从20分钟到4分钟的背后先说一个我印象很深的案例。某天早上&#xff0c;一位同事跑过来问&#xff1a;同样一张表&#xff0c;一样的SQL&#xff0c;为什么测试环境几秒钟就出来&#xff0c;生产环境要跑…

作者头像 李华
网站建设 2026/10/21 3:39:56

PyTorch量化入门:INT8静态量化与QAT实战全解析

去年我接的一个部署需求&#xff0c;遇到一个特别典型的问题&#xff1a;模型精度不错&#xff0c;但单张推理时间太慢&#xff0c;在只允许用 CPU 推理的服务器上根本扛不住并发。一开始我试着优化预处理、减少后处理逻辑、换更轻的骨干网络&#xff0c;效果始终有限。后来把模…

作者头像 李华
网站建设 2026/10/21 3:39:51

Linux压缩归档实战:tar、gzip、zip用法与避坑指南

做Linux维护这几年&#xff0c;“压缩、解压、归档”这几个字出现过太多次了。部署代码要先打包再传&#xff0c;备份数据要先压缩再落地&#xff0c;别人发来一个源码包&#xff0c;第一反应就是tar -xzvf一把梭。很多新手把“归档”和“压缩”当成同一件事&#xff0c;把tar、…

作者头像 李华
网站建设 2026/10/21 3:38:31

医疗NLP序列标注实战:用Flair落地电子病历实体识别

做医疗NLP时间长了&#xff0c;你会发现“序列标注”这四个字几乎每天都在碰。电子病历、影像报告、出院小结&#xff0c;哪个不是一堆非结构化的自然语言&#xff0c;而要把药物、诊断、症状、手术这些关键信息从里面捞出来&#xff0c;绕不开的就是序列标注。这几年我陆续试过…

作者头像 李华
网站建设 2026/10/21 3:36:57

ASP学校网站整站源码解析:从IIS配置到Access数据库实战

简介&#xff1a;这是一套面向中学、技校及职业院校的整站ASP网站源码&#xff0c;适合学校信息化建设人员、计算机专业教师以及学习ASP动态建站的学生使用&#xff0c;可快速搭建校园门户、教务信息发布或班级展示类站点。压缩包共167个文件&#xff0c;约1.04MB&#xff0c;其…

作者头像 李华