Вернуться в блог

Скрапинг цен на Amazon: сбор на Python, проверка данных и выгрузка в Excel

Adrian Cole

29 сент. 2026 г. · Сценарии использования · 17 мин. чтения

Скрапинг цен на Amazon — это автоматизированное извлечение ценовых предложений для мониторинга и анализа рынка. Полезная запись содержит не только сумму, но и ASIN, региональный сайт Amazon, продавца, вариант товара, условия доставки и время наблюдения. Без этого две одинаковые на вид цены могут относиться к разным предложениям.

Для русскоязычной команды задача часто начинается с зарубежного каталога: например, нужно отслеживать предложения Amazon.com и анализировать их в Excel. Язык отчёта при этом не определяет валюту товара. Цену в долларах нельзя записать как рублёвую, а адрес доставки в США — считать подтверждением доступности товара для российского покупателя.

Кратко: сначала выберите источник с подходящими условиями доступа и использования. Зафиксируйте сайт Amazon, ASIN, валюту и контекст доставки. Проверяйте HTTP-ответ и тип страницы до извлечения цены. Сохраняйте неполные результаты как кандидаты на проверку, а не как подтверждённые цены. Rola IP отвечает за сетевой маршрут и параметры прокси-сессии; корректность предложения проверяется отдельно.

Почему сбор цен с Amazon даёт ошибки даже без сбоя скрипта

Слишком много запросов за короткое время

Цикл, который отправляет сотни запросов к карточкам товаров с одного IP за короткий промежуток времени, создаёт концентрированную нагрузку. Ответы могут замедлиться, смениться ошибкой или страницей проверки. Универсального числа запросов, которое Amazon гарантированно принимает, нет.

Начинайте с небольшого набора ASIN и нагрузки, допустимой для выбранного источника. Учитывайте не только число открытых карточек: браузер дополнительно загружает изображения, скрипты, шрифты и фоновые запросы. Одна навигация не равна одному обращению через прокси.

Вместо карточки возвращается CAPTCHA

HTTP 200 означает успешный возврат документа, но не подтверждает, что этот документ содержит нужный товар. Страница CAPTCHA, входа или уведомление об ограничении могут попасть в тот же обработчик, что и карточка.

amazon-captcha-challenge-page

Перед извлечением данных проверяйте заголовок товара, ASIN, итоговый URL, ценовой блок и признаки проверки пользователя. При CAPTCHA, 403, 429 или требовании входа текущий сбор следует остановить и разобраться в условиях доступа. Сервис решения CAPTCHA не подтверждает право на автоматизированный сбор и не исправляет неподходящий источник данных.

В руководстве по диагностике CAPTCHA Amazon (на английском) можно найти дополнительные пояснения. Используйте его для распознавания ответа и проверки причин, а не как обещание убрать проверку заменой IP.

Сетевое местоположение не совпадает с условиями предложения

Выходной IP — лишь один параметр запроса. На ответ также могут влиять cookies, состояние аккаунта, поведение браузера, частота обращений и выбранный адрес доставки. Полная модель проверки посетителей Amazon не опубликована: нельзя надёжно объяснить каждый отказ конкретным внутренним рейтингом IP.

Для исследования американского каталога фиксируйте Amazon.com, валюту USD, индекс доставки, состояние аккаунта и cookies. Если условия исследования требуют выхода из сети в США, выберите и проверьте соответствующий выходной IP. Сам по себе американский IP не определяет продавца, цену или доступность доставки.

Русскоязычная аудитория не означает, что сбору нужен российский выходной IP. Регион выбирают по условиям исследования и разрешённому сценарию, а не по языку отчёта. Прокси не меняет требования к аккаунту, оплате или доставке.

Парсер находит не ту цену

В карточке могут одновременно находиться текущая и справочная цена, купон, стоимость подписки, цена за единицу, доставка, другие продавцы и рекомендации. Часть блоков обновляется JavaScript, а разметка со временем меняется.

amazon-product-page-price-example

При анализе основного предложения, или Buy Box, сохраняйте его продавца рядом с ценой. Продавец и отправитель — разные сущности. Для исследования скидок текущую и справочную цену записывайте в отдельные поля. Первый найденный знак доллара не доказывает, что сумма относится к выбранному товару.

С чего начать: API, сервис данных или собственный парсер

Выбор источника важнее выбора библиотеки. Если официальная интеграция или партнёрский API предоставляет необходимые поля и разрешает ваш сценарий, этот путь обычно требует меньше обслуживания, чем поддержка HTML-селекторов.

