Groupon ежедневно запускает тысячи местных скидок в сферах питания, путешествий, оздоровления и розничной торговли, и каждое объявление содержит структурированные данные, которые нужны инструменту для отслеживания скидок, наблюдателю за ценами или дашборду конкурентного исследования: название, цену со скидкой, исходную цену, экономию, зону применения скидки и ссылку на предложение. Загвоздка в том, что Groupon создаёт свои страницы категорий в браузере с помощью JavaScript, поэтому простой HTTP-запрос возвращает почти пустую оболочку вместо нужных предложений.

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

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

Скрипт Python, который принимает публичный URL категории Groupon, получает отрендеренный HTML через Crawling API и извлекает структурированную запись для каждого предложения на странице. В качестве текущего примера мы будем использовать страницу предложений города и извлекать следующие поля из каждой карточки:

  • Название заголовок предложения, например «55-минутный массаж для пар».
  • Текущая цена цена со скидкой, которую вы фактически платите.
  • Исходная цена перечёркнутая прайс-цена.
  • Скидка экономия, вычисленная из двух цен.
  • Местоположение адрес или район, на который распространяется предложение.
  • Ссылка URL страницы отдельного предложения.

Почему простой запрос не работает на Groupon

Если вы запрашиваете URL категории Groupon с помощью обычного HTTP-клиента, вы получаете ответ со статусом 200 и почти никаких данных о предложениях в теле. Против вас работают две вещи. Во-первых, Groupon рендерит карточки предложений в браузере с помощью JavaScript, поэтому начальный HTML, это фрейм, который заполняется только после того, как скрипты страницы выполнятся. Во-вторых, Groupon быстро помечает автоматизированный трафик: IP дата-центров и паттерны запросов, не похожие на настоящий браузер, получают вызов или блокируются ещё до того, как доберутся до отрендеренных карточек.

Таким образом, работающий парсер Groupon в одном запросе должен обеспечить две вещи: браузер, который фактически рендерит страницу, и IP, который платформа воспринимает как реального посетителя. Вы можете собрать это самостоятельно с помощью безголового браузера плюс пул ротирующих резидентных прокси, но их совместная настройка и поддержание в рабочем состоянии, это большая часть работы. Crawling API объединяет оба аспекта в один вызов: вы отправляете ему URL с токеном JavaScript, он рендерит страницу за доверенным IP и возвращает готовый HTML для парсинга.

Зачем нужен JS-токен

Crawlbase предлагает два типа токенов. Обычный токен получает статический HTML; JavaScript (JS) токен сначала рендерит страницу в реальном браузере. Groupon рендерится на стороне клиента, поэтому здесь нужен JS-токен. Использование обычного токена возвращает тот же пустой фрейм, что и обычный запрос, и из него нечего парсить. Можно начать с лимита до 20 000 бесплатных запросов, без кредитной карты.

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

Перед написанием кода необходимо подготовить несколько вещей. Ни одно из них не займёт много времени.

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

Python 3.8 или выше. Проверьте свою версию с помощью python --version. Если его нет, установите его с python.org или через дистрибутив, такой как Anaconda.

Аккаунт Crawlbase и JS-токен. Зарегистрируйтесь, откройте дашборд и скопируйте ваш JavaScript (JS) токен со страницы документации аккаунта. Обращайтесь с токеном как с паролем: он аутентифицирует ваши запросы, поэтому не добавляйте его в систему контроля версий.

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

Создайте виртуальное окружение, чтобы зависимости проекта оставались изолированными, а затем установите две библиотеки, необходимые парсеру.

bash
python --version

python -m venv groupon_env
source groupon_env/bin/activate

pip install crawlbase beautifulsoup4

На Windows активируйте окружение с помощью groupon_env\Scripts\activate вместо строки с source. Две зависимости выполняют всю работу: crawlbase, официальный клиент для Crawling API, а beautifulsoup4 парсит возвращённый HTML, позволяя извлекать отдельные поля по CSS-селектору.

Шаг 1: получение отрендеренной страницы категории

Начните с получения готовой страницы. Импортируйте класс CrawlingAPI, инициализируйте его с вашим JS-токеном и запросите URL категории. Проверка статуса перед парсингом делает ошибки заметными, а не молчаливыми.

python
from crawlbase import CrawlingAPI

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

