标题里的“打劫”是我开的玩笑,本质上就是用 Python 爬虫把得物 H5 页面上公开展示的热度数据抓下来,供自己分析使用。这个话题最近问的人不少,想做竞品观察、商品热度对比、选品调研的技术同学,几乎都会卡在同一个地方:直接用 requests 去请求页面背后的接口,返回的结果不是缺字段,就是带着一串动态签名参数,完全没法直接复用。后来我改用 Playwright 模拟真机的方式,让浏览器像一部真实的手机客户端一样去打开 H5 页面,页面自己完成渲染,数据自然出现在 DOM 和接口响应里,我们再按需采集,反而省掉了逆向加密参数的整个环节。这篇文章会完整复盘这套思路和可落地的代码,适合正在用 Python、想搞定 H5 数据采集方案的读者参考。
1. 项目目标拆解:得物 H5 热度数据到底指什么
1.1 热度数据显示在哪里
很多朋友一听“热度数据”,第一反应是去翻某个现成的数据报表,其实得物这类平台的“热度”是散落在页面各个角落的。商品卡片上通常会有想要人数、已购人数,详情页里会有热度值、收藏数、评论数,搜索结果里也经常出现“多少人正在浏览”之类的动态指标。标题里说的“热度数据”,按行业习惯就理解为这些公开展示、用来衡量商品受关注程度的数字。
以商品详情页为例,常见的热度指标大致有几类:
- 热度值:平台自己加权算出的一个指数,类似抖音的热度指数,浏览、互动、转化都会影响它。
- 想要人数:多少用户点了“想要”,这是得物的一大特色指标,对选品判断很有参考价值。
- 已购人数和收藏数:反映真实成交和沉淀意愿。
这些数字有一个共同点,它们都在 H5 页面上直接渲染出来,不需要登录就能看到一部分。只要能稳定打开页面,就能稳定取数。这也是我选 H5 而不是 App 的根本原因。
1.2 为什么 H5 比 App 和 PC 端更适合切入
先说说 App 这条路。得物 App 的接口能力很强,但想拿到它的数据,常规思路是抓包、逆向签名、处理证书绑定,再考虑设备指纹和风控。这套链路对普通爬虫爱好者来说学习成本很高,而且 App 一旦加固升级,之前写的逆向代码可能一夜之间全部作废。PC 网页呢,虽然也是 Web,但 PC 端的 JS 加密和后端风控同样不轻,有些接口连完整参数都难凑齐。
H5 在这两者之间取了一个非常好的折中:它是移动端网页,基于常规 Web 技术构建,Playwright 这类自动化工具能直接驱动;同时它面向手机浏览器,页面交互设计上更轻量,数据渲染路径更清晰。只要把浏览器伪装成一部真实的手机,让页面自己把数据跑出来,我们就能绕开“手动构造签名参数”这个最大的坑。这也是为什么真正做采集的人,遇到这种目标第一反应都是:先打开页面,看接口和渲染方式,再决定用什么工具。
1.3 先给这次项目画一条技术边界
坦白说,“打劫”只是标题的玩笑,实际做的事情就是访问公开页面、解析公开数据,跟你在浏览器里手动看数据没有本质区别,只是用程序代替了手。但这不代表可以无限度地操作。我建议所有看到这篇文章的人,先把边界定好:只采集无需登录即可访问的公开展示数据;不绕过登录态、不利用未授权接口、不抓取非公开的会员专属内容;控制在合理频率,不给对方服务器造成压力;采集结果只用于个人学习和技术研究。
这个边界不是客套话,而是关系到你的 IP、账号和长期稳定性。得物对自动化访问是有感知能力的,真正能长期跑的采集任务,靠的不是对抗,而是克制。
2. 反爬难点与方案选型:直接 requests 为何不行,Playwright 凭什么行
2.1 先看前端反爬:签名参数、Cookie 与行为检测
直接用 requests 请求得物 H5 的接口,大概率会收到一串校验失败或者空数据,原因在于前端页面请求接口时,并不是裸奔的。页面里的 JS 会在每次请求前动态计算签名参数,再把签名拼进 URL 或请求头里。后端收到请求后会校验签名是否合法、是否过期、是否和当前会话绑定。requests 拿不到这个签名,自然就被挡在门外。
还有一层是 Cookie 与指纹绑定。H5 页面首次加载时,服务端可能下发一个带有访问特征的 Cookie,后面每次请求都会校验这个 Cookie 和请求环境是否匹配。你用 requests 去访问,缺少浏览器执行环境,Cookie 链很容易断。更麻烦的是行为检测,高频请求、固定间隔、无操作轨迹,都会让风控系统把你的请求标记为机器流量。等到被标记之后,即使伪造了签名也会触发验证码。
所以问题很清楚:requests 面对的是“请求+加密+行为”三座大山,而 Playwright 的出发点完全不同。
2.2 选型依据:Playwright 相对 Selenium 和 Requests 的优势
如果你以前写过爬虫,大概率用过 Selenium。Selenium 能做浏览器自动化,但有几个痛点:驱动版本和浏览器版本经常不匹配,换个环境就要折腾半天;对移动端设备的模拟支持很弱,想模拟 iPhone 要手动拼 UA、改窗口大小,还要处理触摸事件;页面元素的自动等待逻辑也比较粗糙,容易因为时序问题抓不到数据。
Playwright 是微软维护的自动化测试框架,底层支持 Chromium、Firefox、WebKit,API 设计比 Selenium 现代很多。它在爬虫场景最大的优势有两块:一是内置大量真实设备的参数描述符,比如 iPhone 13、Pixel 5,一行代码就能切到对应的 UA、viewport、触摸能力;二是原生支持监听网络响应、拦截请求、注入脚本,这让“页面自动算好数据,我们再从网络层取结果”变成了一件很自然的事。
从工程角度对比,Playwright 还有两个很实际的好处:安装简单,一个 pip 命令加一个浏览器下载命令就够;异步和同步 API 都有,写脚本调试都方便。对个人项目来说,它的学习曲线比 Selenium 更平滑,维护成本也更低。
2.3 Playwright“模拟真机”具体模拟哪些维度
很多人以为模拟真机就是改一下 User-Agent,这个理解太浅了。服务端判断你是不是真机,看的是一个组合特征,至少要覆盖四个维度:
- 设备指纹层:User-Agent、设备型号、屏幕分辨率、像素比、系统版本。Playwright 的设备预设里都帮你配好了,但要注意部分站点会读取
navigator.platform、navigator.hardwareConcurrency等字段,必要时还需要用注入脚本修正。 - 交互能力层:移动端站点会判断
navigator.maxTouchPoints、ontouchstart是否存在、window.innerWidth的值。Playwright 里对应的是is_mobile=True和has_touch=True,这两个参数直接影响页面是否渲染成移动端形态。 - 运行环境层:浏览器自动化工具会在
navigator.webdriver上留下痕迹,常规页面不读它,但风控脚本会读。这个字段要用初始化脚本清掉,否则你在真机上打开页面和在自动化工具里打开页面,看到的东西可能完全不一样。 - 行为时间层:真机用户打开页面会有一个自然的过程,滚动、停顿、点击都有随机性。程序里如果每次都秒开秒关、固定间隔,即使指纹伪装得再好,也会被行为模型标记。
这四个维度配合起来,才算真正意义上的“模拟真机”。
2.4 稳定性预期:能解决什么,不能解决什么
把话说在前面,Playwright 模拟真机不是万能钥匙。它能帮你省掉签名逆向的复杂工作,把“手工构造请求”变成“页面自己请求,我就等着拿结果”;也能在小规模采集场景下保持不错的成功率,一天跑几百条数据完全没问题。
但它不能解决所有问题。如果你打算一台机器开几十个并发去怼同一个商品页,那不管你用什么工具,最终都会触发验证码和 IP 限制。模拟真机解决的是“请求合法性问题”,不解决“频率合规问题”。另外,如果目标数据需要登录才能看到,或者属于平台明确不允许外部采集的权益内容,那 Playwright 也不应该被用来绕过这个限制。理解这一点,后面的实操才有意义。
3. 从零跑通:Playwright 模拟真机抓 H5 的完整代码
3.1 环境准备与依赖安装
实操部分从环境开始。先把 playwright 装进 Python 环境,建议用虚拟环境隔离,避免和系统 Python 包冲突。
pip install playwright装完只是装了 Python 库,还得下载浏览器内核。Python 环境下对应的是playwright install命令。
playwright install chromium这条命令会下载 Playwright 定制的 Chromium 内核。下载失败很常见,尤其是服务器网络不稳定的时候,我在第 4 章会专门讲处理办法,这里先把入口跑通。
安装完成后,可以用极简代码验证环境:
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() page.goto("https://www.baidu.com") print(page.title()) browser.close()能打出标题,说明环境和下载的浏览器内核都正常,可以进入下一步。
3.2 用设备预设构造一个“手机浏览器”上下文
Playwright 的devices对象里预设了市面上常见手机的参数,我一般直接用 iPhone 13 的预设,然后微调时区和语言。
from playwright.sync_api import sync_playwright import random import time with sync_playwright() as p: browser = p.chromium.launch( headless=False, args=["--disable-blink-features=AutomationControlled"] ) context = browser.new_context( **p.devices["iPhone 13"], locale="zh-CN", timezone_id="Asia/Shanghai", color_scheme="light", ) page = context.new_page() page.goto("https://目标H5商品页地址", wait_until="networkidle")这段代码最有价值的是**p.devices["iPhone 13"]这行。它把 iPhone 13 的 User-Agent、屏幕尺寸、像素比、触摸点数量、设备型号等参数一次性注入到浏览器上下文里。后面再设置locale="zh-CN"和timezone_id="Asia/Shanghai",是为了让页面前的 JS 读取语言环境和时区时,看到的就是一部国行手机。
需要注意networkidle这个等待条件在网络不稳定的页面会卡很久,实际项目里我更常用domcontentloaded加手动等待某个关键节点出现,后面会讲。
3.3 隐藏自动化痕迹的三板斧
代码写到这里,页面能打开,但还不够安全。很多站点会在加载完成后执行一段脚本,读取navigator.webdriver的值。如果返回 true,直接判定为机器人,后续数据就不给了。这是第一个要处理的地方。
处理方法是利用 Playwright 的add_init_script,在页面任何脚本执行之前,把关键字段改掉:
context.add_init_script(""" Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh'] }); Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] }); """)这段脚本做的事情很简单:让navigator.webdriver变成 undefined,把语言列表改成中文,给navigator.plugins塞几个假插件。这些细节单独看都不起眼,但组合起来,页面里的环境检测脚本就很难通过几个特征判断你是自动化工具了。
第二板斧是启动参数。args=["--disable-blink-features=AutomationControlled"]这个参数会禁掉 Chromium 里和自动化控制相关的功能标记,让浏览器内核对外表现得更接近普通 Chrome。
第三板斧是尽量用有头模式。headless=False会弹出真实浏览器窗口,视觉上更接近真机,同时很多站点对 headless 内核的识别参数更敏感。如果你的服务器没有显示器,可以配合虚拟显示工具运行,但不建议一开始就追求全无头化,先在有头模式下把流程跑通再说。
3.4 抓数据的两条技术路线:监听接口与解析 DOM
到达目标页面后,取数有两种思路,我建议把两种都掌握,因为不同数据源的稳定性不一样。
第一种是监听网络响应。页面加载时会自动请求后端接口,这些接口返回的数据往往比 DOM 里展示的更原始、更结构化。用page.on("response")把接口响应截住,再用response.json()解析:
def handle_response(response): url = response.url if "/api/" in url and "item" in url: try: data = response.json() print(url) print(data) except Exception: pass page.on("response", handle_response)这里要提醒一下,接口 URL 里的关键字得先手动在浏览器开发者工具里看一遍,找到真正返回热度数据的那个请求,再把特征字符串写进过滤条件。实际项目里,同一个页面可能有十几个接口,全打印出来太吵,只过滤你需要的。
第二种是直接解析 DOM。有些热度数据不会走独立的接口,只以文本形式渲染在页面上,这时候就得用选择器取值:
page.wait_for_selector(".product-heat") heat_text = page.locator(".product-heat").inner_text() want_text = page.locator(".product-want-count").inner_text()思路很简单,但具体 class 名要以你实际调试的页面为准。我的经验是:先用page.locator定位一个包含目标数字的区块,打印它的inner_text,看真实结构,再写正式的选择器。不要凭直觉猜 class 名,否则八成会踩空。
如果页面数据是在滚动后才加载的,取数前先让页面做一个自然的滚动:
page.evaluate("() => window.scrollTo(0, document.body.scrollHeight * 0.3)") time.sleep(random.uniform(1, 2)) page.evaluate("() => window.scrollTo(0, document.body.scrollHeight * 0.8)")这个动作模拟的是真实用户翻看页面的过程,既能让懒加载的数据出来,也能让行为模型更认可。
3.5 数据清洗与存储:写一个带时间戳的采集器
拿到原始数字后,第一件事不是存,而是清洗。页面上的“1.2万”“热度8.6万”这些文本,得先转换成统一格式。我一般用正则处理:
import re def clean_num(text: str) -> int: text = text.strip().replace(",", "") if "万" in text: return int(float(text.replace("万", "")) * 10000) return int(float(text))然后写一个简单的 SQLite 存储,把每次采集的数据加上时间戳存下来。别小看时间戳,热度数据是动态变化的,有了时间维度,你才能做趋势分析。
import sqlite3 conn = sqlite3.connect("dewu_heat.db") conn.execute(""" create table if not exists heat_data ( id integer primary key autoincrement, product_id text, heat_value integer, want_count integer, create_time datetime default (datetime('now', 'localtime')) ) """) conn.execute( "insert into heat_data(product_id, heat_value, want_count) values (?, ?, ?)", (product_id, heat_value, want_count) ) conn.commit() conn.close()这一步的扩展性很好。今天存的是热度值和想要人数,明天想加收藏数、评论数,只要加字段就行。数据量大了以后,SQLite 也够个人项目用了,不需要一上来就上 MySQL 或者 MongoDB。
3.6 控制采集频率与随机行为
最后是让整个脚本更贴近真人的关键一步:随机化。很多爬虫脚本被限制,不是因为技术不够,而是行为太规律。每 5 秒一次、每次页面停留 2 秒、永远按顺序访问商品,这些特征在风控系统眼里比任何指纹都明显。
我常用的随机策略有三条:
- 请求间隔随机化:
time.sleep(random.uniform(3, 8)),不要固定在一个值。 - 页面操作随机化:滚动幅度、滚动次数、鼠标在页面上的停留时间都随机。
- 批次顺序随机化:采集商品列表时,先
random.shuffle打乱顺序,再逐个访问。
product_urls = [...] # 要采集的商品页 URL 列表 random.shuffle(product_urls) for url in product_urls: page.goto(url, wait_until="domcontentloaded") time.sleep(random.uniform(2, 4)) page.mouse.wheel(0, 800) time.sleep(random.uniform(1, 2)) # 取数逻辑... time.sleep(random.uniform(8, 15))这套节奏跑起来,一天采几百条数据,基本不会触发验证码。记住一个原则:慢就是快。
4. 常见问题与排查技巧实录
4.1 playwright 安装或浏览器下载失败怎么处理
这是新手最容易卡住的地方。pip 安装失败,多半是网络源问题,换国内 PyPI 镜像就行:
pip install playwright -i https://pypi.tuna.tsinghua.edu.cn/simpleplaywright install chromium下载失败,本质是浏览器二进制文件下载超时。可以设置下载源环境变量再执行:
export PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright/ playwright install chromium如果你的机器上已经装了 Chrome 或者 Edge,也可以不下载 Playwright 自带的浏览器内核,直接指定系统浏览器路径启动:
browser = p.chromium.launch( executable_path="/usr/bin/google-chrome", headless=False )注意executable_path要填实际存在的浏览器路径,Windows 上一般是C:\Program Files\Google\Chrome\Application\chrome.exe。
4.2 打开页面就出现验证码或滑块怎么办
首先要明确一点,任何自动化工具都无法保证 100% 不触发验证码,我们能做的是把触发概率降到最低。如果你一打开页面就遇到滑块,优先检查三件事:是不是 headless 模式、有没有设置 device 预设、访问频率是不是太高。
headless 模式对部分站点的环境检测更敏感,建议先改用有头模式调试。device 预设没设置的话,页面可能把你当成 PC 浏览器在访问移动页面,交互检测会更严格。访问频率高的话,哪怕第一次能打开,第二次第三次也会触发验证码。
还有一个很实用的技巧:用真实浏览器手动打开一次目标页面,完成正常的滑动验证或人机校验,然后让 Playwright 接管这个带登录态和 Cookie 的浏览器上下文。这样新环境会继承你手动操作产生的信任数据,后续请求的成功率会高很多。
4.3 页面正常渲染但取不到目标数字
页面能打开、内容也看到了,但locator取不到值,这个问题多半出在三个方面。第一是数据在 iframe 或者 Shadow DOM 里,普通选择器钻不进去。Playwright 对此有专门接口,iframe 用page.frame_locator,Shadow DOM 用穿透选择器。
第二是数据加载有延迟。你在脚本里立刻取值,可能异步请求还没回来。解决思路是不要用固定sleep,而是用page.wait_for_selector等待目标节点出现,设置一个合理的超时时间。
第三是页面文本被特殊处理。有些站点的数字不是纯文本,而是用字体文件映射的乱码,DOM 里看到的是奇怪的字符。遇到这种情况,直接解析 DOM 文本就不靠谱了,你得回到监听接口那条路线,从 JSON 响应里取原始字段。这也是我为什么在第 3 章强调两条路线要同时掌握。
4.4 运行一段时间后接口返回空数据或被临时限制
这种情况通常在采集脚本跑了十几分钟或者几百条数据之后出现。现象是页面还能打开,但接口开始返回空数据,或者偶尔弹一次验证码。核心原因就是频率触发了风控阈值。
我的处理步骤通常是:立即停掉任务,等待半小时以上,让风控标记冷下来;然后降低单次运行的数据量,比如每次只跑 50 条;把请求间隔从 5 秒拉长到 20 秒以上。如果项目对实时性要求不高,就把任务拆到凌晨执行,那时候平台的访问压力小,风控阈值也相对宽松。
这里不推荐一遇到限制就上高匿代理池,个人项目完全没必要,而且代理质量参差不齐,反而更容易触发风控。先把频率降下来,比什么工具都管用。
4.5 浏览器进程残留与内存持续上涨
长任务跑久了,你可能会发现系统里堆了一堆没有退出的浏览器进程,内存越来越满。这通常是因为代码在异常退出时没有关闭浏览器。Playwright 的同步 API 里,只要脚本进程结束,浏览器一般会自动关闭,但如果你用了多路复用、持久化上下文或者异常捕获不完整,就容易残留。
最稳的做法是显式管理生命周期:
from playwright.sync_api import sync_playwright browser = None try: with sync_playwright() as p: browser = p.chromium.launch() # ... 业务逻辑 ... finally: if browser: browser.close()长任务建议加一个定期重启机制:每采集 200 条数据,主动关闭浏览器,重新启动一个新实例。这样即使 Chromium 内部有内存泄漏,也不会无限积累。
5. 提升采集稳定性的工程化建议
5.1 用持久化上下文记录登录态,减少验证
很多人每次跑脚本都是全新的浏览器上下文,相当于一个新用户第一次访问设备,风控系统对这种情况天然不信任。一个更好的办法是用持久化上下文,把第一次手动访问产生的 Cookie、LocalStorage 保存下来,后续任务直接复用。
第一次运行时,打开浏览器手动完成一次正常浏览,然后执行:
context.storage_state(path="state.json")后续脚本启动时带上这份状态:
context = browser.new_context( storage_state="state.json", **p.devices["iPhone 13"], locale="zh-CN", timezone_id="Asia/Shanghai", )这样服务端看到的是一个有浏览历史、有稳定状态的用户,而不是一个每次见面都是白纸的新访客。验证码的出现频率会明显下降。
5.2 小规模试跑加断点续采,比一次写完更稳
我见过很多新手写完一个完整采集脚本,恨不得一口气跑完全部数据,结果跑到一半被验证码打断,前面的进度全丢。工程化的做法是把采集任务拆成可以续跑的小批次。
最简单的方案是维护一张任务表,每条商品一个状态:待采集、采集中、已完成、失败。每次启动脚本只取待采集的数据,处理成功后就标记完成。运行中途停了也没关系,下次启动从待采集的地方继续。
pending_items = get_pending_items(limit=50) for item in pending_items: try: collect_item(item) mark_done(item) except Exception as e: mark_failed(item, str(e)) time.sleep(60)这个模式不复杂,但能救你无数次。还有一点值得养成习惯:每次跑任务前,先小批量跑 20 条,确认解析、存储、频率都正常,再放长任务跑。直接拉满跑全量,一旦代码逻辑有疏漏,浪费的不只是时间,还有账号的信任度。
5.3 热度数据的趋势价值远大于单次快照
写到最后,我想聊一个容易被忽略的点。很多人抓热度数据,抓完就存下来,过几天就丢了,其实热度数据的核心价值在趋势。单次快照只能告诉你此刻哪个商品热,而按时间维度持续采集,你能看到商品的上升期、爆发期和衰退期,甚至能发现异常数据。
我的做法是每天固定两个时段各采一次,比如早上 8 点和晚上 20 点,数据入库后按商品维度做环比分析。某件商品热度值一夜之间翻了十倍,背后大概率有市场事件在推动;连续一周热度下跌,那可能就是真实的热度退潮。这些分析不需要复杂的建模工具,Excel 透视表就能完成,但前提是你手里有连续的、带时间戳的数据。
另外,这套 Playwright 模拟真机的代码完全不是只能用在得物一个网站上。我后来把 device 预设、初始化脚本和监听响应这三个核心部分抽出来,改改参数就复用到其他 H5 页面上,都跑得挺顺。核心思路始终是那句话:别和平台拼逆向算法,让页面自己把数据跑出来,你再从浏览器里拿结果。
我个人最后的体会是,这类采集任务最重要的是分寸感。技术手段再强,也不如控制频率、尊重规则来得持久。热度数据不是非抓不可,但如果你想练练 Playwright 和 H5 采集,这是一个非常合适的练手项目。把每一步跑稳,你收获的不只是数据,还有一套可以复用的自动化采集思路。