通过逆向分析后台API调用找出隐藏价格
通过查看页面背后的XHR、REST和GraphQL请求的DevTools信息,找出HTML中未显示的价格,然后直接重新发送这些请求。
“抓取技术实战”系列的第二篇文章
在上一篇文章中,我们遇到了难题:有时所需的价格信息根本不存在于requests获取的HTML内容中。它既没有嵌入在JSON-LD格式中,也没有藏在元标签里,完全找不到踪迹。但这并不意味着我们无计可施——通常只是意味着页面会在初始加载之后,通过单独的后台请求来获取这些数据。我们的目标就是找到那个请求并自行发起它。
第一步:打开“网络”标签页
我们将通过一个具体示例来演示:Target网站上的一个产品页面,地址为target.com/p/dyson-ball-animal-3-upright-vacuum/-/A-85269291(这只是用于演示——同样的方法几乎适用于所有产品页面)。
- 打开任意一个产品页面。
F12 键)。步骤2:找到你真正需要的请求
逐项查看列表,点击每一项并检查右侧的响应或预览面板。你需要寻找包含价格、评论数量或你所关注的其他数值的JSON数据。
还有更快的方法:使用“网络”标签页中的放大镜搜索框,输入页面上已显示的数值。以 Target 为例,输入“449.99”就会直接触发名为 deferred_enrichment/modules 的 POST 请求。其响应体是一个较大的 JSON 对象,在其中可以找到 "formatted_current_price":"$449.99
步骤 3:自行复现该请求
选中该请求并获取其完整 URL。此处为 POST 请求,但它所需的所有信息都已包含在查询字符串中:
https://www.target.com/cdui_orchestrations/v1/pages/pdp/deferred_enrichment/modules
?tcin=85269291
&key=9f36aeafbe60771e321a7cc95a78140772ab3e96
&channel=WEB
&device_type=DESKTOP
以下是在 Python 中复现该请求的代码:
import requests
url = "https://www.target.com/cdui_orchestrations/v1/pages/pdp/deferred_enrichment/modules"
params = {
"tcin": "85269291", # the product ID, from the page URL
"key": "9f36aeafbe60771e321a7cc95a78140772ab3e96",
"channel": "WEB",
"device_type": "DESKTOP",
}
resp = requests.post(url, params=params, headers={"User-Agent": "Mozilla/5.0"})
data = resp.json()
price = data["modules"][0]["module_data"]["data"]["product"]["price"]["formatted_current_price"]
print(price) # $449.99
这就是整个技巧所在。无需解析库,也无需使用 CSS 选择器——只需发送页面自身 JavaScript 在后台使用的相同查询即可。
提示:在开发者工具中,右键点击该请求并选择复制 → 复制为cURL格式。这样就能获得浏览器附带的完整请求头和Cookie信息,你可以将这些内容直接导入如Postman这样的客户端工具中,或将其转换为Python代码,从而准确判断哪些部分是真正必需的。
第4步:当API为GraphQL格式时
有些网站使用GraphQL而非传统的REST架构来处理所有请求。你可以通过以下特征识别这种模式:每个请求都会发送到同一个端点(通常是/graphql),且实际要查询的内容位于请求体中而非URL中:
{
"operationName": "GetPrice",
"variables": { "productId": "12345" },
"query": "query GetPrice($productId: ID!) { product(id: $productId) { price } }"
}
还有些网站会进一步采用持久化查询机制,即客户端不传输完整的查询文本,而只发送一个代表该查询的简短哈希值:
/graphql/GetPrice/a1b2c3d4e5f6...?variables={"productId":"12345"}
那个哈希值不是你可以自行生成的——必须直接从开发者工具中获取。由于它与网站的前端构建相关,对方端的任何更新都可能改变该哈希值,从而导致你的脚本无法正常运行。这就是依赖这种技术的真正持续成本。
第5步:检查该API密钥是否真的属于私有信息
再次查看第3步中的目标URL——注意查询字符串中明文显示的key=9f36aeafbe60771e321a7cc95a78140772ab3e96。这是你不应该能够访问的内容吗?
通常不是。最快速的验证方法是打开查看页面源代码(Ctrl+U),然后使用Ctrl+F搜索apiKey。你会在Target发送给每位访问者的配置块中,以纯文本形式找到完全相同的密钥。
如果某个值已经存在于页面的标记中——没有绕过任何机制,也不涉及身份验证——那它从一开始就并非机密信息。它只不过是一个用于在浏览器中运行的公共键而已。(有时它会存在于单独的 .js 文件中;如果“查看源代码”功能找不到相关内容,可以尝试使用 DevTools 的“源代码”面板,并通过 Ctrl+Shift+F 进行搜索。)
一个好的做法是:在费力反编译某个接口之前,先查看该公司是否已经提供了有文档说明的公共 API 或合作伙伴 API。这类 API 通常比通过 DevTools 暴力探索发现的接口要稳定得多。
第 6 步:当数据根本不是来自零售商时
有时,你追踪到的请求甚至不会指向零售商自己的域名——而是指向诸如 bazaarvoice.com 或 judge.me 这样的第三方服务。这说明该零售商并未自行搭建评论系统,而是购买了供应商的系统,并在页面上嵌入了该供应商提供的插件。
由于这些由供应商提供的插件 API 是为嵌入成千上万个无关的商店页面而设计的,而非专门用于检测和阻止抓取工具,因此其安全防护通常比零售商的官方网站要弱。
本文刻意未提及的内容
如果您已按照上述所有步骤操作,但请求仍返回403状态、空内容,或是类似安全检查的结果而非实际数据,那并非代码中的错误。这说明该网站已检测到您并非正常的浏览器会话。这一挑战相当重要,值得单独讨论,这也是下一篇文章要探讨的内容。
术语表
网络标签页 —— DevTools面板中的功能,用于记录页面发出的每一项请求以及发送的内容和返回的结果。
XHR / Fetch —— JavaScript用于在无需完整页面刷新的情况下发起后台请求的两种机制;可通过网络标签页进行筛选,仅显示这类请求。
REST API — 一种常见的API模式,每种资源都有其专属的URL,例如/products/123/price。
GraphQL — 一种基于单一端点的API风格,请求体中会明确指定所需的数据。
Persisted query — GraphQL的一种快捷方式,通过用服务器已识别的紧凑哈希值替代完整的查询文本来实现。
API密钥 — 附加在请求上用于标识该请求的值;有些确实是私密的,但很多则是公开的、面向客户端的密钥,可在任何人的浏览器中查看。
复制为cURL — DevTools中的一个选项,可将捕获的请求转换为可运行的命令行指令,便于查看其确切的请求头和Cookie信息。
第三方插件——指网站并非自行开发,而是从其他公司的产品中嵌入的功能,例如评论板块。