首页 / 文章

通过逆向分析后台API调用找出隐藏价格

通过查看页面背后的XHR、REST和GraphQL请求的DevTools信息,找出HTML中未显示的价格,然后直接重新发送这些请求。

发布者:REACTAPP.TOP阅读约 5 分钟1213 词

来源: Talking to the API Instead of the Page

“抓取技术实战”系列的第二篇文章

在上一篇文章中,我们遇到了难题:有时所需的价格信息根本不存在于requests获取的HTML内容中。它既没有嵌入在JSON-LD格式中,也没有藏在元标签里,完全找不到踪迹。但这并不意味着我们无计可施——通常只是意味着页面会在初始加载之后,通过单独的后台请求来获取这些数据。我们的目标就是找到那个请求并自行发起它。

第一步:打开“网络”标签页

我们将通过一个具体示例来演示:Target网站上的一个产品页面,地址为target.com/p/dyson-ball-animal-3-upright-vacuum/-/A-85269291(这只是用于演示——同样的方法几乎适用于所有产品页面)。

  1. 打开任意一个产品页面。
  • 在页面上右键点击,选择“检查”(或按 F12 键)。
  • 切换到网络面板。
  • 重新加载页面。
  • 应用Fetch/XHR过滤器——这会隐藏图片、CSS和字体相关的请求,仅显示实际传输数据的请求。
  • 步骤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信息。

    第三方插件——指网站并非自行开发,而是从其他公司的产品中嵌入的功能,例如评论板块。