返回博客

亚马逊价格抓取:Python、代理与Excel导出

Adrian Cole

2026年9月29日 · 应用场景 · 20 分钟阅读

亚马逊价格抓取适用于价格监控、竞品研究、选品和跨市场分析。可用的价格快照不只是一个数字,还应同时记录ASIN、站点、币种、卖家、变体、配送位置和采集时间。否则,脚本可能把不同国家站点、其他卖家、优惠券价格或验证码页面误当成目标报价。

本文围绕授权的数据采集流程,说明如何识别请求被拒、验证码和错误页面,如何选择Rola IP的网络会话,如何校验报价上下文,以及如何把结果安全地导出到Excel。文章中的示例用于合规的价格研究和市场监测,不构成绕过亚马逊访问控制或平台规则的承诺。

核心摘要: 先确认数据来源、账户权限和亚马逊使用条款,再决定使用API、托管数据源还是浏览器采集。对授权的浏览器流程,要让站点、配送位置、币种和代理出口保持一致;相关页面使用同一会话,解析前先检查HTTP状态、最终URL、ASIN和验证码信号。每条观察结果都要保存验证状态,而不是只保存价格。Rola IP提供网络路由,商品报价的准确性仍取决于页面、会话、选择器和人工复核。

抓取亚马逊价格时最先遇到的故障

速率限制与集中请求

很多失败首先是流量设计问题,而不是Python代码语法问题。短时间内从单个IP连续请求数百个商品页,会形成集中的请求模式,结果可能是响应变慢、返回错误页或出现挑战页面。亚马逊不会公开一个适用于所有站点、账户和页面的“安全请求数”,因此不应在文章中承诺固定的每分钟上限。更可靠的做法是从小样本开始,记录失败率、并发、重试次数和Retry-After,再按照授权范围调整工作负载。

验证码和挑战页替代商品页

解析器期待的是商品标题和报价,服务器返回的却可能是验证码、登录页或通用错误页。HTTP状态即使是200,文档也不一定是商品页面。如果代码只判断status_code,就可能把挑战文本写入价格字段,或把缺失价格错误地保存成降价。

amazon-captcha-challenge-page

提取前至少检查以下信号:

  • 页面是否包含预期的ASIN和商品标题;
  • 最终主机名是否为预期的亚马逊站点;
  • 目标报价容器内是否有价格;
  • 是否出现验证码、登录、验证或挑战元素;
  • 是否返回403、429、503等非预期状态。

验证码、登录墙或非2xx响应应结束本次运行,随后检查授权、会话和工作负载。无限重试只会增加噪声和代理流量;验证码解决服务也不能解决数据来源没有权限或解析器选择错误的问题。若需要了解验证码类型和触发机制,可参考传统验证码与隐形验证码,但该资料是诊断参考,不代表代理能够移除挑战。

网络信誉和地区会改变结果

IP只是请求上下文的一部分。Cookie、账户状态、浏览器特征、协议行为、请求集中度和页面显示的配送设置,都可能影响响应。亚马逊没有公开完整的零售站点检测模型,因此应记录这些输入,不能臆测平台内部评分逻辑。

面向美国市场的研究,至少要固定亚马逊站点、配送ZIP、币种、Cookie和账户状态。如果研究需要模拟美国网络位置,还要选择并验证美国出口代理。美国IP不能单独决定价格、卖家或配送优惠;相关商品和变体应在同一会话中核验。

价格区块是动态的

商品页通常会同时出现当前价、划线价、优惠券后价格、其他卖家报价、运费和推荐商品价格。部分字段由JavaScript渲染或异步更新,页面结构也会变化。

amazon-product-page-price-example

先确定业务真正需要的字段:Buy Box研究要把Buy Box卖家与显示价格绑定;折扣分析则应分开保存当前价和参考价。页面中出现的第一个美元金额经常不是目标商品价格。

先决定亚马逊价格数据从哪里来

开始写爬虫前,先决定数据源。若符合条件的Amazon或合作伙伴API已经提供所需字段,通常是维护成本最低的方案。对于需要自行维护请求、浏览器或解析器的项目,可先了解网络爬虫代理适用的网络层边界。Amazon Creators API编程建议适用于符合条件的集成,不等于所有价格监测项目都可以直接使用;要核对访问模式、字段范围和许可用途。

需要长期监控多个站点时,托管数据服务可能比自行维护选择器更省工程成本。范围小、已获授权且能控制运行环境的项目,才适合使用HTTP或浏览器采集。定时任务启动前,应阅读Amazon使用条款以及账户或API附带的协议。

