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

В этом руководстве показано, как создать инструмент сравнения цен на Python, который получает один и тот же товар с двух и более публичных страниц интернет-магазинов, нормализует данные в единую структуру, сопоставляет объявления из разных источников, сохраняет результат и сообщает минимальную цену. Каждую страницу мы загружаем через Crawling API, чтобы рендеринг и ротация IP обрабатывались автоматически, а всё руководство ограничивается публичными каталожными данными: название товара, цена, валюта, ретейлер и URL товара.

Что вы создадите

Небольшой Python-проект, который превращает несколько URL товаров в аккуратную таблицу сравнения. Каждое скрапнутое объявление становится записью с фиксированным набором полей, так что два ретейлера, описывающих товар по-разному, всё равно выстраиваются в одну линию. В итоге вы получите:

  • Название. Заголовок товара, как его указывает ретейлер.
  • Цена. Числовое значение, извлечённое из «грязного» текста на странице.
  • Валюта. Код валюты, чтобы никогда не сравнивать доллары с евро.
  • Ретейлер. Источник объявления.
  • URL. Точная страница, чтобы человек мог проверить предложение.
  • Сохранённый вывод. Каждая запись записывается в JSON и CSV, а также формируется сводка с минимальными ценами.

Почему обычный запрос не работает на страницах ретейлеров

На простых сайтах можно далеко продвинуться с библиотекой requests, но страницы современных интернет-магазинов защищаются двумя способами. Во-первых, многие из них рендерят цену на стороне клиента: исходный HTML почти пуст, а JavaScript заполняет каталог после загрузки. Обычный requests.get видит только эту оболочку, и цена, которую вы ищете, просто отсутствует в скачанном тексте.

Во-вторых, ретейлеры отслеживают автоматизированный трафик. Несколько запросов с одного IP датацентра в узнаваемом паттерне приводят к ограничению скорости, CAPTCHA или блокировке. Для сравнения цен в нескольких магазинах нужно, чтобы каждая страница была отрендерена и каждый запрос приходил с надёжного ротирующего адреса. Именно это сочетание предоставляет Crawling API в одном вызове, поэтому мы направляем каждую загрузку через него, а не обращаемся к сайтам напрямую. Если рендеринг JavaScript на страницах ретейлеров для вас в новинку, наше руководство по скрапингу JavaScript-страниц на Python объясняет основы.

Предварительные требования

Три вещи до начала написания кода.

Python 3.8 или новее. Проверьте командой python --version. Подойдёт любой актуальный релиз 3.x; скачайте текущую версию с официального сайта Python, если нужно.

Аккаунт и токен Crawlbase. Зарегистрируйтесь для получения бесплатного аккаунта и скопируйте токен из панели управления. Вы получите обычный токен (для статического HTML) и JavaScript-токен (для страниц с клиентским рендерингом); держите оба под рукой.

Базовые знания Python. Вы должны уметь запускать скрипт из терминала, устанавливать пакеты через pip и читать словари. Знакомство с HTML и CSS-селекторами полезно, поскольку вам придётся адаптировать их под целевые страницы.

Настройка проекта

Создайте папку и установите зависимости. Мы используем официальный клиент crawlbase для загрузки, beautifulsoup4 для парсинга HTML и pandas для хранения и отображения сравнения. Модули json и csv встроены, поэтому ничего дополнительно устанавливать не нужно.

bash
mkdir price-comparison && cd price-comparison

pip install crawlbase beautifulsoup4 pandas

Если хотите подробнее разобраться с парсингом, наше руководство по использованию BeautifulSoup в Python идёт глубже, чем приведённые ниже фрагменты.

Шаг 1: Загрузите первый источник

Начните с одной страницы одного ретейлера. Клиент Crawling API принимает ваш токен, загружает URL через ротирующий резидентный IP и возвращает готовый HTML. Для страницы с JavaScript-рендерингом передайте ajax_wait и page_wait, чтобы API дождался асинхронного контента и выдержал паузу после загрузки перед захватом. Для таких страниц используйте JavaScript-токен, для простых статических, обычный.