Источник Когда подходит Что проверить до запуска
Официальный или партнёрский API Нужные поля доступны для разрешённого использования Требования к участникам, регион, лицензия, правила хранения и обновления цен
Сервис готовых данных Нужен регулярный мониторинг нескольких рынков Происхождение данных, полнота, задержка обновления, право использования и стоимость
HTTP-клиент, например Requests Разрешённый источник возвращает достаточный HTML Тип ответа, доступность полей без JavaScript, ограничения нагрузки
Playwright Цена зависит от JavaScript или состояния браузера Расход памяти и трафика, контекст сессии, изменения DOM, условия остановки

Например, руководство Amazon Creators API — на английском относится к определённым интеграциям, а не ко всем проектам ценового мониторинга. Проверьте соответствие вашей задачи условиям программы; не предполагайте, что доступ к API автоматически разрешает любой способ хранения или дальнейшего использования цен.

Для Amazon.com ориентиром служат условия использования Amazon.com и договоры, связанные с аккаунтом или API. У другого регионального сайта могут быть иные условия. Публичная видимость страницы сама по себе не решает вопрос автоматизации и повторного использования данных.

Если сбор затрагивает личные данные, требует аккаунта, противоречит условиям источника или постоянно возвращает проверки, остановитесь и пересмотрите способ получения данных. Для коммерческого проекта зафиксируйте источник и допустимый объём использования до регулярного запуска.

Где в этой схеме нужен Rola IP

Rola IP работает на сетевом уровне. Прокси для мониторинга цен помогают организовать маршрут для повторных наблюдений. На странице сценария, доступной на английском, можно сопоставить задачу мониторинга с требованиями к подключению; настройка браузерного клиента рассматривается ниже.

rola-ip-rotating-residential-proxy-setup

Что выбрать: ротацию или закреплённую сессию?

Для связанных действий удобнее сохранять один выходной IP и контекст браузера. Для независимых наблюдений можно рассматривать ротацию, если она соответствует условиям источника и политике запросов. Ротация не должна служить способом продолжить сбор после явного отказа в доступе.

Задача Подход к сессии Зачем это нужно
Независимые наблюдения по разным ASIN Допустимая для сценария ротация Каждое наблюдение оценивается отдельно
Товар, его варианты и предложения продавцов Закреплённая, или sticky-сессия Сохранить IP, cookies и контекст доставки для связанных шагов
Длительная работа с одним сетевым адресом Статический резидентный или ISP-прокси при подходящих условиях Уменьшить изменения сетевого контекста
Воспроизведение ошибки Сначала те же параметры, затем одно изменение за раз Отделить сетевую причину от ошибки страницы или парсера
Локальные HTML-образцы и разрешённые тестовые ресурсы В том числе датацентровый маршрут Проверить подключение без предположений о поведении Amazon

Резидентный адрес, статический IP, выделенный IP и sticky-сессия — не синонимы. Ротационный продукт тоже может поддерживать удержание адреса в течение заданного периода. Подробнее о выборе подхода — в сравнении sticky-сессий и ротации IP (на английском).

На странице резидентных прокси Rola IP уточняйте параметры конкретного продукта: страну выхода, способ авторизации, длительность сессии и протокол. Большой общий пул адресов не равен гарантии доступа к отдельной карточке.

Что проверяет прокси, а что остаётся за парсером?

Инструмент проверки прокси с англоязычным интерфейсом позволяет оценить только те свойства, которые он показывает: например, подключение или выходной адрес. Это не проверка цены Amazon.

В клиенте сбора отдельно проверьте итоговый URL, сайт, ASIN, продавца, валюту и доставку. Если селектор сломан, открылась другая площадка или пришла CAPTCHA, смена IP не исправляет запись. Связность сети и пригодность данных — два разных критерия.

Сбор цен с Amazon на Python: пример с Playwright

Playwright управляет браузерным движком и позволяет дождаться содержимого, которое появляется после выполнения JavaScript. Это полезно, когда Requests возвращает неполный HTML. Условия доступа при этом остаются прежними.

Шаблон ниже выполняет одну верхнеуровневую навигацию к разрешённой карточке Amazon.com, извлекает ценовой кандидат и сохраняет его в CSV и XLSX. Он не обходит CAPTCHA, не перебирает IP после отказа и не запускает бесконечные повторы.

amazon-product-page-automated-browser

1. Установите зависимости в отдельное окружение

Понадобятся Python 3.10 или новее, Playwright, pandas, openpyxl и браузер Chromium. Также нужны разрешённый товарный URL, ожидаемый ASIN, валюта, параметры доставки и актуальные реквизиты HTTP-прокси Rola IP.