如果流程必须使用未经授权的账户、会暴露个人信息、与目标站条款冲突,或反复收到挑战页面,应暂停自动化。页面公开可见,不等于自动化访问和再利用当然获得许可。

Rola IP在亚马逊价格监控中的作用

Rola IP位于网络层。用于价格监控的价格监控代理可提供选定的出口位置和会话行为;浏览器自动化的连接方式应以当前Rola文档和Playwright官方网络配置文档为准。代理只负责路由,不负责判断商品报价是否正确。

rola-ip-rotating-residential-proxy-setup

会话策略应与研究任务匹配:

研究任务 会话方式 原因
比较互不相关的ASIN独立样本 在来源条款和请求策略允许时,可考虑轮换 每条观察彼此独立
检查同一商品的变体和卖家报价 使用粘性会话 配送、Cookie和站点上下文保持一致
复现一次失败响应 先保持所有输入不变,再一次只改一个变量 便于区分路由、页面还是解析器导致失败

轮换住宅代理和静态住宅代理都可能支持会话连续性,取决于套餐和配置。需要较长身份连续性的任务可以评估静态住宅或ISP代理;数据中心路由适合夹具测试、连通性检查或目标明确接受的场景。先在实际计划运行的环境中采集小样本。

代理只处理网络路由。选择器失效、跳到了错误站点或收到了挑战页时,单独更换IP不会修复数据。可以使用代理检测工具检查工具实际报告的连通性和出口IP,但仍要在采集客户端分别验证最终URL、站点、ASIN、卖家和价格。连通性不是报价准确性的证明。

使用Python和Playwright做一次受控检查

Playwright控制真实浏览器引擎,可以渲染JavaScript并检查浏览器实际收到的页面,适用于Requests返回不完整HTML的场景。它不会改变授权要求,也不能保证亚马逊始终返回商品页。下面示例只做一次授权的Amazon.com商品检查,并把候选观察写入CSV和Excel;只有所有上下文检查通过时,才标记为candidate_context_complete。选择器仍需随页面结构维护。

amazon-product-page-automated-browser

环境准备

  • Python3.10或更高版本;
  • 在隔离环境中安装Playwright、pandas和openpyxl;
  • 已授权的商品清单和采集流程;
  • 从当前Rola控制台或文档复制的HTTP代理主机、端口、用户名和密码;
  • 预期站点、ASIN、ISO币种代码和配送标签。
python -m pip install playwright pandas openpyxl
python -m playwright install chromium
python --version
python -m playwright --version
python -m pip show pandas openpyxl

连接信息放在环境变量中,不要提交到代码仓库,也不要在日志中打印完整代理URL。需要把脚本接入Rola端点时,可参阅Python代理接入中的当前参数说明:

export ROLA_HTTP_PROXY_SERVER="http://CURRENT_ROLA_HOST:PORT"
export ROLA_PROXY_USERNAME="replace-with-account-username"
export ROLA_PROXY_PASSWORD="replace-with-account-password"
export AMAZON_PRODUCT_URL="https://www.amazon.com/dp/B0XXXXXXXXX"
export EXPECTED_ASIN="B0XXXXXXXXX"
export EXPECTED_MARKETPLACE="amazon.com"
export EXPECTED_CURRENCY="USD"
export EXPECTED_DELIVERY_LABEL="10001"
export PLAYWRIGHT_STORAGE_STATE="amazon-context.json"

示例明确使用带用户名和密码的HTTP代理语法。Playwright对HTTP(S)代理的认证参数有明确说明,也说明浏览器可以通过SOCKS5加载,但这不等于同一Chromium配置一定支持经过认证的SOCKS5端点。除非当前Rola端点和浏览器版本已共同测试,否则不要把http://擅自改成socks5://。可查阅Playwright代理文档。

可选的storage_state文件用于复用准备阶段的Cookie和本地存储。可在准备会话中设置配送位置并保存状态,然后在采集时再次验证页面显示的标签。文件可能包含Cookie和请求头,必须排除出版本库和共享日志;相关行为见Playwright浏览器状态文档。

将下面代码保存为amazon_price_snapshot.py。它只执行一次顶层导航,但Chromium仍可能加载脚本、字体、图片和API等大量子资源,因此顶层导航次数不等于代理请求总数。

import os
import re
from datetime import datetime, timezone
from decimal import Decimal
from pathlib import Path
from urllib.parse import urlparse
import pandas as pd
from playwright.sync_api import Error as PlaywrightError, TimeoutError as PlaywrightTimeoutError, sync_playwright