python
from crawlbase import CrawlingAPI

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

def fetch_page(url):
    response = api.get(url, {"ajax_wait": "true", "page_wait": 3000})
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    raise RuntimeError(f"Crawl failed: {response['status_code']}")

html = fetch_page("https://store-a.example.com/product/smartphone-xyz")
print(html[:500])

При запуске печатаются первые 500 символов отрендеренного HTML. Если вы видите реальную разметку страницы, а не пустую оболочку, загрузка сработала, и API выполнил рендеринг и ротацию IP за вас. Проверка status_code не позволяет мягким сбоям попасть в нижестоящий код как будто это полноценный HTML.

Crawlbase Crawling API

Тот единственный вызов fetch_page, который вы только что выполнили, сделал за вас две сложные вещи: отрендерил JavaScript, чтобы цена реально присутствовала в HTML, и отправил запрос через ротирующий резидентный IP, чтобы ретейлер вас не заблокировал. Вам не нужно разворачивать флот headless-браузеров и пул прокси, что особенно важно, когда вы начинаете загружать данные со второго и третьего магазина по расписанию. Направьте его на страницы, которые раньше возвращали пустой результат.

Шаг 2: Загрузите второй источник

Для сравнения нужно минимум два ретейлера. Логика загрузки не меняется: та же функция fetch_page работает с любым URL, потому что API справляется с любой защитой каждого сайта. Меняется только парсинг, поскольку у каждого ретейлера разная разметка названия и цены. Напишите один небольшой парсер для каждого источника, который читает страницу и возвращает нормализованную запись.

python
import re
from bs4 import BeautifulSoup

def parse_price(text):
    # Pull a number out of strings like "$279.99" or "1,299.00 USD"
    match = re.search(r"[\d,]+\.?\d*", text.replace(",", ""))
    return float(match.group()) if match else None

def parse_store_a(html, url):
    soup = BeautifulSoup(html, "html.parser")
    return {
        "name": soup.select_one("h1.product-title").get_text(strip=True),
        "price": parse_price(soup.select_one(".price").get_text()),
        "currency": "USD",
        "retailer": "Store A",
        "url": url,
    }

def parse_store_b(html, url):
    soup = BeautifulSoup(html, "html.parser")
    return {
        "name": soup.select_one("#productName").get_text(strip=True),
        "price": parse_price(soup.select_one("span.amount").get_text()),
        "currency": "USD",
        "retailer": "Store B",
        "url": url,
    }

