多地区商品页面采集:记录语言、币种、价格与可售范围

为不同市场建立独立 Playwright 上下文,分别配置语言、时区与可选代理,保留请求条件和页面实际价格,避免把地区标签当成观测事实。

亿牛云技术团队2026年10月28日5 分钟阅读

把浏览器语言改成 en-US,不代表网站就会显示美元;使用某地区出口,也不保证商品可以配送到该地区。站点还可能参考 Cookie、账号和页面中的地区选择。

本文建立两组独立访问条件,采集同一商品并记录语言、出口、价格和库存文案。示例使用公开图书练习页,它没有完整地区配送系统,因此可能在两组条件下都显示 GBP。相同结果也是有效观察,不能为了做地区对照强行改币种。

准备输入地址

先在浏览器找到下列页面并确认内容,再从地址栏复制完整地址。服务或仓库地址从相应账户设置、官方项目的接入说明或克隆菜单取得。不要从示例输出中反推真实地址。

  • BOOK_DETAIL_URL:Books to Scrape 的 A Light in the Attic 详情页地址。

在执行后续命令的同一个 Bash/WSL 终端中设置:

export BOOK_DETAIL_URL='<paste-the-actual-book-detail-url>'

下文 JSON 或任务参数中的 <变量名> 是待替换标记,提交前填写刚复制的实际值;JSON 不会自动展开环境变量。结果示例中的 example.16yun.cn 只表示来源字段的形状,不是本文部署的演示站。

先区分配置与观察

字段表示什么
requested_market本次希望检查的市场标签
configured_locale/timezone浏览器设置
egress_origin回显服务看到的出口,不自动等于国家
document_language页面实际声明的语言
price_display/currency当前页面的金额与币种合同
availability_text库存文案,不等于地区配送结论
shipping_eligibility没有明确证据时保持 null

这些字段不能相互替代。地区检测是额外观测,语言是上下文设置,可售与配送由站点业务规则决定。

安装与配置两个独立环境

使用 Python 3.12:

python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install playwright
python -m playwright install chromium

创建 regions.json:

[
  {"market": "US", "locale": "en-US", "timezone_id": "America/New_York", "proxy": null},
  {"market": "GB", "locale": "en-GB", "timezone_id": "Europe/London", "proxy": null}
]

proxy 为 null 表示没有设置独立出口,两个上下文可能使用同一个本地网络。若任务需要真实地区出口,将对应值换成你自己的可用配置,例如:

{"server":"http://proxy.16yun.cn:8888","username":"replace-with-user","password":"replace-with-password"}

这是格式示例,不是可用凭据。两个上下文分别配置代理的方式见 Playwright BrowserContext 文档。

完整的采集与记录程序

创建 regional_product.py:

import asyncio
import os
import json
from datetime import datetime, timezone
from pathlib import Path
from playwright.async_api import async_playwright
 
TARGET = os.environ["BOOK_DETAIL_URL"]
 