EXPORT_COLUMNS = [
    "requested_url", "final_url", "asin", "title", "marketplace",
    "variation", "variation_status", "seller", "raw_price", "numeric_price",
    "currency_symbol", "currency_code", "expected_currency", "delivery_location",
    "delivery_status", "offer_container", "captured_at_utc", "http_status",
    "top_level_navigations", "browser_request_count", "validation_status",
    "missing_reason",
]
STRICT_USD_DISPLAY = re.compile(r"^\$(?P<whole>(?:0|[1-9]\d{0,2}(?:,\d{3})*))\.(?P<fraction>\d{2})$")

class CollectionError(RuntimeError):
    def __init__(self, stage, message, status=None, retry_after=None):
        super().__init__(message); self.stage = stage
        self.status = status; self.retry_after = retry_after

def text(value):
    return " ".join((value or "").split())

def host(url):
    return (urlparse(url).hostname or "").lower().removeprefix("www.")

def page_type(page):
    if page.locator("input#captchacharacters, form[action*='validateCaptcha'], img[src*='captcha']").count():
        return "captcha"
    if "/ap/signin" in page.url.lower() or page.locator("form[name='signIn'], input#ap_email").count():
        return "login_wall"
    if page.locator("#productTitle").count():
        return "product"
    return "unknown"

def parse_usd(raw):
    value = text(raw); match = STRICT_USD_DISPLAY.fullmatch(value)
    if not match:
        raise CollectionError("price_format", "价格不是明确的美国美元格式")
    amount = match.group("whole").replace(",", "")
    return str(Decimal(f"{amount}.{match.group('fraction')}"))

def visible(scope, selectors):
    for selector in selectors:
        loc = scope.locator(selector).first
        if loc.count() and loc.is_visible():
            value = text(loc.inner_text())
            if value: return value
    return None

def collect_price(url, expected_asin, expected_marketplace):
    proxy = {"server": os.environ["ROLA_HTTP_PROXY_SERVER"],
             "username": os.environ["ROLA_PROXY_USERNAME"],
             "password": os.environ["ROLA_PROXY_PASSWORD"]}
    expected_currency = os.environ.get("EXPECTED_CURRENCY", "").upper()
    expected_delivery = text(os.environ.get("EXPECTED_DELIVERY_LABEL", ""))
    state = os.environ.get("PLAYWRIGHT_STORAGE_STATE")
    options = {"locale": "en-US"}
    if state:
        if not Path(state).is_file(): raise CollectionError("context_setup", "找不到storage state文件")
        options["storage_state"] = state
    stage = "browser_launch"
    with sync_playwright() as pw:
        browser = pw.chromium.launch(headless=True, proxy=proxy)
        context = browser.new_context(**options); page = context.new_page()
        requests = {"count": 0}; page.on("request", lambda _: requests.__setitem__("count", requests["count"] + 1))
        try:
            stage = "navigation"
            response = page.goto(url, wait_until="domcontentloaded", timeout=30000)
            if response is None: raise CollectionError("navigation", "没有返回HTTP响应")
            status = response.status; final_url = page.url
            if not 200 <= status < 300: raise CollectionError("http_response", f"目标返回HTTP {status}", status, response.headers.get("retry-after"))
            stage = "page_identity"
            page.wait_for_timeout(1500)
            if page_type(page) != "product": raise CollectionError("page_identity", f"收到{page_type(page)}而不是商品页", status)
            if host(final_url) != expected_marketplace: raise CollectionError("marketplace", "最终站点不匹配", status)
            asin = (page.locator("input#ASIN").get_attribute("value") or "").upper()
            if asin != expected_asin.upper(): raise CollectionError("product_identity", "ASIN不匹配", status)
            stage = "offer_validation"
            offer = page.locator("#apex_desktop, #buybox, #rightCol").filter(has=page.locator(".a-price")).first
            raw = visible(offer, [".a-price:not(.a-text-price) .a-offscreen"])
            if not raw: raise CollectionError("offer_validation", "没有找到唯一活动报价")
            currency = visible(offer, ["[itemprop='priceCurrency']"])
            seller = visible(offer, ["#sellerProfileTriggerId", "#merchant-info"])
            delivery = visible(page, ["#glow-ingress-line2"])
            reasons = []
            if currency != expected_currency: reasons.append("币种未独立确认或不匹配")
            if not seller: reasons.append("报价容器内没有卖家")
            if not state: reasons.append("未加载准备好的浏览器状态")
            if not expected_delivery: delivery_status = "undeclared"; reasons.append("未声明配送标签")
            elif not delivery: delivery_status = "missing"; reasons.append("未显示配送标签")
            elif expected_delivery.casefold() not in delivery.casefold(): delivery_status = "mismatch"; reasons.append("配送标签不匹配")
            else: delivery_status = "observed_match"
            record = {"requested_url": url, "final_url": final_url, "asin": asin,
                "title": text(page.locator("#productTitle").inner_text()), "marketplace": host(final_url),
                "variation": None, "variation_status": "manual_review", "seller": seller,
                "raw_price": raw, "numeric_price": parse_usd(raw), "currency_symbol": "$",
                "currency_code": currency, "expected_currency": expected_currency or None,
                "delivery_location": delivery, "delivery_status": delivery_status,
                "offer_container": "#apex_desktop/#buybox/#rightCol", "captured_at_utc": datetime.now(timezone.utc).isoformat(),
                "http_status": status, "top_level_navigations": 1, "browser_request_count": requests["count"],
                "validation_status": "candidate_context_complete" if not reasons else "candidate_manual_review",
                "missing_reason": "; ".join(reasons) or None}
            return record
        except (PlaywrightTimeoutError, PlaywrightError) as exc:
            message = str(exc).lower()
            if "407" in message or "proxy authentication" in message:
                stage = "proxy_authentication"
            elif "dns" in message or "name_not_resolved" in message:
                stage = "dns_resolution"
            elif "tls" in message or "ssl" in message or "certificate" in message:
                stage = "tls_handshake"
            elif "connection refused" in message or "connection reset" in message:
                stage = "connection"
            elif isinstance(exc, PlaywrightTimeoutError):
                stage = f"{stage}_timeout"
            raise CollectionError(stage, "browser operation failed") from exc
        finally:
            context.close(); browser.close()