python -m venv .venv

В PowerShell активируйте окружение командой .\.venv\Scripts\Activate.ps1, в macOS или Linux — source .venv/bin/activate. Если политика PowerShell не разрешает активацию, можно вызывать .\.venv\Scripts\python.exe вместо python, не меняя политику выполнения.

python -m pip install playwright pandas openpyxl
python -m playwright install chromium
python --version
python -m playwright --version
python -m pip show pandas openpyxl

Запишите фактические версии и ОС. Установка пакетов не доказывает совместимость селекторов с текущей карточкой Amazon.

2. Задайте параметры подключения и наблюдения

Схема подключения браузера описана в руководстве по настройке прокси в Playwright (на английском). Сверьте параметры сервера и авторизации со своим подключением Rola IP, затем передайте их через переменные окружения.

Пример для PowerShell:

$env:ROLA_HTTP_PROXY_SERVER="http://CURRENT_ROLA_HOST:PORT"
$env:ROLA_PROXY_USERNAME="YOUR_USERNAME"
$env:ROLA_PROXY_PASSWORD="YOUR_PASSWORD"
$env:AMAZON_PRODUCT_URL="https://www.amazon.com/dp/B0XXXXXXXXX"
$env:EXPECTED_ASIN="B0XXXXXXXXX"
$env:EXPECTED_MARKETPLACE="amazon.com"
$env:EXPECTED_CURRENCY="USD"
$env:EXPECTED_DELIVERY_LABEL="10001"
$env:PLAYWRIGHT_STORAGE_STATE="amazon-context.json"

Замените заглушки реквизитами своего подключения и реальным разрешённым ASIN. 10001 — пример, а не обязательный индекс: значение должно соответствовать исследованию и видимой метке доставки. В bash используйте те же имена через export, например export EXPECTED_CURRENCY="USD".

Не сохраняйте пароль в репозитории, общем скрипте или публичном журнале. В рабочей системе передавайте секреты через принятый в организации механизм управления учётными данными.

В примере используется HTTP-прокси с логином и паролем. Документация Playwright по сетевым настройкам описывает HTTP(S)-авторизацию и отдельно упоминает SOCKSv5. Это не доказывает совместимость конкретного SOCKS5-сервера с авторизацией и выбранной сборки Chromium. Не заменяйте схему http:// на socks5:// без проверки этой комбинации.

3. Подготовьте состояние браузера с нужной доставкой

До запуска сбора откройте отдельную разрешённую подготовительную сессию:

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

Установите нужную доставку и закройте браузер, чтобы сохранить состояние. Не входите в аккаунт, если разрешённая задача этого не требует. Подготовительный сеанс сам создаёт сетевые запросы; они не входят в счётчик основного скрипта.

Файл состояния может содержать cookies и локальное хранилище с чувствительными данными. Не публикуйте его и ограничьте доступ. Загрузка файла не доказывает, что доставка восстановилась: видимую метку нужно проверить повторно при сборе. Поведение описано в руководстве по состоянию браузера и документации codegen.

4. Сохраните полный пример кода

Создайте файл amazon_price_snapshot_ru.py. Имена колонок оставлены стабильными для обработки данных, а сообщения и пояснения даны по-русски. Пример ограничен Amazon.com и USD; для Amazon.de или другой площадки потребуется отдельная проверка формата цены и контекста, а не только замена домена.

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 normalized_host(url: str) -> str:
    host = (urlparse(url).hostname or "").lower()
    return host.removeprefix("www.")


def normalized_text(value: str) -> str:
    return " ".join(value.split())


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


def wait_for_page_outcome(page):
    page.wait_for_function(
        """() => Boolean(
            document.querySelector('#productTitle') ||
            document.querySelector('input#captchacharacters') ||
            document.querySelector("form[action*='validateCaptcha']") ||
            document.querySelector("form[name='signIn']") ||
            document.querySelector('input#ap_email')
        )""",
        timeout=12_000,
    )


def detect_page_type(page, final_url: str) -> str:
    lower_url = final_url.lower()
    captcha = page.locator(
        "input#captchacharacters, form[action*='validateCaptcha'], img[src*='captcha']"
    )
    login = page.locator("form[name='signIn'], input#ap_email")

    if "validatecaptcha" in lower_url or captcha.count():
        return "captcha"
    if "/ap/signin" in lower_url or login.count():
        return "login_wall"
    if page.locator("#productTitle").count():
        return "product"
    return "unknown"


