如何在Python中构建价格下跌警报:使用无损抓取浏览器进行实时监控
Senior Web Scraping Engineer
主要要点:
- 先渲染,再读取价格。 现代零售价格是在客户端,通过JavaScript运行后根据地区个性化处理的。真正渲染的页面——而不是原始的HTTP获取——才是购物者实际看到的数字。Scrapeless Scraping Browser在一个反检测的云浏览器中渲染产品页面,并返回填充后的DOM。
- 锁定代理国家,因为价格与地域相关。 价格、货币和可用性因地区和IP信誉而异。设置
proxy_country="US"(或任何警报跟踪的市场)可以使每次检查在相同的出口,这样价格历史可以进行真正的比较。 - 价格历史只是一个附加日志。 每次检查写入一条
{product, url, price, currency, checked_at}记录。警报逻辑只是一个简单的比较:最新价格是否低于之前的最低价?这就是整个决策。 - 价格下降会触发Webhook。 当比较结果显示“更低”时,一行
requests.post到Slack/Discord/email中继端点会发送警报。没有队列,没有代理——只有一个HTTP调用。 - 设定时间,随即离开。 定时任务、计划任务或无服务器定时器以一定节奏运行检查。渲染→提取→比较→警报的循环小到可以无人值守地运行。
- 适用于大多数公共产品页面。 同样的循环适用于几乎任何在DOM中渲染价格的产品页面——固定出口,锚定在稳定的价格节点上,并重用比较。
- 免费开始。 新的Scrapeless账户包括免费的Scraping Browser运行时——请在Scrapeless网站注册。
引言:停止自己刷新的产品页面
价格跟踪是人们抓取公共网络的最常见原因之一。寻求优惠的人想在最低价时购买。定价团队想知道竞争对手何时削减某个SKU。采购方希望在补货前得到提前通知。任务总是相同的:观察产品页面,注意何时价格下跌,并告知他人。
问题在于,产品页面不再是静态文档。零售目录在客户端动态加载:页面作为一个薄壳抵达,价格、货币、促销标志和可用性在JavaScript运行后被绘制。一个普通的HTTP请求返回的是外壳,而不是价格。这个数字也与地区相关——同一URL可能因出口IP显示不同的价格、货币或库存状态——许多网站在反机器人检查后阻止自动请求,这会在HTTP 200状态下返回挑战页面。一个在测试中“有效”的纯HTTP轮询器可能会悄悄开始记录挑战页面,而不是价格。
这篇文章通过一个基于Scrapeless Scraping Browser的Python工作流程,填补了这些空白:在一个固定的美国住宅出口的反检测云浏览器中渲染产品页面,从渲染的DOM中提取价格,将其附加到一个小的价格历史存储,比较最新值与之前的低点,当价格下跌时触发Webhook。一个调度程序按一定节奏运行这个循环。同样的渲染原语为本地零售定价提供工具比较,例如2026年最佳Zillow抓取工具。
你可以用它做什么
- 个人优惠提醒。 关注一份心愿清单,一旦其中任何产品降到目标价格以下就会收到提醒。
- 竞争价格监测。 定价团队以滚动的方式跟踪竞争对手的SKU,并在几分钟内响应价格调整,而不是几天。
- 补货和可用性监测。 将价格读取与可用性读取配对,以同时提醒“已补货”和“现在更便宜”。
- MAP风格的漂移检测。 品牌所有者在跟踪的产品列表低于预期底价时进行标记。
- 采购时机。 在几周内记录价格历史,以便在提交采购订单之前识别产品折扣的节奏。
- 历史价格数据集。 附加日志也可以作为干净的时间序列,用于图表、趋势分析或模型输入。
在Scrapeless,我们仅访问公开可用的数据,并严格遵守适用的法律、法规和网站隐私政策。本文中的内容仅用于演示目的。
为什么选择Scrapeless Scraping Browser
Scrapeless Scraping Browser是一种可定制的反检测云浏览器,旨在用于网络爬虫和AI代理。特别是为了价格下降警报,它提供:
- 覆盖195多个国家的住宅代理,因此警报可以将其出口固定在它所跟踪的市场——价格、货币和可用性与地理相关,而固定的
proxy_country使每次检查可比较。 - 云端JavaScript渲染,因此价格、促销标志、货币符号和库存状态以填充后的DOM形式到达,而不是作为一个空的React外壳。
- 防检测指纹识别在每个会话中,因此产品页面呈现出与有机流量相同的视图——包括真实价格网格,而不是在HTTP 200状态下提供的挑战页面。
- 会话持久性跨越导航,因此在着陆产品页面之前预热主页的检查可以保持cookies和状态在一个运行内的一致性。
- 干净的CDP端点,Puppeteer或Playwright直接驱动——通过CDP连接,云浏览器完成其余工作。
在app.scrapeless.com的免费计划中获取您的API密钥。Scraping Browser产品页面涵盖运行时,Proxy Solutions涵盖支持的住宅出口。
先决条件
- Python 3.10或更高版本。
- 一个Scrapeless账户和API密钥——请在Scrapeless网站注册。SDK从
SCRAPELESS_API_KEY环境变量中读取它。 - Playwright for Python,它通过CDP驱动云浏览器。连接详情和库指南请参见docs.scrapeless.com。
- 对终端的基本熟悉程度和接收警报的Webhook URL(Slack、Discord或任何电子邮件中继)。
安装
安装Playwright以通过CDP驱动云浏览器,并安装requests以供Webhook调用:
bash
pip install playwright requests
设置您的API密钥,以便它可以访问连接URL:
bash
export SCRAPELESS_API_KEY=your_api_token_here
这就是完整设置。Playwright的connect_over_cdp连接到Scrapeless Scraping Browser端点,并驱动在Scrapeless云中运行的真实浏览器——不需要本地Chromium下载,因为渲染发生在云端。
步骤1 — 构建云浏览器连接URL
Scrapeless Scraping Browser是一个CDP端点。以您的API密钥作为token和出口市场作为proxyCountry构建WebSocket URL;Playwright直接连接它。
python
import os
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
def scraping_browser_url(proxy_country: str = "US", session_ttl: int = 240) -> str:
# API密钥作为`token`附加在URL上;出口和生存时间是查询参数。
params = urlencode({
"token": os.environ["SCRAPELESS_API_KEY"],
"sessionTTL": session_ttl,
"proxyCountry": proxy_country,
})
return f"wss://browser.scrapeless.com/api/v2/browser?{params}"
proxyCountry是价格监控的支撑标志:相同的产品URL可以根据地区呈现不同的价格、货币或可用状态,因此固定出口可以确保每个记录的价格在同一市场上。sessionTTL是会话的生存时间,以秒为单位——保持足够长以呈现页面并读取价格。
步骤2 — 渲染产品页面并读取价格
将CDP客户端连接到会话,渲染产品页面,并从填充的DOM中提取价格。通过美国住宅Scrapeless会话实时渲染的Walmart搜索URL返回HTTP 200,带有真实的产品网格——标题解析为laptop - Walmart.com,页面暴露出形式为https://www.walmart.com/ip/<slug>/<id>的产品链接。这些/ip/页面是价格监控读取的每个产品目标。下面的示例跟踪这样一个产品页面。
产品URL直接呈现。作为对需要冷请求到深层URL的站点的防御措施,示例首先在该站点的主页上预热会话,然后在同一会话中导航到产品URL——当不需要时无害,当需要时有用。
python
PRODUCT = "示例15英寸笔记本电脑"
URL = "https://www.walmart.com/ip/example-15-inch-laptop/123456789"
def read_price(url: str) -> dict:
# 通过Playwright使Scrapeless Scraping Browser通过CDP运行。
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(scraping_browser_url("US"))
page = browser.new_page()
# 首先在主页上预热会话,然后登陆产品页面
# 以便价格网格渲染。
page.goto("https://www.walmart.com/", wait_until="domcontentloaded")
page.wait_for_timeout(2500)
page.goto(url, wait_until="domcontentloaded")
page.wait_for_timeout(3000) #让价格节点充水
# 固定在一个稳定的价格节点上。零售商会轮换哈希类名,
# 所以更喜欢语义锚点(itemprop,data-testid,aria-label)。
price_node = page.query_selector('[itemprop="price"], [data-testid="price-wrap"]')
price_text = price_node.inner_text() if price_node else ""
browser.close()
# 标准化"$1,299.00" -> 1299.0
digits = "".join(ch for ch in price_text if ch.isdigit() or ch == ".")
price = float(digits) if digits else None
return {"product": PRODUCT, "url": url, "price": price, "currency": "USD"}
渲染返回与有机流量所见相同的DOM,因此价格节点携带实时的、地区正确的数字。应锚定在语义选择器(itemprop、data-testid、aria-label)上,而不是哈希的类名——类名在部署中会轮换,但语义锚点不会。如果一个页面将美元和中心分开为单独的节点,请读取两个节点,然后在规范化之前将它们连接起来。
在免费计划中获取您的API密钥: Scrapeless网站
第3步 — 存储价格历史
价格历史是一个仅可附加的日志。每次检查写入一条记录;该文件是第4步比较的真实来源。一个以换行符分隔的JSON文件(JSONL)保持仅可附加,并且非常容易读取回去:
python
import json
from datetime import datetime, timezone
HISTORY_FILE = "price_history.jsonl"
def append_history(record: dict) -> dict:
record = {
**record,
# 可读的UTC时间戳;如果需要严格的时间序列,请换成Unix纪元。
"checked_at": datetime.now(timezone.utc).strftime("%d-%b-%Y %H:%M UTC"),
}
with open(HISTORY_FILE, "a", encoding="utf-8") as f:
f.write(json.dumps(record) + "\n")
return record
def load_history(url: str) -> list[dict]:
rows = []
try:
with open(HISTORY_FILE, encoding="utf-8") as f:
for line in f:
row = json.loads(line)
if row.get("url") == url:
rows.append(row)
except FileNotFoundError:
pass
return rows
每条记录的规范格式为{product, url, price, currency, checked_at}形状。checked_at是以读取时间写入的UTC ISO时间戳,因此每一条条目都是自我描述的。对于超过少量产品的情况,将JSONL文件替换为SQLite表或任何数据库——模式是相同的,比较逻辑也没有变化。
第4步 — 与之前的最低价进行比较并决定
警报决策是单一比较:最新价格是否低于当前为此URL看到的最低价格?拉取前一个历史,找到之前的最低值,然后进行比较。
python
def is_price_drop(url: str, current: float) -> dict:
prior = [r["price"] for r in load_history(url) if r.get("price") is not None]
previous_low = min(prior) if prior else None
dropped = (
current is not None
and previous_low is not None
and current < previous_low
)
return {
"dropped": dropped,
"current": current,
"previous_low": previous_low,
"delta": (current - previous_low) if dropped else None,
}
第一次检查产品时没有之前的历史,因此previous_low为None,没有警报触发——运行会种下日志。从第二次检查开始,任何价格严格低于当前最低值的都被视为下降。要针对目标价格而不是历史最低价进行警报,可以将current与固定阈值进行比较;要针对与上次检查相比的任何下降进行警报,可以将其与最后一条记录进行比较,而不是最小值。无论触发哪个规则,存储和渲染步骤保持不变。
第5步 — 在价格下降时触发Webhook
当比较结果显示“下降”时,发送警报。Webhook是最简单的传递路径——一次requests.post请求到Slack、Discord或电子邮件中继端点。没有中介,没有队列。
python
import requests
WEBHOOK_URL = os.environ["PRICE_ALERT_WEBHOOK"] # Slack / Discord / relay URL
def send_alert(record: dict, decision: dict) -> None:
message = (
f"价格下降: {record['product']}\n"
f"现在 {record['currency']} {decision['current']:.2f} "
f"(之前 {decision['previous_low']:.2f}, "
f"下降 {abs(decision['delta']):.2f})\n"
f"{record['url']}"
)
response = requests.post(WEBHOOK_URL, json={"text": message}, timeout=15)
response.raise_for_status()
raise_for_status() 会显示来自Webhook的非2xx响应,以便配置错误的端点可以明显失败,而不是默默丢弃警报。{"text": message}正文符合Slack和Discord的传入Webhook格式;根据接收端点的期望调整JSON形状。
将这五个步骤连接成一次检查:
python
def check_once():
reading = read_price(URL) # 第2步:渲染 + 提取
record = append_history(reading) # 第3步:存储
decision = is_price_drop(URL, record["price"]) # 第4步:比较
if decision["dropped"]:
send_alert(record, decision) # 第5步:警报
return record, decision
第6步 — 安排检查
循环足够小,可以在任何调度器上无人值守运行。大多数交易监控每日检查即可;竞争监控可能每小时运行一次。在每次运行中生成一个新的会话,以保持出口的清洁。
在Linux或macOS上,cron条目每天09:00运行脚本一次:
bash
# crontab -e
0 9 * * * cd /opt/price-watch && /usr/bin/python3 check.py >> watch.log 2>&1
在Windows上,使用任务调度程序注册相同的命令。无服务器定时器(计划的云函数)同样有效——该脚本没有超出历史文件的长久状态,因此适合无状态调用。对于许多产品的监视列表,循环遍历URL并保持并发适中——每个主机大约三个并行渲染是合理的上限——以便出口保持稳定。
python
WATCHLIST = [
"https://www.walmart.com/ip/example-15-inch-laptop/123456789",
"https://www.walmart.com/ip/example-wireless-headphones/987654321",
]
def run_watchlist():
for url in WATCHLIST:
reading = read_price(url) # 每次读取都重新连接,使用美国出口
record = append_history(reading)
decision = is_price_drop(url, record["price"])
if decision["dropped"]:
send_alert(record, decision)
您将获得的结果
每次检查都会将一条记录附加到历史日志中。记录的格式是标准的价格监控记录:
json
// 架构准确反映append_history写入的内容。
// 字段值是示例样本,而非任何产品的实时读取。
{
"product": "示例 15 英寸笔记本电脑",
"url": "https://www.walmart.com/ip/example-15-inch-laptop/123456789",
"price": 1299.00,
"currency": "USD",
"checked_at": "2026年5月25日 09:00 UTC"
}
随着时间的推移,日志将变成干净的价格时间序列——每次检查一行,便于图表或供趋势模型使用。对于在大规模运行前值得了解的几点诚恳观察:
- 地理位置驱动数字。 记录的价格只有在其读取的出口上才有意义。对于给定的监视,保持
proxy_country固定;如果比较跨市场的价格,请将国家与每条记录一起存储。 - 选择器稳定性。 针对
itemprop、data-testid或aria-label节点进行锚定。哈希类名称在部署之间会发生变化,并将静默开始返回None——当价格停止解析时,请重新检查渲染的DOM并收紧锚定。 - 拆分价格节点。 一些页面将美元和美分(或货币符号和金额)渲染在不同的元素中。读取每个并在归一化之前连接,否则解析的数字将是错误的。
- 状态码不是价格。 页面可以返回HTTP 200,但仍然可能是挑战或插页。通过检查价格节点是否存在且可解析来确认真实读取,而不仅仅是请求是否成功。
- 可空价格。 将缺失的价格视为
None而不是零。步骤4中的比较已经跳过None读取,因此偶尔不可解析的页面不会触发假警报。
结论:五步完成价格监控
整个流程简化为五步:在反检测云浏览器中渲染产品页面,从填充的DOM中提取价格,将其附加到历史日志中,与先前的最低价格进行比较,并在价格下降时触发Webhook——使用调度程序以某一节奏运行循环。由于渲染步骤返回与有机流量看到的相同视图,因此记录的价格是购物者实际支付的区域正确的数字。
要将监控扩展到其他零售商,请重用相同的循环:将出口固定在正确的市场上,为该网站的稳定价格节点锚定,并保持比较不变。零售商特定的渲染和选择模式可以直接融入步骤2中。固定代理国家,锚定语义选择器,将缺失的价格视为可空,并保持每个主机的并发适中。
准备好构建您的AI驱动数据管道了吗?
加入我们的社区,申请免费计划并与构建价格监控管道的开发者联系:Discord · Telegram。
在Scrapeless网站注册以获得免费的Scraping Browser运行时,并将上述模式应用于管道需要的产品和地区。请查看定价以了解计划详细信息。
常见问题
Q1:建立价格下降警报合法吗?
跟踪公开可见的价格是一种常见且广泛实践的活动,但规则因地区和每个网站的服务条款而异。请阅读目标网站的条款,仅抓取公开可用的数据,避免收集个人信息,并就您的具体用例咨询法律顾问。Scrapeless 仅访问公开可用的数据,同时遵守适用的法律和网站隐私政策。
Q2:我需要代理吗?国家是否重要?
两者都需要。价格、货币和可用性因地区和 IP 声誉而异,因此需要使用住宅出口,国家是关键。将 proxy_country 固定到警报跟踪的市场——在此示例中为“US”——以便每个记录的价格进行横向比较。要在多个市场监控产品,每个国家运行一个监控,并将国家与每个价格一起存储。
Q3:为什么不只是发送一个 HTTP 请求并解析价格?
大多数零售价格在 JavaScript 运行后被客户端渲染,因此原始的 HTTP 获取返回一个空的壳,而不是价格。许多网站还会在 HTTP 200 状态下对自动请求提供一个反机器人挑战页面。在反检测的云浏览器中渲染页面会返回与有机流量看到的相同填充的 DOM,包括真实价格节点。
Q4:我如何确认读取的价格是真实价格而非挑战页面?
检查价格节点是否存在,并解析为数字,而不仅仅是请求是否返回了 HTTP 200。比较逻辑将缺失或不可解析的价格视为 None,因此不会触发警报,因此偶尔的插页不会产生虚假的下降。当价格持续无法解析时,请重新检查渲染的 DOM 并收紧选择器。
Q5:检查应多久运行一次?可以监控多少产品?
每日检查适合大多数交易监控;竞争监测可能每小时运行一次。对于监控列表,在一次调度运行中循环遍历 URL,并保持并发适度——每个主机大约三次并行渲染是一个合理的上限——以便出口行为良好。对于更大的扩展,请在主机之间进行分片。
Q6:可以在没有 AI 代理的情况下运行吗?
可以。步骤 1-6 中的 Python 脚本可以独立端到端运行——连接、渲染、提取、存储、比较、警报、调度。AI 代理是一种方便的方法,可以以自然语言驱动渲染和选择步骤,但这是可选的;Playwright 和调度程序是循环所需的一切。
Q7:在网站重新设计后价格停止解析——发生了什么变化?
网站更换了类名。请重新检查渲染的 DOM,并重新锚定在语义选择器(itemprop、data-testid、aria-label)上,而不是哈希类。将价格视为可空的,以便重新设计不会向历史记录注入错误的数字。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