if __name__ == "__main__":
    try:
        record = collect_price(os.environ["AMAZON_PRODUCT_URL"], os.environ["EXPECTED_ASIN"], os.environ.get("EXPECTED_MARKETPLACE", "amazon.com"))
        if set(record) != set(EXPORT_COLUMNS): raise RuntimeError("record schema mismatch")
        frame = pd.DataFrame([record], columns=EXPORT_COLUMNS)
        stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
        base = Path(f"amazon-price-snapshot-{record['validation_status']}-{stamp}")
        frame.to_csv(base.with_suffix(".csv"), index=False); frame.to_excel(base.with_suffix(".xlsx"), index=False)
        print(f"已保存{base}.csv和{base}.xlsx")
    except CollectionError as exc:
        print(f"采集停止:{exc};stage={exc.stage} status={exc.status} retry_after={exc.retry_after}")
        raise SystemExit(2)

代码故意只接受完整的$19.99或$1,299.00,拒绝C$19.99、US$19.99、价格区间、缺少符号和格式不完整的数值。美元符号单独保存,不能作为“币种为USD”的唯一证据。若自动检查通过,记录状态是候选上下文完成,仍需检查渲染后的报价和保留证据。

运行代码:

python amazon_price_snapshot.py

如果必须使用特定配送位置,可先在本地浏览器设置配送地址并保存状态:

python -m playwright codegen --save-storage=amazon-context.json https://www.amazon.com/

只有授权流程需要账户时才登录。状态文件可能含Cookie和本地存储,不要提交或分享。

采集结果状态

状态 含义 下一步
candidate_manual_review 已捕获商品身份和一个候选价格,但一个或多个上下文检查未完成 查看渲染页面和缺失原因
candidate_context_complete 自动上下文检查全部通过,但仍只是候选报价 检查同一次运行的截图或脱敏DOM
complete_comparable_record 人工确认当前报价、卖家、币种、变体和配送上下文 才能加入比较集,并保存审计证据
退出码2 导航、HTTP、页面身份、报价、格式或浏览器阶段失败 根据stage排查,不生成价格记录

英文原稿中的离线检查于2026年9月28日完成,使用macOS26.3.1和Python3.9.6,接受3个明确价格字符串、拒绝7个含糊或格式错误字符串;该检查没有测试Playwright、Chromium、pandas、openpyxl、Rola端点或亚马逊在线响应。因此发布前应在受控环境运行,并记录操作系统、Python、浏览器、库版本、出口地区和脱敏输出。

将亚马逊价格抓取到Excel

脚本会把同一条分类观察同时写入CSV和XLSX。构建DataFrame前先比较实际字段和EXPORT_COLUMNS,由列清单固定顺序;带时间戳的文件名可以保留历史快照。pandas通常通过openpyxl写入.xlsx。

amazon-price-scraping-data-table

如果之后需要换算币种,保留原始金额和原始币种,并新增换算金额、汇率来源和汇率日期。直接覆盖原价格会让后续审计无法还原页面观察值。