def extract_asin(page, final_url: str):
    asin_input = page.locator("input#ASIN").first
    if asin_input.count():
        value = asin_input.get_attribute("value")
        if value:
            return value.upper()

    match = re.search(r"/(?:dp|gp/product)/([A-Z0-9]{10})", final_url, re.I)
    return match.group(1).upper() if match else None


def parse_strict_usd_display(raw_price: str) -> str:
    value = normalized_text(raw_price)
    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 extract_currency_code(offer_container):
    marker = offer_container.locator("[itemprop='priceCurrency']").first
    if not marker.count():
        return None
    value = marker.get_attribute("content") or marker.text_content() or ""
    value = normalized_text(value).upper()
    return value if re.fullmatch(r"[A-Z]{3}", value) else None


def extract_offer_candidate(page):
    container_selectors = ("#apex_desktop", "#buybox", "#rightCol")

    page.wait_for_function(
        """() => Boolean(
            document.querySelector('#apex_desktop .a-price') ||
            document.querySelector('#buybox .a-price') ||
            document.querySelector('#rightCol .a-price') ||
            document.querySelector('input#captchacharacters') ||
            document.querySelector("form[name='signIn']")
        )""",
        timeout=10_000,
    )

    page_type = detect_page_type(page, page.url)
    if page_type != "product":
        raise CollectionError(
            "offer_wait",
            f"Во время ожидания предложения тип страницы изменился: {page_type}",
        )

    for container_selector in container_selectors:
        container = page.locator(container_selector).first
        if not container.count() or not container.is_visible():
            continue

        nodes = container.locator(
            ".a-price:not(.a-text-price) .a-offscreen"
        )
        observed = []
        for index in range(nodes.count()):
            node = nodes.nth(index)
            price_box = node.locator("xpath=..")
            if price_box.count() and price_box.is_visible():
                text = normalized_text(node.text_content() or "")
                if text and text not in observed:
                    observed.append(text)

        if len(observed) > 1:
            raise CollectionError(
                "offer_ambiguity",
                f"Найдено несколько ценовых кандидатов в {container_selector}",
            )
        if len(observed) == 1:
            return container, observed[0], container_selector

    raise CollectionError(
        "offer_validation",
        "В контейнерах предложения не найден единственный ценовой кандидат",
    )


def extract_variation_state(page):
    controls = page.locator("[id^='variation_']")
    selections = page.locator("[id^='variation_'] .selection")

    if controls.count() == 0:
        return None, "not_applicable"

    values = []
    for index in range(selections.count()):
        text = normalized_text(selections.nth(index).inner_text())
        if text and text not in values:
            values.append(text)

    if not values:
        return None, "unresolved"
    return " | ".join(values), "observed"


def classify_browser_error(exc, stage):
    message = str(exc).lower()
    if "407" in message or "proxy authentication" in message:
        return "proxy_authentication"
    if "name_not_resolved" in message or "dns" in message:
        return "dns_resolution"
    if "certificate" in message or "ssl" in message or "tls" in message:
        return "tls_handshake"
    if "connection refused" in message or "connection reset" in message:
        return "connection"
    if isinstance(exc, PlaywrightTimeoutError):
        return f"{stage}_timeout"
    return stage


