Python动态页面抓取实战:商品名称、价格与评论数提取

发布时间:2026/10/21 18:44:59
Python动态页面抓取实战:商品名称、价格与评论数提取
1. 项目缘起与整体思路拆解电商平台的价格和销量数据对于做市场分析、竞品调研或者选品决策的人来说价值不用我多讲。但真正动手去抓的时候你会发现一个很现实的问题页面打开明明能看到商品名称、价格、评论数可你右键查看网页源代码这些东西全都不在HTML里。这就是典型的动态渲染页面数据是浏览器执行JavaScript之后才塞进DOM的。我这次要聊的就是怎么把这类动态页面上的商品名称、评论数、价格这三类核心字段稳定地拿下来。适合谁看有一定Python基础、了解HTTP请求基本概念、但一碰到动态加载就卡壳的朋友。如果你之前只用过requests加BeautifulSoup抓静态页面抓下来的结果永远是空列表或者一堆占位符那这篇内容基本就是为你写的。先说清楚整体思路。动态页面的数据来源无非两条路第一条是找到页面背后真正返回数据的接口直接请求那个接口拿JSON第二条是用浏览器自动化工具让一个真实的浏览器把页面渲染完再从渲染后的DOM里提取数据。这两条路各有各的适用场景不是谁一定比谁好。我个人的选择逻辑是这样的如果接口结构清晰、参数不复杂、没有太强的加密签名优先走接口路线因为快、轻、稳定一个请求就能拿到几十条数据。如果接口被各种token、签名、时间戳保护得很死或者数据是分块异步拼出来的那就退回到浏览器自动化用Selenium或者Playwright这类工具牺牲速度换稳定性。这个项目里商品列表页的价格和评论数通常在接口里能拿到但商品名称有时候是拼接的评论数有时候是单独一个接口。所以实际做的时候往往是两条路混着用列表页走接口批量拿详情页或者需要精确评论数的时候走浏览器渲染。下面我会把两条路都拆开讲透你根据自己的目标站点情况选。还有一个前置判断很重要先别急着写代码手动打开浏览器的开发者工具切到Network面板筛选XHR或者Fetch请求然后刷新页面看看有没有返回JSON的请求。如果有点进去看Response里面是不是有你想要的字段。这一步花五分钟能帮你省掉后面两小时的瞎折腾。我见过太多人上来就装Selenium结果发现人家接口干干净净纯属杀鸡用牛刀。2. 动态页面数据加载的核心原理2.1 为什么源代码里看不到数据要理解动态抓取得先理解浏览器是怎么把页面呈现出来的。你请求一个URL服务器返回的是一份HTML文档这份文档叫初始HTML。对于传统静态页面商品信息就写在这份HTML里requests拿到之后直接解析就行。但对于现在的电商页面初始HTML里往往只有一个空壳容器比如一个div里面什么都没有真正的数据靠页面加载完成后执行的JavaScript去异步请求拿到JSON之后再动态插入到这个div里。所以你用requests拿到的是JavaScript还没执行的那份HTML自然看不到商品名称和价格。这不是你代码写错了是页面机制决定的。理解这一点后面所有的方案选择就顺理成章了要么你绕过浏览器直接找那个异步请求要么你模拟一个浏览器把JavaScript跑完。2.2 接口路线与渲染路线的取舍接口路线的本质是跳过浏览器这个中间商。浏览器能拿到数据是因为它请求了某个API那我也直接请求那个API不就行了。优点是请求量小、速度快、解析简单返回的就是结构化JSON。缺点也明显很多平台的接口带签名参数比如一个sign字段它是用时间戳、设备信息、密钥等一堆东西算出来的你复现这个签名算法成本很高而且平台一改算法你就得跟着改。渲染路线的本质是雇一个听话的浏览器替你干活。Selenium驱动一个真实的Chrome让它加载页面、执行JS、等数据出来然后我从渲染后的DOM里用XPath或者CSS选择器提取。优点是所见即所得页面上能看到什么我就能抓什么不用管接口怎么加密。缺点是慢一个页面加载加渲染好几秒量大之后效率是硬伤而且资源占用高。我的经验是小批量、字段复杂、接口难搞的用渲染路线大批量、字段规整、接口能拿到的用接口路线。这个项目里商品名称、评论数、价格三个字段如果接口能一次性返回那接口路线是首选。下面两章我分别把两条路线的实操讲清楚。3. 接口路线定位真实数据请求并提取字段3.1 用开发者工具锁定目标接口打开目标商品列表页按F12打开开发者工具切到Network面板勾选Fetch/XHR过滤器然后刷新页面。这时候你会看到一串请求逐个点开看Response。找什么找返回内容里包含商品名称或者价格的请求。通常列表页的数据接口名字里会带list、search、product、item这类词。找到之后重点看三样东西Request URL、Request Method、以及Payload或者Query String Parameters。URL告诉你请求地址Method告诉你GET还是POST参数告诉你需要传什么。把这些记下来这就是你后面用代码复现的依据。这里有个细节有些接口的返回是分页的参数里有pageNum或者offset、pageSize。你要抓多页数据就得把这些参数做成可变的。还有的接口返回里有个total字段告诉你总数你可以据此算出总页数避免盲目翻页。3.2 用requests复现请求并解析JSON定位到接口之后用requests复现它。关键是把请求头带全尤其是User-Agent、Referer、有时候还有Cookie。很多接口不带Referer会直接返回403。下面是一段可直接参考的代码骨架import requests import json import time headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://example.com/list, Accept: application/json, text/plain, */*, } def fetch_page(page_num): params { pageNum: page_num, pageSize: 30, categoryId: 12345, } resp requests.get( https://example.com/api/product/list, headersheaders, paramsparams, timeout10, ) resp.raise_for_status() return resp.json() def parse_items(data): results [] for item in data.get(data, {}).get(list, []): results.append({ name: item.get(productName, ).strip(), price: item.get(price), comment_count: item.get(commentCount), }) return results all_items [] for page in range(1, 6): payload fetch_page(page) all_items.extend(parse_items(payload)) time.sleep(1.5) print(f共抓取 {len(all_items)} 条) for row in all_items[:5]: print(row)这段代码里有几个点值得展开。time.sleep(1.5)不是可有可无的它是给请求之间留间隔避免短时间内高频请求触发风控。raise_for_status()帮你快速发现4xx、5xx错误不然你拿到一个错误页面还在那解析半天。字段提取用.get()而不是直接索引是因为接口字段偶尔会缺失直接索引会抛KeyError把整个流程打断。3.3 字段清洗与价格评论数的处理接口返回的价格字段经常不是干净的浮点数。有的返回分单位比如1990代表19.90元你得除以100。有的返回带货币符号的字符串比如19.90得先去掉符号再转float。评论数有时候返回1.2万这种中文单位得写个转换函数。def parse_price(raw): if raw is None: return None if isinstance(raw, (int, float)): return round(raw / 100, 2) if raw 1000 else float(raw) text str(raw).replace(, ).replace(¥, ).replace(,, ).strip() try: return float(text) except ValueError: return None def parse_comment_count(raw): if raw is None: return 0 text str(raw).strip() if 万 in text: return int(float(text.replace(万, )) * 10000) try: return int(text) except ValueError: return 0这两个函数看着简单但实际项目里能帮你省掉大量脏数据带来的麻烦。我踩过的坑是某平台价格字段有时候返回整数有时候返回字符串如果不做类型判断后面做价格排序的时候直接报错。提示接口返回的字段名可能和页面上显示的不完全对应比如页面上叫评价数接口里可能叫commentCount或者evaluateNum。以接口实际返回为准别想当然。4. 渲染路线用浏览器自动化抓取动态内容4.1 Selenium环境搭建与驱动配置当接口走不通的时候Selenium就是你的备选方案。安装很简单pip install selenium但真正容易出问题的是浏览器驱动。现在的Selenium 4.x版本内置了驱动管理大多数情况下你不需要手动下载chromedriver它会自动匹配。但如果你的Chrome版本比较特殊或者在公司网络环境下自动下载失败那就得手动指定驱动路径。from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC options Options() options.add_argument(--headlessnew) options.add_argument(--disable-gpu) options.add_argument(--no-sandbox) options.add_argument(--window-size1920,1080) options.add_argument( user-agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ) driver webdriver.Chrome(optionsoptions)--headlessnew是无头模式不弹出浏览器窗口适合服务器上跑。但要注意有些站点会检测无头浏览器如果发现被识别可以试着去掉headless或者加上一些反检测的参数。--window-size设大一点有好处因为有些页面会根据窗口宽度决定加载多少内容窗口太小可能只渲染部分商品。4.2 等待策略显式等待比sleep靠谱新手最容易犯的错就是time.sleep(5)然后去抓元素结果有时候5秒不够有时候5秒浪费。正确的做法是用显式等待让Selenium等到某个条件满足再继续。driver.get(https://example.com/list) wait WebDriverWait(driver, 15) wait.until( EC.presence_of_all_elements_located( (By.CSS_SELECTOR, .product-item) ) ) items driver.find_elements(By.CSS_SELECTOR, .product-item) for item in items: name item.find_element(By.CSS_SELECTOR, .product-name).text price item.find_element(By.CSS_SELECTOR, .product-price).text comment item.find_element(By.CSS_SELECTOR, .comment-count).text print(name, price, comment)presence_of_all_elements_located的意思是等到至少有一个匹配元素出现。比它更严格的是visibility_of_all_elements_located要求元素可见。用哪个取决于你的场景如果元素存在但被隐藏用前者如果必须可见才能提取文本用后者。4.3 滚动加载与翻页处理很多商品列表是滚动加载的你不动鼠标它就只加载第一屏。这时候需要模拟滚动到底部触发下一页数据加载。import time last_height driver.execute_script(return document.body.scrollHeight) while True: driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) time.sleep(2) new_height driver.execute_script(return document.body.scrollHeight) if new_height last_height: break last_height new_height这段逻辑是滚到底等两秒让新内容加载再量一次页面高度。如果高度没变说明到底了退出循环。如果变了继续滚。这里的两秒是个经验值网络慢的时候可以调到三秒。翻页的话找到下一页按钮点击然后重新等待元素出现循环往复。注意滚动加载的页面元素是不断追加的你之前抓过的元素引用可能失效。稳妥的做法是每滚一次就重新find_elements一次或者一次性抓完再统一解析。5. 反爬应对与请求稳定性优化5.1 请求头与Cookie的合理构造不管是接口路线还是渲染路线请求头都是第一道门槛。最基本的User-Agent必须带而且最好用真实浏览器的。Referer有时候也必要它告诉服务器你是从哪个页面跳过来的。Cookie更关键有些接口需要登录态才能返回完整数据。我的做法是先用浏览器正常访问一次从开发者工具里把完整的请求头复制出来然后在代码里逐个还原。不要偷懒只写User-Agent很多平台会检查Accept、Accept-Language、甚至Sec-Fetch-*系列头。缺了不一定马上被封但风控系统会给你打低分积累到一定程度就给你返回假数据或者直接拒绝。5.2 请求频率控制与重试机制频率控制是保命的关键。我一般会遵循两个原则单次请求间隔不低于1秒批量任务每抓50到100条休息几秒。这不是矫情是给服务器减压也是给自己留后路。你请求太猛轻则IP被限速重则整个网段被封。重试机制也要有。网络抖动、偶发的超时、临时的5xx错误这些都不应该让整个任务崩掉。用一个简单的重试装饰器就能解决import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session requests.Session() retry Retry( total3, backoff_factor1, status_forcelist[429, 500, 502, 503, 504], ) adapter HTTPAdapter(max_retriesretry) session.mount(https://, adapter) session.mount(http://, adapter)backoff_factor1意味着第一次重试等1秒第二次等2秒第三次等4秒指数退避。status_forcelist里列的状态码才会触发重试429是请求过多5xx是服务端错误这些重试有意义。404这种就别重试了重试多少次都是404。5.3 数据去重与增量抓取思路抓多了之后你会发现重复数据很烦。同一个商品可能在多个页面出现或者你多次运行脚本抓了同一批。去重最简单的办法是用商品ID做唯一键存到一个set里抓之前先判断。增量抓取是另一个实用技巧。如果你每天都跑一次没必要每次全量抓。记录上次抓到的最大ID或者时间戳下次只抓比它新的。这样既省时间又降低被封风险。实现上可以在本地存一个json文件记录进度或者用sqlite存已抓取的ID。6. 常见问题与排查技巧实录6.1 抓到的数据是空的或者不对这是最高频的问题。排查顺序我一般是这样的先确认页面是不是真的动态渲染手动在浏览器里禁用JavaScript看看数据还在不在。如果禁用后数据消失那就是动态页面你requests抓不到很正常。然后确认接口是否找对了在Network面板里看Response是不是包含你要的字段。最后检查请求头是否完整尤其是Cookie和Referer。还有一个隐蔽的情况接口返回了数据但字段名和你以为的不一样。比如你以为叫price实际叫salePrice。这种就老老实实把返回的JSON打印出来肉眼看一遍字段名。6.2 元素定位失败或者超时Selenium报NoSuchElementException或者TimeoutException原因通常有三个选择器写错了、元素在iframe里、元素还没加载出来。选择器写错最常见建议在浏览器控制台里用document.querySelector先验证一下选择器能不能选中。iframe的问题需要先driver.switch_to.frame()切进去。加载问题就用显式等待解决。另外有些站点的class名是动态生成的每次刷新都不一样比如classitem_1a2b3c。这种就不能用class定位得用相对稳定的属性比如data-id或者用XPath按结构定位。6.3 被封禁或者返回验证页面如果你发现请求返回的是验证码页面、或者一个提示访问过于频繁的页面说明已经被风控盯上了。这时候别硬刚先停下来。处理办法包括降低频率、更换请求头、清理Cookie重新来。如果用的是渲染路线可以试试加上--disable-blink-featuresAutomationControlled参数减少被识别为自动化的概率。下面这张表是我整理的高频问题速查问题现象可能原因排查方向返回空列表页面动态渲染检查Network找接口403 Forbidden请求头缺失补全Referer和Cookie429 Too Many Requests频率过高加间隔、加重试退避元素找不到选择器错误或iframe控制台验证选择器数据字段缺失接口字段名不符打印JSON核对字段价格是整数分为单位除以100转换6.4 实操避坑心得说几个文档里不会写但实际很要命的点。第一别在循环里反复创建driver一个任务用一个driver实例就够了反复创建销毁开销极大。第二抓到的数据及时落盘别全攒在内存里万一中途崩了全白干。第三时间戳和随机数在参数里很常见别写死用代码动态生成。第四测试的时候先用小批量跑通确认字段都对再放大规模不然抓了几万条发现字段错位哭都来不及。7. 数据存储与后续扩展方向抓下来的数据总得有个地方放。小批量用CSV就够了pandas一个to_csv搞定。中大批量建议上sqlite或者MySQL方便去重和查询。字段设计上商品ID、名称、价格、评论数、抓取时间这五个是基础建议再加一个来源页面的URL方便回溯。import csv with open(products.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter( f, fieldnames[name, price, comment_count] ) writer.writeheader() writer.writerows(all_items)utf-8-sig这个编码值得注意它会在文件开头加一个BOM这样用Excel打开CSV不会乱码。普通utf-8在Excel里打开中文经常是乱码这个小细节能省不少事。后续扩展的话价格可以做历史趋势追踪每天抓一次存下来时间长了就能看出价格波动规律。评论数可以做增速分析评论增长快的商品往往近期热度高。这些都是在基础抓取之上能做的增值分析也是这类项目真正的价值所在。我个人在实际操作中的体会是动态抓取这件事七分靠前期分析三分靠写代码。花时间把页面加载机制搞清楚、把接口摸透后面写代码就是水到渠成。反过来上来就闷头写遇到问题再回头查效率低还容易心态崩。另外就是一定要有耐心做小批量验证别贪快稳扎稳打比什么都强。