一条价格快照应包含哪些字段?

字段 回答的问题 验证要求
requested_url、final_url 浏览器是否到达预期页面? 拒绝意外站点和异常跳转
asin 是否为目标商品? 与任务输入匹配
marketplace 哪个亚马逊站点返回报价? 记录最终主机名
variation、variation_status 选择了哪个尺寸、颜色或样式? 区分无变体、已观察和未解析
seller 价格属于哪个卖家? 必须来自同一报价容器
raw_price 页面显示了什么文本? 保留符号和格式
numeric_price、currency_symbol、currency_code 数值能否安全比较? 不能只凭货币符号判断币种
delivery_location、delivery_status 哪个配送条件影响报价? 与同一浏览器状态中的页面标签比较
captured_at_utc 何时观察到? 使用带时区时间戳
http_status、validation_status 响应和页面是否通过检查? 将候选记录与完整记录分开
missing_reason 为什么字段为空? 缺失不等于零

公开日志不得包含代理密码、完整Cookie、账户标识或个人配送地址。敏感运行信息应保存在受访问控制的运营记录中。

哪些亚马逊抓取失败可以用代理诊断?

排错应先定位故障层。代理可以帮助测试网络路由,但不能修复账户限制、目标挑战页或错误选择器。

症状 要收集的证据 处理方式 重新测试什么
代理返回407 Proxy Authentication Required 端点、协议、用户名格式、客户端配置 修正代理认证;这不是亚马逊账户错误 先对授权的IP检测端点测试路由
目标返回非2xx 状态、最终URL、Retry-After、时间和请求模式 停止解析,检查权限和负载 根因处理后再进行有限测试
429 Too Many Requests Retry-After、并发、请求数、重试 停止批次并等待允许的重试窗口 更小样本、更低并发恢复
503 响应正文、页面类型、时间和其他请求结果 区分临时服务响应与挑战文档 仅在合适时做一次有限恢复
超时或连接失败 DNS、代理连通性、TLS阶段、目标耗时 单独测试路由,记录连接停止位置 保持同一输入后再测
HTTP200但价格错误或缺失 ASIN、变体、同容器卖家、币种、配送、选择器 拒绝或标为候选,不得把缺失保存为零 检查渲染报价并修正字段模型
账户限制或验证通知 账户提示、最终URL、官方支持指引 通过平台账户流程处理 网络路由不能移除账户限制

一次只改变一个变量。若同时更换IP、Cookie、站点、选择器和ZIP码,即使重试成功,也无法知道是哪项变化起作用。

2026年如何抓取亚马逊数据而不被封锁?

不存在通用的“永不被封锁”设置。2026年的可维护做法是减少无价值流量、在错误出现时停止、保留可审计证据,并使用符合项目条款的数据源。

  1. 从小样本和人工核验开始。 先采集少量ASIN,确认站点、变体、卖家、币种和配送上下文,再扩大范围。
  2. 控制并发和重试。 设置有边界的并发、延迟和失败率阈值;遇到403、429、503、验证码或意外页面立即停批,不用无限重试。
  3. 保持相关步骤的会话一致。 同一商品的变体和报价使用粘性会话;互不相关的独立样本是否轮换,应以来源条款和授权工作负载为前提。
  4. 不要把浏览器请求当成一次请求。 浏览器会加载脚本、字体、图片和后台API。扩大规模前核对计费单位、最低购买量、流量有效期、失败请求和重试是否计费,以及超预算停止条件。产品数值以当前Rola资料为准,不要复制旧文章数据。
  5. 先判断刷新频率。 如果业务每天只做一次决策,通常不需要每分钟抓取;降低无必要的频率有助于控制成本和故障面。
  6. 校验出口地区与站点。 需要特定市场时,同时固定Amazon域名、配送设置、币种、Cookie和代理出口;单独改变IP不能证明价格变化来自地区。

如果授权流程需要特定出口或会话行为,可以查看Rola IP亚马逊代理解决方案,然后在自己的环境中先验证小样本。应同时评估出口地区、报价准确性、失败率和流量成本。Rola IP提供路由,授权、解析器维护、响应校验和数据治理仍由使用方负责。

结论:可靠的亚马逊价格抓取依赖上下文验证

验证码页、错误站点、不同卖家、错误变体和过期价格,都可能在“导航成功”的情况下看起来有效。抓取亚马逊价格时,应先确认数据源和许可,再用小样本验证页面和报价,把原始文本、币种、卖家、配送和时间一起保存;只有在同一检查逻辑稳定运行后再扩大规模。

常见问题