def collect_price(url: str, expected_asin: str, expected_marketplace: str) -> dict:
    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 = normalized_text(
        os.environ.get("EXPECTED_DELIVERY_LABEL", "")
    )
    storage_state = os.environ.get("PLAYWRIGHT_STORAGE_STATE")
    state_loaded = False

    context_options = {"locale": "en-US"}
    if storage_state:
        storage_path = Path(storage_state)
        if not storage_path.is_file():
            raise CollectionError(
                "context_setup",
                "Файл PLAYWRIGHT_STORAGE_STATE не найден",
            )
        context_options["storage_state"] = str(storage_path)
        state_loaded = True

    with sync_playwright() as playwright:
        browser = None
        context = None
        stage = "browser_launch"
        try:
            browser = playwright.chromium.launch(headless=True, proxy=proxy)
            stage = "context_setup"
            context = browser.new_context(**context_options)
            page = context.new_page()
            network = {"requests": 0}
            page.on(
                "request",
                lambda _request: network.__setitem__(
                    "requests", network["requests"] + 1
                ),
            )

            stage = "navigation"
            response = page.goto(
                url,
                wait_until="domcontentloaded",
                timeout=30_000,
            )
            if response is None:
                raise CollectionError("navigation", "HTTP-ответ не получен")

            status = response.status
            retry_after = response.headers.get("retry-after")
            final_url = page.url

            if status < 200 or status >= 300:
                raise CollectionError(
                    "http_response",
                    f"Сайт вернул HTTP {status}",
                    status,
                    retry_after,
                )

            stage = "page_identity"
            wait_for_page_outcome(page)
            page_type = detect_page_type(page, page.url)
            if page_type != "product":
                raise CollectionError(
                    "page_identity",
                    f"Ожидалась карточка товара, получено {page_type}",
                    status,
                    retry_after,
                )

            final_url = page.url
            marketplace = normalized_host(final_url)
            if marketplace != expected_marketplace:
                raise CollectionError(
                    "marketplace",
                    f"Другой сайт: ожидался {expected_marketplace}, получен {marketplace}",
                    status,
                )

            observed_asin = extract_asin(page, final_url)
            if observed_asin != expected_asin.upper():
                raise CollectionError(
                    "product_identity",
                    f"Другой ASIN: ожидался {expected_asin}, получен {observed_asin}",
                    status,
                )

            title = normalized_text(
                page.locator("#productTitle").inner_text(timeout=10_000)
            )

            stage = "offer_wait"
            offer, raw_price, offer_selector = extract_offer_candidate(page)
            numeric_price = parse_strict_usd_display(raw_price)
            currency_code = extract_currency_code(offer)
            seller, _ = first_visible_text(
                offer, ["#sellerProfileTriggerId", "#merchant-info"]
            )
            delivery, _ = first_visible_text(page, ["#glow-ingress-line2"])
            variation, variation_status = extract_variation_state(page)

            reasons = []
            if not expected_currency:
                reasons.append("Ожидаемая валюта не задана")
            elif currency_code != expected_currency:
                reasons.append(
                    "Независимый ISO-код валюты отсутствует или не совпадает"
                )
            if not seller:
                reasons.append("Продавец не найден в выбранном контейнере предложения")
            if variation_status == "unresolved":
                reasons.append("Элементы выбора варианта найдены, но выбор не определён")
            if not state_loaded:
                reasons.append("Подготовленное состояние браузера не загружено")
            if not expected_delivery:
                reasons.append("Ожидаемая метка доставки не задана")
                delivery_status = "undeclared"
            elif not delivery:
                reasons.append("Метка доставки не найдена")
                delivery_status = "missing"
            elif expected_delivery.casefold() not in delivery.casefold():
                reasons.append("Метка доставки не соответствует ожидаемому контексту")
                delivery_status = "mismatch"
            else:
                delivery_status = "observed_match"

            validation_status = (
                "candidate_context_complete"
                if not reasons
                else "candidate_manual_review"
            )

            return {
                "requested_url": url,
                "final_url": final_url,
                "asin": observed_asin,
                "title": title,
                "marketplace": marketplace,
                "variation": variation,
                "variation_status": variation_status,
                "seller": seller,
                "raw_price": raw_price,
                "numeric_price": numeric_price,
                "currency_symbol": "$",
                "currency_code": currency_code,
                "expected_currency": expected_currency or None,
                "delivery_location": delivery,
                "delivery_status": delivery_status,
                "offer_container": offer_selector,
                "captured_at_utc": datetime.now(timezone.utc).isoformat(),
                "http_status": status,
                "top_level_navigations": 1,
                "browser_request_count": network["requests"],
                "validation_status": validation_status,
                "missing_reason": "; ".join(reasons) or None,
            }
        except CollectionError:
            raise
        except (PlaywrightTimeoutError, PlaywrightError) as exc:
            category = classify_browser_error(exc, stage)
            raise CollectionError(category, "Операция браузера завершилась ошибкой") from exc
        finally:
            if context is not None:
                context.close()
            if browser is not None:
                browser.close()


