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

发布时间:2026/10/21 3:42:06
动态渲染页面爬虫实战:商品名称、价格与评论数采集方案
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 参数规律与分页处理分页参数是最常见的。有的用page1pageSize20有的用offset0limit20还有的用start0count20。搞清楚每页多少条、页码从几开始就能写循环批量拉取。我一般会先手动改参数请求两三次确认分页逻辑再写进代码。如果参数里有看不懂的签名字段比如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, headersheaders) for page in range(1, 6): params {page: page, pageSize: 20, keyword: 耳机} resp session.get(https://example-mall.com/api/search, headersheaders, paramsparams) 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(headlessTrue) page browser.new_page() page.goto(https://example-mall.com/list?keyword耳机) page.wait_for_selector(.product-item, timeout15000) 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. 我在实际项目中的几点体会做这类动态采集技术本身不是最难的难的是持续稳定。平台会改版、接口会调整、风控会升级今天能跑的脚本下个月可能就失效了。所以我在设计时会把“易维护”放在很重要的位置选择器写宽松、配置抽出来、日志打详细、异常有重试。这样出问题时改一处配置就能恢复而不是从头重写。另一个体会是不要追求一次抓全。先抓核心字段跑通流程再逐步加字段、加页数、加频率。贪多求快往往导致问题堆在一起排查起来痛苦。我见过不少人一上来就想把整个平台的商品全抓下来结果卡在反爬上连第一页数据都没拿稳。最后说个具体的评论数这个字段很多平台在列表页显示的是总数但点进详情页才加载具体评论内容。如果只需要评论数列表页就够了如果要评论内容那是另一个量级的工程涉及详情页采集和翻页复杂度高不少。搞清楚需求边界能省很多无用功。这个方向后续还能扩展的地方不少比如把采集到的价格数据做成趋势图或者结合评论数做热度排序。工具是死的思路是活的把动态采集这套流程跑通之后换个目标平台无非是重新定位字段、重新分析接口的事。