← Все статьи

Скрапинг цен в продуктовых сетях: какой магазин, какой покупатель?

Скрапинг цен в продуктовых сетях сбоит незаметно: потерянный store cookie возвращает реальную цену, но для другого магазина. Как подтвердить магазин, и почему одного профиля покупателя недостаточно.

Дюжина яиц Lucerne в одном магазине Safeway в Вашингтоне (округ Колумбия) предлагалась на Instacart по ценам $3.99, $4.28, $4.59, $4.69 и $4.79. Один магазин, один момент времени, разные покупатели. Какое из этих пяти чисел должно попасть в ваш ценовой дашборд?

Проблема

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

Начнем с магазина. В руководстве Scrapfly по сравнению цен на продукты галлон молока стоит $3.98 в одном Walmart и $4.29 в другом, расположенном в 20 милях. Там же предупреждают: сессия без геопривязки магазина получает дефолтные данные, которые могут не совпадать ни с одной реальной точкой. В руководстве по доставке продуктов 2026 года от ScrapeInsight описано то же самое на уровень глубже: $3.99 в одной зоне доставки, $4.49 в нескольких милях от нее. В кейсе Bright Data о ShopGrok показано, как австралийский ритейл перешел на ценообразование по почтовым индексам за почти четыре года работы компании.

Далее покупатель. В декабре 2025 года Groundwork Collaborative, Consumer Reports и More Perfect Union провели тесты с участием 437 покупателей в четырех городах, одновременно собирая одинаковые корзины Instacart в одних и тех же магазинах. 74% товаров отображались более чем по одной цене. Там, где цены различались, разрыв между минимальной и максимальной составлял в среднем 13%, а стоимость всей корзины отличалась примерно на 7%.

Сложите это вместе, и вы получите сбой, из-за которого продуктовые данные обходятся дорого. Потеряйте контекст магазина, и визуально ничего не сломается. Сайт не вернет ошибку. Он вернет корректную цену для магазина, который вы не запрашивали, с SKU, числом и таймстемпом, которые пройдут любые ваши проверки на null.

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

Валидация магазина внутри ответа

Большинство сборщиков отправляют параметры выбора магазина (cookie, query-параметр, header) и слепо им доверяют. Более надежный подход: требовать подтверждения от самого ответа. Если на странице указан магазин, для которого она была сгенерирована (например, store ID во встроенных данных или название точки в баннере самовывоза), этот маркер становится вашим критерием успешности. В FourA это решается одним вызовом Proxy Finder:

import requests

r = requests.post(
    "https://api.foura.ai/api/proxy",
    headers={"X-API-Key": "pk_live_..."},
    json={
        "maxTries": 6,
        "request": {
            "method": "GET",
            "url": "https://grocer.example/product/0001234",
            "headers": [["Cookie", "store=1234"]],
            "validate": {
                "status": {"accept": [200]},
                "data": {
                    "accept": ["\"storeId\":\"1234\""],
                    "fail": ["Just a moment", "Access Denied"]
                }
            }
        }
    }
).json()

if "error" in r:
    report = r.get("attemptReport", {})
    print(report.get("summary", r["error"]))
    if report.get("contentRejected", 0) > report.get("defense", 0) + report.get("noResponse", 0):
        print("the site answered for another store: check the store selection")
else:
    page, exit_id = r["data"], r["proxy"]

В этом request легко ошибиться в трех деталях.

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

Ваш собственный header Cookie меняет логику повторных попыток. Когда сайт отклоняет предложенный браузер, Proxy Finder обычно переключается на другое семейство браузеров при следующей попытке. Но не в том случае, когда прикреплен ваш cookie. Сессия привязана к сигнатуре, с которой она была получена, поэтому request с собственным cookie сохраняет исходный браузер. Если сайт сети магазинов чувствителен к браузеру, задайте его явно через профили браузера, а не полагайтесь на ротацию.

Неудачная задача сообщает о характере сбоя. Каждый неуспешный response от Proxy Finder содержит attemptReport. defense считает ответы, где была обнаружена проверка на бота, noResponse считает точки выхода, которые вообще не дошли до сайта, а contentRejected считает страницы, вернувшиеся с HTTP 200 без проверок на бота, но отклоненные исключительно вашим правилом по контенту. Для парсера продуктового ритейла последнее значение означает конкретную проблему: сайт ответил, но не для вашего магазина. Дополнительные точки выхода этого не исправят. Остальные счетчики описаны в руководстве по отчетам о попытках.

Фиксация профиля покупателя или измерение разброса

Данные Instacart добавляют вторую переменную, и есть два корректных способа работы с ней.

Для мониторинга цен ведите сбор по каждому магазину в рамках одного профиля. Повторно используйте успешную точку выхода (r["proxy"], opaque ID) через Single с тем же cookie и сохраняйте этот ID вместе с response header X-FourA-Request-Id для каждой строки. При скачке цены вы сможете отличить реальное изменение в магазине от смены сессии.

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

Результаты

Возьмем региональную сеть, которая ежедневно отслеживает 120 магазинов конкурентов по 2 500 SKU (наглядный сценарий на базе отраслевых показателей). Это 300 000 страниц в день, и в любую ночь часть ответов вернется не для того магазина: изменился формат cookie, закрылся магазин или сайт стал отдавать приоритет геопозиции по IP вместо cookie.

  • Страницы не того магазина отсекаются на этапе сбора. Они не попадают в строки данных, поэтому изменение цены в выборке означает изменение именно в этом магазине.
  • Сбои поступают уже отсортированными. Высокий показатель contentRejected уходит к тому, кто отвечает за выбор магазина, высокий defense означает проблему с доступом, высокий noResponse указывает на выходные узлы. Три ответственных, три решения, и никому не нужно тратить утро на угадывание причин.
  • Каждая строка отслеживаема. ID выходного узла и ID request для каждого наблюдения превращают вопрос "реален ли этот всплеск?" в простой поиск по логам.
  • Разброс цен становится известным числом. Для SKU, которые вы собираете по разным сессиям, можно показать реальный диапазон цен для покупателей вместо единственного замера.

Где этот подход упирается в ограничения: цены для участников программ лояльности скрыты за учетными записями, а персонализированные эксперименты для авторизованных пользователей вообще не видны в анонимных сессиях. Сбор таких данных упирается в правила сервиса раньше, чем в инженерные задачи. Кроме того, маркер магазина надежен ровно настолько, насколько надежен ваш выбор этого маркера. Извлекайте его из исходного кода страницы, а не из кэша, и перепроверяйте при каждом редизайне торговой сети.

Главный вывод

Раньше цена товара была характеристикой самого продукта. Сейчас это характеристика продукта, магазина и конкретного покупателя, а сборщик, фиксирующий только первый параметр, генерирует шум с очень аккуратной схемой данных.

Если персонализированное ценообразование продолжит расти, вопрос заказчиков к данным о ценах сместится с "сколько это стоит?" на "сколько это стоит здесь и каков разброс?". Поэтому доверие вызовут не те сборщики, у которых больше всего выходных узлов, а те, чей каждый request может точно указать целевой магазин и подтвердить это.