def main():
    required = (
        "ROLA_HTTP_PROXY_SERVER", "ROLA_PROXY_USERNAME", "ROLA_PROXY_PASSWORD",
        "AMAZON_PRODUCT_URL", "EXPECTED_ASIN", "EXPECTED_CURRENCY",
    )
    if any(not os.environ.get(key) for key in required):
        raise CollectionError("configuration", "Не заданы обязательные переменные окружения")
    if os.environ["EXPECTED_CURRENCY"].upper() != "USD":
        raise CollectionError("configuration", "Этот пример рассчитан на USD")
    target = urlparse(os.environ["AMAZON_PRODUCT_URL"])
    if target.scheme != "https" or normalized_host(target.geturl()) != "amazon.com":
        raise CollectionError("configuration", "Укажите HTTPS-адрес товара Amazon.com")
    if target.username or target.password:
        raise CollectionError("configuration", "URL товара не должен содержать реквизиты")
    if os.environ.get("EXPECTED_MARKETPLACE", "amazon.com") != "amazon.com":
        raise CollectionError("configuration", "Этот пример рассчитан на Amazon.com")
    if not re.fullmatch(r"[A-Z0-9]{10}", os.environ["EXPECTED_ASIN"].upper()):
        raise CollectionError("configuration", "ASIN должен содержать 10 букв или цифр")
    captured = collect_price(
        os.environ["AMAZON_PRODUCT_URL"],
        os.environ["EXPECTED_ASIN"],
        os.environ.get("EXPECTED_MARKETPLACE", "amazon.com"),
    )

    expected_keys = set(EXPORT_COLUMNS)
    actual_keys = set(captured)
    if actual_keys != expected_keys:
        missing = sorted(expected_keys - actual_keys)
        extra = sorted(actual_keys - expected_keys)
        raise RuntimeError(f"Несовпадение схемы: отсутствуют={missing}, лишние={extra}")

    # Защищаем экспорт от формул в строках, полученных со страницы.
    def safe_cell(value):
        if isinstance(value, str):
            value = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", " ", value)
            if value.lstrip().startswith(("=", "+", "-", "@")):
                return "'" + value
        return value

    exported = {key: safe_cell(value) for key, value in captured.items()}
    frame = pd.DataFrame([exported], columns=EXPORT_COLUMNS)

    stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
    state = captured["validation_status"].replace("_", "-")
    base = Path(f"amazon-price-snapshot-{state}-{stamp}")
    frame.to_csv(base.with_suffix(".csv"), index=False, encoding="utf-8-sig", sep=";")
    frame.to_excel(base.with_suffix(".xlsx"), index=False, engine="openpyxl")
    print(f"Запись {state} сохранена: {base}.csv и {base}.xlsx")


if __name__ == "__main__":
    try:
        main()
    except CollectionError as exc:
        details = [f"stage={exc.stage}"]
        if exc.status is not None:
            details.append(f"status={exc.status}")
        if exc.retry_after:
            details.append(f"retry_after={exc.retry_after}")
        print(f"Сбор остановлен: {exc}. " + ", ".join(details))
        raise SystemExit(2)

Запуск из того же окружения:

python amazon_price_snapshot_ru.py

Как устроена проверка цены

Скрипт прекращает обработку при любом верхнеуровневом ответе вне диапазона 2xx. Затем в ограниченное время ожидает признаки товара, CAPTCHA или входа. Наличие слова «войти» в обычной навигации сайта не считается отдельной страницей авторизации: проверяются специальные элементы.

Цена ищется внутри ограниченного набора областей предложения. Если обнаружено несколько разных активных сумм, запись не создаётся. Если сумма одна, она всё равно остаётся кандидатом: широкий контейнер #rightCol не доказывает, что найденная сумма — именно текущая цена Buy Box.

Парсер принимает строки вида $19.99 и $1,299.00. Он отклоняет C$19.99, US$19.99, диапазоны, строку без символа и неоднозначные дробные части. Это намеренно узкий формат. Символ $ сохраняется отдельно от ISO-кода: он не доказывает, что перед нами USD. Если в том же блоке нет независимого кода валюты, нужна ручная проверка.

Проверки ASIN, продавца, вариантов и доставки тоже имеют границы. Один и тот же текст продавца может включать дополнительные сведения; отсутствие найденных контролов варианта может объясняться изменением DOM. Совпадение метки доставки по подстроке не подтверждает полный адрес. Сопоставьте результат с отображённым предложением, прежде чем использовать его для коммерческого решения.

Скрипт не делает снимок экрана и не сохраняет DOM автоматически. Если результат нужен для аудита, отдельно сохраните минимальный фрагмент или снимок из того же запуска, исключив персональные данные и секреты. Историческая иллюстрация из статьи не заменяет такое подтверждение.

Что означают статусы результата

Результат Значение Допустимый следующий шаг
candidate_manual_review Товар и одна цена-кандидат найдены, но контекст неполон Проверить missing_reason, предложение, валюту и доставку
candidate_context_complete Все предусмотренные автоматические проверки пройдены Сопоставить запись с предложением и доказательствами того же запуска
complete_comparable_record Ручная проверка подтвердила смысл цены и условия предложения Добавить в набор сравнения, сохранив подтверждающие материалы
Код завершения 2 Не пройдены проверки ответа, страницы, формата или подключения Разобрать этап ошибки; не создавать фиктивную цену