async def main():
    regions = json.loads(Path("regions.json").read_text(encoding="utf-8"))
    records = []
    async with async_playwright() as pw:
        browser = await pw.chromium.launch()
        try:
            for config in regions:
                context = await browser.new_context(
                    locale=config["locale"],
                    timezone_id=config["timezone_id"],
                    proxy=config.get("proxy"),
                )
                try:
                    page = await context.new_page()
                    echo = await page.goto("https://httpbin.org/ip")
                    if echo is None or echo.status != 200:
                        raise ValueError("无法取得出口观察")
                    origin = (await echo.json()).get("origin")
                    response = await page.goto(TARGET)
                    if response is None or response.status != 200:
                        raise ValueError("商品页面加载失败")
                    await page.locator(".product_main h1").wait_for()
                    title = (await page.locator(".product_main h1").inner_text()).strip()
                    fields = {}
                    for row in await page.locator("table.table-striped tr").all():
                        key = (await row.locator("th").inner_text()).strip()
                        fields[key] = (await row.locator("td").inner_text()).strip()
                    price = fields.get("Price (incl. tax)")
                    if not title or not price or not price.startswith("£"):
                        raise ValueError("练习页价格合同不符合预期")
                    records.append({
                        "requested_market": config["market"],
                        "configured_locale": config["locale"],
                        "configured_timezone": config["timezone_id"],
                        "proxy_configured": config.get("proxy") is not None,
                        "egress_origin": origin,
                        "navigator_language": await page.evaluate("navigator.language"),
                        "document_language": await page.locator("html").get_attribute("lang"),
                        "title": title, "upc": fields.get("UPC"),
                        "price_display": price, "currency": "GBP",
                        "availability_text": fields.get("Availability"),
                        "shipping_eligibility": None,
                        "source_url": page.url,
                        "observed_at": datetime.now(timezone.utc).isoformat(),
                    })
                except Exception as error:
                    records.append({
                        "requested_market": config["market"],
                        "status": "failed", "error_type": type(error).__name__,
                    })
                finally:
                    await context.close()
        finally:
            await browser.close()
    Path("regional-products.json").write_text(
        json.dumps(records, ensure_ascii=False, indent=2), encoding="utf-8"
    )
    print({"conditions_checked": len(records)})
 
asyncio.run(main())

执行:

python regional_product.py

每组条件使用独立 Cookie 存储与页面上下文,避免一个地区的站内设置影响另一个。输出不包含代理账号密码,只记录是否配置了代理。

如何解释同价、异价与缺失

当前练习页的币种合同是 GBP,因此代码不根据 US 标签换成 USD。它也没有提供地区配送证据,所以 shipping_eligibility 为 null,不能把 In stock 自动解释成“可配送到美国”。

如果两份结果相同,先确认配置已生效,再判断站点是否本来就没有地区化。若不同,检查币种、税费、规格、账号与观测时间是否一致,再比较金额。不能把 20 USD 与 18 GBP 当成同单位直接相减。

echo 返回的 origin 是出口观察,不提供可靠国家判定。若任务需要国家证据,另选明确的地理验证方式,并保存判断来源;不要用你给代理起的名称代替验证。

接入真正有地区选择的商店

在每个 context 中分别设置站点地区,检查页面上的当前市场标记,再读取商品。若站点通过 Cookie 保存市场,记录建立 Cookie 的操作与结果;若通过账号地址决定配送,使用对应测试账号并明确其范围。

币种优先从站点明确的 ISO 字段或可解释文案读取,保留显示金额和货币代码。本文只为已知练习站固定 GBP,不是全球货币符号识别器。

可售范围可能需要具体邮编、配送方式或规格。将这些条件纳入记录,把“商品有库存”“本地区可售”“此地址可配送”分开,不合并成一个 available 布尔值。

常见问题与修复

语言改了价格没变。 检查页面是否依赖站内市场或账号设置。语言设置不保证业务市场改变。

两组出口一样。 配置为 null 时这是预期;即使配置不同节点,也可能共享出口。先验证网络路径,再检查地区业务结果。

登录状态串了。 检查是否复用了同一 context 或同一份可写 Profile。每个条件独立建立状态,不只修改 market 字段。

价格不同但规格也不同。 先按 UPC/SKU 与包装对齐,再比较报价。父商品名称相同不足以证明同一规格。

显示有货却无法配送。 查看地址与配送限制;库存文案不是地区配送承诺,不能把下单失败归因于采集价格错误。

某个地区失败。 保留该条件的失败状态,不把另一地区数据复制过去补齐表格。修复该环境后单独补采,再统一比较。

多地区采集首先是受控条件下的观察。把配置、网络和页面业务事实分别保存,结果才具有可比性。

需要企业代理方案?

我们可根据目标站点、并发规模与稳定性目标提供定制方案。