尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Playwright动态页面爬虫实战:从XHR拦截到预约趋势监控

发布时间:2026/9/19 0:13:07

资讯中心
01
ARTICLE

Playwright动态页面爬虫实战:从XHR拦截到预约趋势监控

Playwright动态页面爬虫实战:从XHR拦截到预约趋势监控
如果你关注手机圈应该记得每次新品发布前华为商城都会提前挂出预约页面右下角那个“XX万人已预约”的数字是外界判断热度最直观的指标。发布会还没开群里已经开始传截图了我盯了两天发现人肉截图是真不靠谱白天忙起来忘记刷新凌晨爬起来看到数字突然跳一大截又没记录想做一张完整趋势图根本无从下手。索性写了个 Python 爬虫用 Playwright 做浏览器自动化定时抓取预约数据存库、画趋势线把新品从预约开启到发布会前夕的完整热度变化记录下来。这篇文章就把这套监听方案完整拆开讲一遍。适合谁看知道 requests 但处理不了动态页面的爬虫初学者以及想用无头浏览器做数据采集但还没摸到门道的 Python 工程师。你会看到我为什么选择 Playwright 而不是 requests怎么在浏览器里直接拦截 XHR 接口拿到预约数又怎么把它变成一条持续更新的趋势曲线。1. 预约数字背后的诱惑为什么这个需求适合用爬虫解决1.1 人肉盯数据的痛点预约页面的数字不是静态展示它的变化曲线本身就有信息量。比如营销预热期会不会集中放量、发布会前一天会不会突然猛涨、不同时间段增长斜率如何这些都能反映用户真实关注度的变化。但靠人肉盯梢有三个硬伤第一是采样频率不稳定忙起来可能半小时不刷新漏掉关键跳变第二是数据不可结构化截图里的数字没法直接导入表格做分析第三是时间成本极高总不能为了一个数字定闹钟半夜爬起来打卡。我最初的诉求其实很简单让脚本替代我每隔固定时间刷新一次页面把预约数提取出来带着时间戳存下来最后能画出一张图。这个诉求落到技术层面就是一个典型的动态页面数据采集问题。难点不在存储和画图而在“怎么稳定地拿到那个数字”。1.2 requests 和 Scrapy 为什么搞不定如果你打开商城预约页面的源代码会发现页面主体是空的预约数字要么在某个 JavaScript 文件里被异步加载要么在页面初始化后通过接口动态渲染到 DOM 上。requests 拿到的只是服务器返回的 HTML 外壳里面根本没有那个数字。Scrapy 单独用也一样它本质上还是一个基于 requests 的抓取框架面对这种“浏览器渲染后才出现的数据”要么自己去逆向接口和加密参数要么就得引入渲染引擎。对比一下主流方案方案拿到数据的方式学习成本稳定性requests 手动分析接口分析浏览器发出的网络请求模拟 headers、cookies 和签名参数高遇到加密参数很头疼中改版就要重新逆向Scrapy Splash/Scarpyrt通过渲染服务执行 JS 后再抓取中高需要多维护一个渲染服务中环境配置复杂Playwright 监听网络请求浏览器真实发起请求直接拦截响应体低浏览器自己处理了 JS 和加密高只要页面能打开就能拿到数据我选择 Playwright 的核心原因是它没有试图“绕过”什么而是让浏览器自己去做它该做的事。页面需要执行 JS那就执行接口需要带签名浏览器会自动带甚至如果页面里面有动态 iframePlaywright 也能处理因为监听 response 事件是挂在浏览器层面的不管数据渲染在主页面还是 iframe 里只要请求经过浏览器就能被捕获。这一点对“只关心数据、不想逆向前端代码”的人来说是降维打击。2. 搭一个能长期运行的采集环境依赖、目录与首个页面2.1 安装 Playwright 时最容易翻车的三个细节先装 Python 依赖这一步本身不难但很多人卡在后面的浏览器下载上pip install playwright playwright install chromium第一行命令装的是 Playwright 的 Python 绑定库第二行才是真正下载 Chromium 浏览器内核。如果你只执行了第一行运行脚本时会直接报错Executable doesnt exist这是最经典的翻车点。我在一台新服务器上第一次跑的时候也踩了这个坑还以为是环境变量问题排查了半天才发现是漏了后者。第二个容易翻车的地方是操作系统依赖。如果你用的是精简版 Linux 服务器光有 Chromium 二进制还不够缺少 libnss3、libatk 之类的系统库会导致浏览器启动失败。Playwright 提供了一个命令可以自动安装所有依赖playwright install --with-deps chromium第三个细节是版本一致性。Playwright 的 Python 包和浏览器内核有对应关系通常建议通过官方源安装避免从乱七八糟的镜像下载到旧版本导致 API 不兼容。装完后可以用一行命令验证环境是否正常python -c from playwright.sync_api import sync_playwright; print(playwright ok)没报错就说明 Python 绑定层没问题。浏览器能不能真正跑起来我习惯写一个最小脚本试一下直接打开一个空白页面并截图能生成图片就说明整个链路是通的。2.2 项目目录与第一个脚本我习惯把这种小项目做成本地任务目录结构保持简单huawei_reservation/ ├── monitor.py # 核心采集脚本 ├── store.py # 数据入库逻辑 ├── config.py # 商品 ID、URL、采集间隔配置 └── data/ └── reservation.db # SQLite 数据库为什么用 sync API 而不是 async说实话这个项目是低频定时任务单页面操作sync 的同步写法更直观出错了也容易调试。异步 API 适合同时操作大量页面、提高并发吞吐的场景属于后期优化方向前期完全没必要给自己增加心智负担。关于爬虫并发设计我后面会专门讲我的取舍。第一个脚本只做一件事启动 Chromium打开商城预约页打印页面标题然后截图保存。别看这步简单它验证的是整个链路的核心能力from playwright.sync_api import sync_playwright PRODUCT_URL https://www.vmall.com/product/xxx.html # 替换为实际商品页 with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page( user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 ) page.goto(PRODUCT_URL, wait_untildomcontentloaded, timeout30000) print(页面标题:, page.title()) page.screenshot(pathdata/screenshot.png, full_pageTrue) browser.close()这里有几个细节值得解释。headlessTrue是让浏览器不弹窗适合服务器环境wait_untildomcontentloaded表示等待 DOM 解析完成就返回比networkidle快很多后者在网络连接持续不断时可能一直等不到user_agent手动设置为常见浏览器的 UA可以减少被识别为自动化工具的概率。第一次跑通之后你会在data/目录下看到一张商品页截图说明 Playwright 已经能完整渲染这个页面了。接下来才到真正的核心环节预约数到底怎么拿。3. 拦截网络请求而不是解析 DOM预约数藏在 XHR 里3.1 用 DevTools 定位接口比读源代码快十倍在动手写代码之前我先打开浏览器 DevTools 手动看了一遍请求过程。操作很简单F12 打开开发者工具切到 Network 面板勾选 Fetch/XHR 过滤项然后刷新商品页。你会看到页面在加载过程中发了一连串网络请求其中某个接口的响应体里就藏着预约数相关字段。怎么快速定位我一般先看接口名字包含reservation、booking、reserve、count、stock这些关键词的优先点开。如果没有明显命中的就用 CtrlF 在响应内容里搜“预约”两个字搜到的那个请求基本就是目标。这一步不需要读任何前端源码纯粹靠浏览器自带工具就能完成。我经常跟朋友说动态页面爬虫的第一技能不是写代码而是“会看 Network 面板”。拿到接口之后留意一下它的请求方式、Query 参数和返回的数据结构。有些商城接口会把“预约人数”放在data字段里有些则可能是total或者嵌套在某个对象下面。这些信息决定了后面写解析逻辑时怎么取值。顺便说一种特殊情况如果页面把数据渲染在动态 iframe 里Network 面板同样能捕获 iframe 子资源的请求因为浏览器层面是统一的。Playwright 的page.on(response)是页面级事件也会收到 iframe 里的响应所以不用担心数据被 iframe 藏起来。3.2 用 page.on(response) 挂载监听器定位到接口后回到 Playwright 里最直接的做法是给页面挂一个 response 监听器。每当浏览器收到一个响应这个函数就会被调用我们在函数里过滤 URL只要发现目标接口就把它解析成 JSON 并打印出来import json from playwright.sync_api import sync_playwright PRODUCT_URL https://www.vmall.com/product/xxx.html TARGET_KEYWORDS (reservation, reserve, booking, count) def handle_response(response): url response.url if not any(k in url.lower() for k in TARGET_KEYWORDS): return try: data response.json() print(拦截到目标接口:, url) print(json.dumps(data, ensure_asciiFalse, indent2)[:1000]) except Exception: pass with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.on(response, handle_response) page.goto(PRODUCT_URL, wait_untildomcontentloaded, timeout30000) page.wait_for_timeout(8000) browser.close()page.wait_for_timeout(8000)是硬等待给页面初始化请求一点时间。实际生产环境不推荐用固定等待因为网络波动会导致早取或晚取更好的方式是下面要说的expect_response。如果你已经明确知道目标接口的 URL 特征可以不用监听器而是直接“等待”它出现with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() with page.expect_response(lambda res: reservation in res.url, timeout15000) as info: page.goto(PRODUCT_URL, wait_untildomcontentloaded, timeout30000) resp info.value data resp.json() print(拿到预约接口数据:, data) browser.close()这种写法的好处是Playwright 会阻塞到目标响应到达才继续执行不存在“等了几秒数据还没到”的尴尬。lambda里的判断条件可以写得宽松一点宁可多拦截一些请求也不要因为 URL 变化漏掉目标。3.3 从 JSON 里稳妥地取预约数接口返回的数据结构不是固定的不同系统返回格式差异很大。我见过有人写死data[reservationCount]结果接口字段一改就抓了个空。稳妥的做法是写一个递归查找函数自动在 JSON 里找最像“预约数”的字段def find_count(obj): if isinstance(obj, dict): for key, value in obj.items(): if isinstance(value, (int, float)) and (reservation in str(key).lower() or count in str(key).lower()): return int(value) if isinstance(value, (dict, list)): result find_count(value) if result is not None: return result elif isinstance(obj, list): for item in obj: result find_count(item) if result is not None: return result return None拿到数字之后还要注意单位问题。如果接口返回的是“12345”那直接存如果页面显示“1.2万人已预约”接口里可能是 12000也可能是 1.2需要结合实际换算。我实际处理时会把原始响应体和提取结果同时打印出来观察几轮确认单位统一后再写入数据库。这个步骤叫“字段校验”虽然枯燥但能避免你一觉醒来发现存了一堆错数据。4. 让脚本自己运转SQLite 存储、定时任务与异常恢复4.1 设计一个够用的时间序列表采集逻辑跑通后下一步是把数据存下来。这个项目的数据模型非常简单就是“时间点 商品 预约数”。我用 SQLite 就够了轻量、单文件、无需额外服务Python 自带驱动import sqlite3 def init_db(): conn sqlite3.connect(data/reservation.db) conn.execute( CREATE TABLE IF NOT EXISTS reservation_snapshot ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_id TEXT, product_name TEXT, reservation_count INTEGER, captured_at TEXT DEFAULT (datetime(now, localtime)) ) ) conn.commit() conn.close()插入数据时我强烈建议不要覆盖历史记录每次都新插入一行保留完整的时间序列。这样后面画趋势线时有足够多的采样点如果某次采集失败也可以通过前后两条记录看出缺口而不是得到一个“看起来很顺滑但实际缺数据”的曲线。4.2 用 APScheduler 实现每 10 分钟采集定时任务我选了 APScheduler它是一个很成熟的 Python 调度库支持 interval 模式适合这种固定间隔任务。核心逻辑就是把之前写的采集函数collect_once()注册到调度器里from apscheduler.schedulers.blocking import BlockingScheduler def collect_once(): try: count main() # 采集预约数返回 int if count is not None: save_snapshot(product_id, product_name, count) print(f[{datetime.now()}] 预约数: {count}) else: print(f[{datetime.now()}] 未获取到预约数) except Exception as e: print(f[{datetime.now()}] 采集异常: {e}) scheduler BlockingScheduler() scheduler.add_job(collect_once, interval, minutes10) print(监控任务已启动每 10 分钟采集一次) scheduler.start()为什么间隔设置成 10 分钟而不是 10 秒因为预约数不是一个秒级变化的数字10 分钟采样已经完全能还原它的增长曲线。过高的频率只会给商城服务器徒增压力也更容易触发风控。这个项目就是个小观察工具不需要也不能做得像抢购脚本那样高频。还有一点很关键异常处理。采集过程里可能碰到网络抖动、接口超时、页面改版、浏览器崩溃等各种问题。如果异常导致整个进程退出定时任务就断了第二天醒来会发现曲线少了一段。所以我习惯把采集函数体包一层 try/except异常只记录日志不让调度器跟着退出。4.3 数据校验出现回退时先自查预约数在正常情况下只增不减如果某条新数据比上一条还少那一定有问题。我在脚本里加了一个简单校验def validate_count(new_count): last_count get_last_count(product_id) if last_count is not None and new_count last_count: print(f警告预约数从 {last_count} 回退到 {new_count}数据异常不写入) return False return True回退的可能原因有三个第一接口返回了默认值或者 0比如被风控拦截后返回假数据第二解析逻辑出错把别的字段当成了预约数第三页面或接口改版字段含义变了。不管是哪种直接跳过这条数据并输出告警比把脏数据写进库里好处理得多。数据质量是趋势分析的生命线宁可少一条不能错一条。5. 长期跑下去的红线合规、风控与后续扩展5.1 频率控制是最廉价的防范写爬虫的人应该都听过“因爬虫入狱”的案例那些出事的项目绝大多数不是“爬了”这个行为本身而是突破技术防护、绕过访问控制、抓取非公开数据或者对服务器造成了实质性危害。我这套方案的核心原则是只抓公开页面上肉眼能看到的数据并且把采集频率压得很低。10 分钟一次一天就是 144 次请求对一个大型电商站点来说几乎可以忽略不计。这是我最推荐的爬虫并发设计思路先问自己“我真的需要那么高的并发吗”再问“我的目标站点能承受多大的压力”。很多场景下答案都是低频单机就够了。代理池、分布式节点这些方案是用来应对大规模采集或严格风控的放在这个项目里属于过度设计。我不打算在本文里讲绕过 WAF 之类的技术因为正常做数据观察的人根本不需要走到那一步。5.2 合规底线robots、接口公开性与数据使用合规方面我给自己定了四条硬规矩上线前先看目标站点 robots.txt明确哪些路径不允许抓取。只采集无需登录即可访问的公开接口不碰用户手机号、订单等隐私数据。不绕过、不破解任何技术防护措施页面怎么做我就怎么看。采集数据只用于个人技术学习和趋势分析不对外售卖、不用于商业决策。这套约束听起来保守但恰恰是它能长期稳定运行的前提。预约数是商城主动公开展示的营销数据抓取它本身没有侵犯任何人的隐私也不涉及未授权访问。但如果我为了追求实时性去逆向加密参数、模拟预约请求那性质就完全变了。做技术的人最怕的不是技术不行而是边界感模糊。5.3 从预约数到热度看板可视化与多商品扩展数据存了两三天之后就可以画趋势图了。matplotlib 加上 pandas十行代码就能把 SQLite 里的数据变成一张折线图import sqlite3 import pandas as pd import matplotlib.pyplot as plt conn sqlite3.connect(data/reservation.db) df pd.read_sql_query( SELECT captured_at, MAX(reservation_count) as reservation_count FROM reservation_snapshot WHERE product_id ? GROUP BY captured_at ORDER BY captured_at , conn, params(product_xxx,), ) conn.close() df[captured_at] pd.to_datetime(df[captured_at]) df.plot(xcaptured_at, yreservation_count, markero, figsize(10, 5)) plt.title(Reservation Trend) plt.xlabel(time) plt.ylabel(reservation count) plt.grid(True) plt.savefig(data/reservation_trend.png, dpi150)看到那条曲线一点点向上爬我才真正理解“把数据变成信息”是什么意思。后来我还把这个项目扩展成了多商品监控在配置文件里维护一个商品 ID 列表脚本循环采集所有目标商品SQLite 表里用 product_id 字段区分画图的时候按商品分别生成折线。逻辑上没有本质变化但能同时追踪好几款新品的预约节奏了。写到最后我想起第一次跑通完整链路那天我盯着终端里刷刷刷打印出来的预约数字又打开趋势图看那条缓缓上升的线确实比朋友圈里的截图有成就感得多。这种成就感不是来自“我绕过了什么防护”而是来自“我用工具把一个模糊的直觉变成了可视的证据”。如果你也准备做类似的观察项目记住一句话保持低频保持观察者身份这套脚本才能真正陪你跑完整个新品发布周期。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。