多地区商品页面采集:记录语言、币种、价格与可售范围
为不同市场建立独立 Playwright 上下文,分别配置语言、时区与可选代理,保留请求条件和页面实际价格,避免把地区标签当成观测事实。
把浏览器语言改成 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 与包装对齐,再比较报价。父商品名称相同不足以证明同一规格。
显示有货却无法配送。 查看地址与配送限制;库存文案不是地区配送承诺,不能把下单失败归因于采集价格错误。
某个地区失败。 保留该条件的失败状态,不把另一地区数据复制过去补齐表格。修复该环境后单独补采,再统一比较。
多地区采集首先是受控条件下的观察。把配置、网络和页面业务事实分别保存,结果才具有可比性。
需要企业代理方案?
我们可根据目标站点、并发规模与稳定性目标提供定制方案。