Скрипт сам не присваивает complete_comparable_record. Код завершения 0 означает, что файлы записаны, а не что данные готовы к автоматическому пересчёту цен. Ошибки установки зависимостей и записи файлов также требуют отдельной диагностики и могут завершать процесс с другим ненулевым кодом.

Выгрузка цен с Amazon в Excel: формат и работа с русской локалью

Скрипт сначала сверяет ключи записи со списком EXPORT_COLUMNS, затем создаёт DataFrame с фиксированным порядком колонок. Одна и та же классифицированная запись попадает в CSV и XLSX. В имени файла используется UTC с долями секунды, чтобы уменьшить вероятность перезаписи снимка.

amazon-price-scraping-data-table

Для XLSX явно используется openpyxl. CSV сохраняется с BOM в UTF-8 и разделителем ;: такой файл удобнее импортировать в ряде установок Excel с русскими региональными настройками. Если Excel не определил формат, используйте импорт «Из текста/CSV», задав кодировку и разделитель явно.

numeric_price остаётся десятичной строкой с точкой, чтобы не вносить ошибки двоичного округления и локали на этапе сохранения. Для расчётов преобразуйте копию колонки в число с явно выбранным форматом исходных данных, например английской локалью США в Power Query. Не заменяйте запятые и точки во всей таблице: запятая в $1,299.00 означает разряд, а не дробную часть.

Какие поля должны быть в ценовом снимке

Поле Что фиксирует Проверка
requested_url, final_url Запрошенную и фактическую страницу Не произошло ли перенаправление на другую площадку
asin, title Идентичность товара Совпадает ли ASIN с задачей; не выбран ли другой вариант
marketplace Домен магазина Не выводить его из языка интерфейса или IP
variation, variation_status Выбранные размер, цвет и другие параметры Различать наблюдаемый выбор, отсутствие контролов и неопределённое состояние
seller Продавца предложения Извлекать рядом с ценой и проверять, что это не только отправитель
raw_price Отображённую строку цены Сохранить валютный символ и исходное форматирование с учётом экранирования при экспорте
numeric_price, currency_symbol, currency_code Числовое представление и валюту Не считать знак $ независимым доказательством USD
delivery_location, delivery_status Наблюдаемый контекст доставки Сверять с подготовленной сессией, не публиковать личный адрес
offer_container Использованный контейнер DOM Помогает воспроизвести выбор цены при изменении разметки
captured_at_utc Время наблюдения Оставлять часовой пояс; локальное время добавлять отдельной колонкой
http_status, validation_status Состояние ответа и качества записи Успешный HTTP-ответ не равен подтверждённому предложению
missing_reason Причину неполноты Пустое значение не превращать в нулевую цену
browser_request_count Число запросов, замеченных в странице браузера Не считать его объёмом трафика или точной величиной биллинга

Текущий шаблон не извлекает налог, доставку, купон, подписочную скидку, наличие и итоговую сумму заказа. Эти поля добавляют отдельно, если они входят в задачу. Отсутствие цены не доказывает отсутствие товара в продаже.

Как добавить рублёвый отчёт и местное время

Если аналитика ведётся в рублях, сохраните исходную цену и USD, а рядом добавьте converted_price_rub, значение курса, источник и дату курса. Выбранный аналитический курс может отличаться от курса платёжного сервиса и не отражает комиссии. Такой пересчёт не подтверждает возможность покупки.

Пример исключительно для объяснения арифметики: при условной цене 20 USD и условном курсе 90 RUB/USD получается 1 800 RUB до любых дополнительных расходов. Это не фактическая цена Amazon и не текущий валютный курс.

Для команды в Москве можно добавить время Europe/Moscow, для команды в другом регионе — её рабочую зону. Россия охватывает несколько часовых поясов, поэтому московское время не следует автоматически назначать всем пользователям. UTC сохраняйте как исходную временную метку.

Какие ошибки помогает диагностировать прокси

Начинайте с уровня, на котором произошёл сбой. Прокси способен изменить и проверить маршрут, но не исправляет права аккаунта или смысл извлечённой суммы.