Оба парсера возвращают одни и те же пять ключей, и в этом весь смысл: name, price, currency, retailer и url. Селекторы (h1.product-title, .price, #productName, span.amount) являются заглушками. Откройте каждый целевой сайт в инструментах разработчика браузера, найдите элементы, содержащие реальные название и цену, и замените их. Вспомогательная функция parse_price выполняет «грязную» работу по превращению "$279.99" в число 279.99, чтобы цены можно было сравнивать.

Selectors drift

Ретейлеры часто делают редизайн, и селектор, работавший в прошлом месяце, может незаметно начать возвращать None. Воспринимайте селекторы для каждого источника как то, что требует поддержки. Когда цена возвращается пустой, заново проверьте живую страницу и обновите один конкретный парсер, а не весь пайплайн.

Шаг 3: Нормализуйте и сопоставьте по источникам

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

python
# Each entry: (product key, retailer URL, parser)
sources = [
    ("Smartphone XYZ", "https://store-a.example.com/product/smartphone-xyz", parse_store_a),
    ("Smartphone XYZ", "https://store-b.example.com/p/smartphone-xyz", parse_store_b),
]

def collect(sources):
    products = {}
    for key, url, parser in sources:
        try:
            record = parser(fetch_page(url), url)
        except Exception as err:
            print(f"Skipping {url}: {err}")
            continue
        products.setdefault(key, []).append(record)
    return products

Результат, словарь с ключами по товарам и списком записей для каждого ретейлера под каждым ключом. Структура повторяет модель данных из исходного руководства (название товара со списком магазинов и цен), но здесь каждая запись скрапится в реальном времени и нормализуется, а не читается из статического файла. Оборачивание каждой загрузки в try/except позволяет заносить в лог неработающий URL и продолжать выполнение, а не падать на полпути.

Шаг 4: Сравните и сохраните результаты

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

python
def find_lowest_price(records):
    lowest_price = float("inf")
    best = None
    for record in records:
        if record["price"] is not None and record["price"] < lowest_price:
            lowest_price = record["price"]
            best = record
    return best

Затем соберите полный скрипт. Он собирает данные из всех источников, выводит лучшее предложение по каждому товару и записывает плоский список записей в JSON и CSV через pandas, чтобы можно было открыть сравнение в таблице или передать его другому инструменту.

python
import json
import pandas as pd

def main():
    products = collect(sources)
    rows = []

    for name, records in products.items():
        rows.extend(records)
        best = find_lowest_price(records)
        if best:
            print(f"Product: {name}")
            print(f" - Best Price: {best['currency']} {best['price']} at {best['retailer']}")

    # Store the full comparison as JSON and CSV
    with open("comparison.json", "w") as f:
        json.dump(rows, f, indent=2)

    pd.DataFrame(rows).to_csv("comparison.csv", index=False)
    print(f"Saved {len(rows)} records.")

if __name__ == "__main__":
    main()

Запустите командой python compare.py. В терминале отобразится строка с лучшей ценой для каждого товара, а на диске появятся два файла: comparison.json для программного использования и comparison.csv для таблицы. Если вы предпочитаете хранить историю цен со временем, замените запись в CSV на вставку в SQLite через встроенный модуль sqlite3; структура записей остаётся той же, поэтому ничего выше по потоку не меняется.

Как выглядит вывод

Файл JSON содержит один объект на каждое скрапнутое объявление с пятью нормализованными полями. Для двух ретейлеров, продающих один и тот же телефон, это выглядит так:

json
[
  {
    "name": "Smartphone XYZ",
    "price": 299.99,
    "currency": "USD",
    "retailer": "Store A",
    "url": "https://store-a.example.com/product/smartphone-xyz"
  },
  {
    "name": "Smartphone XYZ",
    "price": 279.99,
    "currency": "USD",
    "retailer": "Store B",
    "url": "https://store-b.example.com/p/smartphone-xyz"
  }
]

А сводка в терминале, формируемая функцией find_lowest_price, выглядит так:

bash
Product: Smartphone XYZ
 - Best Price: USD 279.99 at Store B
Saved 2 records.

CSV содержит те же строки в плоском формате: одна строка заголовков, затем по одной строке на объявление. Файл сразу открывается в любой таблице для сортировки и построения диаграмм.

Масштабирование на большее количество товаров и источников

Дизайн масштабируется за счёт добавления строк в список sources, а не переписывания логики. Чтобы отслеживать десять товаров у четырёх ретейлеров, добавьте парсер для каждого нового ретейлера и один кортеж для каждой пары (товар, ретейлер). Запускайте скрипт по расписанию (задание cron раз или дважды в день вполне достаточно для отслеживания цен) и дописывайте каждый запуск в файл с датой или в таблицу SQLite для построения истории цен.

Несколько привычек позволят поддерживать работоспособность при больших объёмах. Распределяйте запросы, чтобы не загружать десятки страниц в быстрой пачке. Обрабатывайте каждую загрузку изолированно (блок try/except в функции collect уже это делает), чтобы одна неработающая страница никогда не останавливала выполнение. Кэшируйте загруженный HTML на диск при работе над селекторами, чтобы не обращаться к живым сайтам при каждом изменении кода. Если страница впоследствии переходит со статического HTML на JavaScript-рендеринг, Crawling API поглощает это изменение, поскольку рендеринг, лишь опция того же вызова. Для страниц с известной структурой Crawling API может автоматически разбирать поля товара и возвращать структурированный JSON напрямую, что позволяет пропустить написание парсера для каждого источника, когда подходящий шаблон существует.

Создание инструмента сравнения цен с ответственным подходом

Инструмент сравнения цен обращается к тем же публичным каталожным страницам, что и покупатель, но это не означает, что всё допустимо. Перед тем как направить цикл на какой-либо ресурс, изучите условия использования каждого ретейлера и проверьте его robots.txt, а также поддерживайте умеренный объём запросов: пара загрузок в день для каждого товара достаточна для отслеживания цен и гораздо менее навязчива, чем постоянные запросы. Придерживайтесь публичных данных о товарах (название, цена, валюта, URL) и не пытайтесь получить данные за логином, личные данные или защищённые авторским правом медиафайлы для распространения.

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

Итоги

Ключевые выводы

  • Нормализуйте перед сравнением. Преобразуйте страницу каждого ретейлера в одинаковые пять полей (название, цена, валюта, ретейлер, URL), чтобы разная разметка всё равно выстраивалась в линию.
  • Один парсер на источник, один общий пайплайн. Загрузка одинакова для всех сайтов; меняются только селекторы для каждого источника, что обеспечивает стабильность остального кода.
  • Crawling API обрабатывает рендеринг и IP. Один вызов fetch_page возвращает отрендеренный HTML через ротирующий резидентный IP, избавляя от необходимости держать флот headless-браузеров и пул прокси.
  • Храните данные в плоском формате: JSON и CSV. Плоский список записей сериализуется в оба формата и легко переходит на SQLite для хранения истории цен без изменений выше по потоку.
  • Масштабируйте данными, а не кодом. Добавляйте товары и ретейлеров, расширяя список источников и запуская по расписанию, а не переписывая логику сравнения.

Часто задаваемые вопросы

Что такое инструмент сравнения цен?

Инструмент сравнения цен собирает цену одного товара у нескольких ретейлеров и показывает, где дешевле. В терминах кода он скрапит публичную страницу товара каждого ретейлера, нормализует данные в единую структуру, сопоставляет объявления, описывающие один товар, и сообщает минимальную цену. Именно это делает Python-проект из этого руководства, опрашивая два и более источника.

Почему использовать Crawling API вместо обычных requests?

Обычный requests скачивает только исходный HTML, поэтому пропускает цены, загружаемые на стороне клиента через JavaScript, а с одного IP быстро получают блокировку при загрузке нескольких магазинов подряд. Crawling API рендерит страницу и направляет каждый запрос через ротирующий резидентный IP в одном вызове, поэтому цена реально присутствует в разобранном HTML и вы остаётесь незаблокированными при работе с несколькими источниками.

Как сопоставить один товар у разных ретейлеров?

Наиболее надёжный подход, явный ключ товара, который вы задаёте для каждого URL, как это сделано в списке sources в этом руководстве, а не попытка автоматически сопоставлять слегка различающиеся названия. Для больших каталогов можно выполнять сопоставление по общему идентификатору, такому как номер модели, GTIN или артикул, когда ретейлеры его раскрывают, это надёжнее нечёткого сопоставления по названию.

Что лучше: хранить данные в JSON или CSV?

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

Можно ли автоматически отслеживать цены со временем?

Да. Запускайте скрипт по расписанию (например, задание cron раз или дважды в день) и дописывайте записи каждого запуска в файл с датой или в таблицу SQLite. Умеренные и регулярные запросы достаточны для обнаружения снижений цен и построения истории без чрезмерной нагрузки на любого ретейлера. Поддерживайте лёгкий ритм и соблюдайте условия использования каждого сайта.

Нужен ли headless-браузер, например Selenium?

При загрузке через Crawling API нет. Headless-браузер, такой как Selenium или Playwright, может рендерить JavaScript-страницы, но каждый экземпляр, это полный браузер, потребляющий память и CPU, и при масштабировании вам всё равно придётся самостоятельно управлять прокси. Crawling API рендерит страницу на стороне сервера и ротирует IP за вас, поэтому достаточно простого Python-скрипта с BeautifulSoup.

Начать создавать

Обходите любой сайт в масштабе, без борьбы с инфраструктурой.

Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.

Самообслуживание · Звонок отдела продаж не требуется · Доступны корпоративные объёмы краулинга