def crawl(page_url):
    options = {"ajax_wait": "true", "page_wait": 5000}
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['status_code']}")
    return None

if __name__ == "__main__":
    page_url = "https://www.groupon.com/local/washington-dc"
    html = crawl(page_url)
    print(html[:500] if html else "No HTML returned")

Два параметра ожидания важны для цели с рендерингом на стороне клиента. ajax_wait говорит API дождаться завершения асинхронной загрузки контента, а page_wait выдерживает фиксированное количество миллисекунд после загрузки, чтобы поздно рендерящиеся карточки появились до захвата страницы. Пять секунд, разумная отправная точка; увеличьте, если карточки предложений возвращаются пустыми. Запустите скрипт командой python scraper.py, и вы должны увидеть настоящую разметку предложений, а не пустой фрейм, который возвращает обычный запрос. Это подтверждает работу рендеринга до написания единого селектора.

Crawlbase Crawling API

Groupon требует отрендеренной страницы за доверенным IP в одном вызове. Crawling API принимает JS-токен, запускает страницу в реальном браузере, ротирует через резидентные IP на стороне сервера и передаёт готовый HTML, поэтому вам не нужно самостоятельно запускать флот безголовых браузеров и пул прокси. Сначала направьте его на публичную страницу предложений города на бесплатном уровне.

Шаг 2: парсинг карточек предложений с помощью BeautifulSoup

Имея отрендеренный HTML, загрузите его в BeautifulSoup и извлеките каждое предложение по его селектору. Groupon размещает карточки предложений в повторяющейся структуре, поэтому вы выбираете все карточки один раз, а затем считываете одни и те же поля из каждой. Проверьте текущие имена классов в инструментах разработчика браузера на живой странице; приведённые ниже селекторы соответствуют макету на момент написания.

python
from bs4 import BeautifulSoup

def text_of(card, selector):
    el = card.select_one(selector)
    return el.get_text(strip=True) if el else None

def parse_deals(html):
    soup = BeautifulSoup(html, "html.parser")
    cards = soup.select('div[data-item-type="card"] > article')
    deals = []

    for card in cards:
        link = card.select_one("a[href]")
        deals.append({
            "title": text_of(card, "h2.text-dealCardTitle"),
            "current_price": text_of(card, 'div[data-testid="green-price"]'),
            "original_price": text_of(card, 'div[data-testid="strike-through-price"]'),
            "location": text_of(card, "h2 + div span"),
            "link": link["href"] if link else None,
        })

    return deals

Вспомогательная функция text_of делает две полезные вещи одновременно: она запрашивает единственный элемент внутри карточки и возвращает None, когда этот элемент отсутствует, вместо того чтобы выдавать ошибку при вызове .get_text() против пустоты. Это делает извлечение устойчивым, когда одно поле отсутствует в данной карточке, что обычно происходит, поскольку не каждое предложение указывает перечёркнутую исходную цену или местоположение. Ссылка считывается из атрибута href якоря, а не из его текста, поэтому обрабатывается отдельно.

Дрейф селекторов

Имена классов и атрибуты data-testid Groupon меняются без предупреждения. Рассматривайте приведённые выше селекторы как начальный шаблон, а не контракт. Когда поле возвращает None для каждой карточки, заново проверьте живое предложение в инструментах разработчика браузера и обновите селектор. Периодическое обслуживание селекторов нормально для любого промышленного парсера, а не признак того, что что-то сломалось.

Шаг 3: вычисление скидки

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

python
import re

def to_amount(price):
    if not price:
        return None
    match = re.search(r"[\d,.]+", price)
    return float(match.group().replace(",", "")) if match else None

def discount_percent(original, current):
    o, c = to_amount(original), to_amount(current)
    if not o or not c or o <= 0:
        return None
    return round((o - c) / o * 100)

Вспомогательная функция to_amount убирает символы валюты и разделители тысяч, чтобы "$45" и "$1,299.00" оба парсились корректно, и возвращает None вместо сбоя, когда цена отсутствует. discount_percent защищает от нулевой или отсутствующей исходной цены перед делением, поэтому карточка без перечёркнутой цены просто даёт скидку None вместо ошибки. Включите результат в каждую запись в цикле с помощью "discount": discount_percent(original_price, current_price).