Симптом Какие данные собрать Что делать Что проверить повторно
HTTP-прокси вернул 407 Протокол, формат имени, endpoint, настройки клиента без пароля Исправить авторизацию прокси; это не ошибка аккаунта Amazon Разрешённый сервис определения IP до обращения к Amazon
Цель вернула 403 или другой не-2xx Статус, итоговый URL, время, тип страницы Остановить парсинг, проверить условия доступа и причину отказа Только после устранения причины; не подменять разрешение сменой IP
429 Retry-After, параллельность, число запросов и повторов Остановить пакет, учитывать указания сервера перед допустимым повтором Небольшую выборку с меньшей нагрузкой
503 Тело ответа, признаки проверки, время и другие ошибки Отличить временную недоступность от страницы проверки Ограниченное восстановление, только если оно уместно
Тайм-аут или ошибка соединения DNS, подключение к прокси, этап TLS, время ответа Проверить маршрут отдельно и зафиксировать этап сбоя Тот же URL при тех же исходных условиях
200, но цена неверна или отсутствует ASIN, вариант, продавец, валюта, доставка, селектор Отклонить результат или оставить кандидатом Отрисованное предложение и обновлённую модель полей
Ограничение аккаунта или проверка Уведомление, итоговый URL, официальные инструкции Решать вопрос через процедуру платформы Сетевой маршрут не снимает ограничение аккаунта

Названия этапов в сообщениях — диагностические категории, а не точное объяснение внутреннего решения Amazon. Браузеры не всегда возвращают ошибки прокси в одинаковом формате. Проверяйте реальные симптомы, не печатая полный текст исключения, если он может содержать реквизиты.

Если одновременно заменить IP, cookies, площадку, индекс и селектор, успешный повтор не покажет, какое изменение помогло. Для диагностики изменяйте по одному условию и сохраняйте результат сравнения.

Как снизить частоту блокировок при сборе данных с Amazon в 2026 году

Снизить число предотвратимых отказов можно за счёт подходящего источника, обоснованной нагрузки и стабильного контекста. Настройки, гарантирующей непрерывный доступ, нет. Год в заголовке не означает наличие нового подтверждённого способа обхода защиты Amazon.

  1. Начните с нескольких ASIN. Каждую запись сначала проверьте вручную: товар, продавца, валюту и доставку.
  2. Ограничьте параллельность и частоту обновления. Интервал должен соответствовать разрешённой нагрузке и задаче. Для ежедневного отчёта редко нужен поминутный обход всего каталога.
  3. Сохраняйте контекст связанных действий. Проверку товара и его вариантов выполняйте в одной подходящей сессии; независимые наблюдения не смешивайте с повтором после отказа.
  4. Останавливайте пакет при явном отказе. CAPTCHA, неожиданные страницы, 403, 429 и 503 сначала требуют разбора. Бесконечные повторы увеличивают расход трафика, а не достоверность данных.
  5. Определите пороги заранее. Установите допустимую долю ошибок, лимит трафика или расходов и условия остановки. Значения должны опираться на разрешённый сценарий и результаты небольшой проверки.

Учтите стоимость браузерного трафика

Playwright загружает больше ресурсов, чем одиночный HTML-запрос. До масштабирования уточните единицу тарификации, минимальную покупку, срок действия трафика, учёт неудачных запросов и повторов, округление и фиксированные платежи. Не переносите условия тарифа из старого скриншота в рабочий бюджет.

Для тарифа с оплатой за объём базовая оценка выглядит так:

Переменная часть расходов = фактически тарифицируемый объём в GB × цена за GB.

Это не вся сумма счёта, если есть минимальный пакет или дополнительные платежи. Счётчик запросов в примере не измеряет GB: для бюджета используйте фактический объём и данные учёта провайдера. Подготовительные действия, проверки подключения и повторные попытки тоже могут расходовать трафик.

Когда требования к стране выхода и сессии определены, изучите решение Rola IP для Amazon . Сначала сравните выходной IP, корректность предложения, долю отказов и расход трафика на небольшой выборке. Увеличивайте объём только после проверки этих показателей. Rola IP предоставляет маршрут; ваша команда отвечает за разрешение на сбор, сопровождение парсера и управление данными.

Итоги

Надёжный скрапинг цен на Amazon начинается с отклонения неподходящих ответов до их попадания в таблицу. CAPTCHA, другой ASIN, иной продавец и неверная валюта могут остаться незамеченными, если проверять только завершение навигации.

Для русскоязычной аналитики сохраняйте валюту и условия исходного предложения, а рублёвый пересчёт и местное время добавляйте отдельно. Проверяйте небольшую выборку, храните объяснимые статусы и измеряйте расходы. Масштабирование имеет смысл после того, как проверки стабильно выполняются в вашем разрешённом сценарии.

Частые вопросы

Готовы начать сбор данных в большом масштабе?

Попробовать бесплатно