Шаг 4: сборка всего вместе

Теперь соедините получение, парсинг и вычисление скидки в один работоспособный скрипт. Получите отрендеренный HTML, передайте его парсеру, обогатите каждую запись вычисленной скидкой и запишите результат в JSON.

python
import json
import re
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

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

def crawl(page_url):
    options = {"ajax_wait": "true", "page_wait": 5000}
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['status_code']}")
    return None

def text_of(card, selector):
    el = card.select_one(selector)
    return el.get_text(strip=True) if el else None

def to_amount(price):
    if not price:
        return None
    match = re.search(r"[\d,.]+", price)
    return float(match.group().replace(",", "")) if match else None

def discount_percent(original, current):
    o, c = to_amount(original), to_amount(current)
    if not o or not c or o <= 0:
        return None
    return round((o - c) / o * 100)

def parse_deals(html):
    soup = BeautifulSoup(html, "html.parser")
    cards = soup.select('div[data-item-type="card"] > article')
    deals = []

    for card in cards:
        link = card.select_one("a[href]")
        current = text_of(card, 'div[data-testid="green-price"]')
        original = text_of(card, 'div[data-testid="strike-through-price"]')
        deals.append({
            "title": text_of(card, "h2.text-dealCardTitle"),
            "current_price": current,
            "original_price": original,
            "discount": discount_percent(original, current),
            "location": text_of(card, "h2 + div span"),
            "link": link["href"] if link else None,
        })

    return deals

def main():
    page_url = "https://www.groupon.com/local/washington-dc"
    html = crawl(page_url)
    if not html:
        return
    deals = parse_deals(html)
    with open("groupon_deals.json", "w") as f:
        json.dump(deals, f, indent=2)
    print(f"Saved {len(deals)} deals")

if __name__ == "__main__":
    main()

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

Запустите полный скрипт командой python scraper.py, и вы получите чистую структурированную запись для каждого предложения, готовую для записи в JSON, CSV или базу данных.

json
[
  {
    "title": "55-Minute Couples Massage or 50-Minute Deep Tissue Massage",
    "current_price": "$65",
    "original_price": "$95",
    "discount": 32,
    "location": "4238 Wilson Boulevard, Arlington",
    "link": "https://www.groupon.com/deals/refresh-therapeutic-massage-3"
  },
  {
    "title": "Spa Package with Glass of Wine at Spa Logic",
    "current_price": "$219",
    "original_price": "$330",
    "discount": 34,
    "location": "1721 Connecticut Avenue Northwest, Washington",
    "link": "https://www.groupon.com/deals/spa-logic-12"
  }
]

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

Одна категория, это демо; реальная задача охватывает несколько городов или вертикалей. Форма остаётся той же: ведите список URL категорий, загружайте каждый через Crawling API, парсите его той же функцией и собирайте строки. Поскольку каждая страница категории имеет одинаковую структуру карточек, написанный вами парсер работает для всех них без изменений. Одна привычка, которая обеспечивает здоровье длинного запуска, это темп: делайте паузу между запросами, чтобы не атаковать Groupon в тесном цикле.

python
import time

categories = [
    "https://www.groupon.com/local/washington-dc",
    "https://www.groupon.com/local/new-york-city",
    "https://www.groupon.com/local/chicago",
]

results = []
for url in categories:
    html = crawl(url)
    if html:
        results.extend(parse_deals(html))
    time.sleep(3)

with open("groupon_deals.json", "w") as f:
    json.dump(results, f, indent=2)

Groupon также загружает дополнительные предложения за кнопкой «Загрузить ещё», а не на пронумерованной странице. Если вы хотите получить карточки ниже сгиба, передайте в Crawling API опцию css_click_selector, указывающую на эту кнопку, и API нажмёт её перед захватом страницы. Проверьте текущий селектор живой кнопки в инструментах разработчика, поскольку этот атрибут меняется так же, как и остальная разметка.

Как оставаться незаблокированным

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

  • Регулируйте частоту запросов. Атака страниц категорий в тесном цикле, самый быстрый способ попасть под ограничение. Распределите запросы и чередуйте цели вместо того, чтобы краулить один город на полной скорости.
  • Полагайтесь на ротацию. Пул резидентных IP распределяет запросы по многим адресам реальных пользователей, чтобы ни один не превысил ограничение скорости. Crawling API справляется с этим за вас; если вы создаёте собственный стек, именно эту часть нужно сделать правильно.
  • Читайте коды состояния. Запуск, который начинает возвращать вызовы или ошибки, сигнализирует о том, что текущей скорости или уровня IP больше недостаточно. Воспринимайте это как сигнал к отступлению, а не как шум, который нужно игнорировать.

Для более широкого руководства смотрите как парсить сайты, не попадая в блокировку и подробное погружение в обход CAPTCHA при парсинге. Если ваша цель рендерится на стороне клиента, как Groupon, наше руководство по краулингу JavaScript-сайтов объясняет, почему важен рендеринг. А если вы предпочитаете маршрутизировать собственный трафик через ротирующий пул вместо использования управляемого API, Smart AI Proxy (также называемый AI Proxy) предоставляет ту же ротацию резидентных IP в виде встраиваемого прокси-эндпоинта.

Законно ли парсить Groupon?

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

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

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

Итоги

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

  • Groupon рендерится на стороне клиента. Обычный запрос возвращает пустой фрейм, поэтому необходимо отрендерить страницу перед парсингом.
  • Нужны рендеринг и доверенный IP вместе. Crawling API с JS-токеном делает оба в одном вызове; ajax_wait и page_wait контролируют время ожидания контента.
  • BeautifulSoup выполняет извлечение. Выберите все карточки предложений, затем считайте название, текущую цену, исходную цену, местоположение и ссылку из каждой, и ожидайте дрейфа селекторов.
  • Вычисляйте скидку самостоятельно. Парсинг обоих цен и вычисление экономии более последовательны, чем парсинг бейджа, который не всегда присутствует.
  • Масштабируйтесь, перебирая категории и регулируя темп. Тот же парсер работает на каждой странице категории, поэтому реальная задача, это список URL плюс задержка между запросами.
  • Оставайтесь на публичных данных. Соблюдайте ToS и robots.txt Groupon и никогда не трогайте аккаунты, персональные данные или страницы, защищённые авторизацией.

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

Почему обычный запрос не возвращает предложения от Groupon?

Потому что Groupon рендерит карточки предложений на стороне клиента с помощью JavaScript. Начальный HTML, это фрейм, который заполняется только после того, как скрипты страницы выполнятся в браузере, поэтому необработанный HTTP-запрос возвращает статус 200 с пустыми карточками. Чтобы получить реальные данные, необходимо сначала отрендерить страницу, что и делает JS-токен Crawling API.

Нужен ли обычный токен или JS-токен для Groupon?

JS-токен. Обычный токен получает статический HTML, который на Groupon, тот же пустой фрейм, что и обычный запрос. JS-токен рендерит страницу в реальном браузере перед возвратом HTML, поэтому карточки предложений присутствуют при парсинге BeautifulSoup.

Как мне собрать все предложения, скрытые за кнопкой «Загрузить ещё»?

Groupon загружает дополнительные предложения за кнопкой, а не на пронумерованной странице. Передайте в Crawling API опцию css_click_selector с CSS-селектором кнопки, и API нажмёт её перед захватом страницы, чтобы дополнительные карточки были в HTML, который вы парсите. Проверьте текущий селектор кнопки в инструментах разработчика браузера, поскольку этот атрибут меняется со временем.

Мои селекторы возвращают None для каждой карточки. Что изменилось?

Почти наверняка разметка Groupon. Его имена классов и атрибуты data-testid меняются без предупреждения, поэтому селекторы, работавшие в прошлом месяце, могут сломаться. Заново проверьте живое предложение в инструментах разработчика браузера и обновите селекторы. Периодическое обслуживание селекторов нормально для любого промышленного парсера.

Можно ли собирать аккаунты или персональные данные из Groupon?

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

Как избежать блокировки при парсинге Groupon?

Держите частоту запросов на один IP низкой, чередуйте цели вместо циклического перебора одного города и маршрутизируйте через ротирующие резидентные IP, чтобы ни один адрес не превысил ограничение скорости. Crawling API управляет ротацией и доверенным пулом IP для вас; если вы создаёте собственный стек, именно в это стоит инвестировать. Следите за кодами состояния и отступайте, когда начинаете видеть вызовы.

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

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

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

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