Значительная часть данных, достойных сбора на современном вебе, никогда не появляется в исходном коде страницы. Сетка товаров, заполняемая при прокрутке, таблица, обновляющаяся при изменении фильтра, панель мониторинга, загружающая числа с небольшой задержкой после отображения макета: все они используют AJAX (Asynchronous JavaScript and XML) для получения контента в фоновом режиме и встраивания его в уже загруженную страницу. Это создаёт плавный пользовательский опыт и незаметно ломает простейшие скраперы.

Это руководство показывает, как парсить данные с AJAX-сайтов с помощью Python. Вы создадите небольшой рабочий скрапер, который отрисовывает страницу через Crawling API, ожидает прихода асинхронного контента, захватывает данные, загружаемые страницей через XHR, парсит их и экспортирует в чистые форматы JSON и CSV. Руководство использует нейтральный заменитель листинга, чтобы вы могли изучить механику, а затем направить тот же поток на собственную цель.

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

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

  • Name (название) заголовок, отображаемый на каждой карточке листинга.
  • Price (цена) числовая цена, отображаемая для элемента.
  • Category (категория) группа или тег, к которому относится элемент.
  • Link (ссылка) URL страницы элемента с подробным описанием.

Вы увидите два пути к одним и тем же данным: прямое воспроизведение AJAX-вызова и отрисовка полной страницы. Оба заканчиваются одним и тем же шагом экспорта.

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

Запрос AJAX-URL с помощью простого HTTP-клиента вернёт статус 200 и почти ни одного из нужных данных. Причина кроется в синхронизации. Сервер отправляет тонкую HTML-оболочку, браузер выполняет JavaScript страницы, и только после этого скрипт инициирует фоновые запросы (AJAX-вызовы), возвращающие реальный контент и встраивающие его в DOM. Обычный запрос requests.get останавливается на оболочке: он никогда не выполняет JavaScript, не инициирует последующие вызовы, и полученное тело в основном представляет собой пустой макет.

Существует два честных способа обойти это. Первый: найти AJAX-эндпоинт, который страница вызывает в фоновом режиме, и запросить его напрямую, что быстро, поскольку вы пропускаете отрисовку. Второй: отрисовать всю страницу в реальном браузере, чтобы асинхронный контент загрузился, а затем парсить готовый HTML. Этот маршрут предпочтителен, когда эндпоинт подписан или сложно воспроизводим. Руководство по сбору данных с JavaScript-сайтов подробно описывает сторону отрисовки.

Normal token vs JS token

Crawlbase предлагает два типа токенов. Обычный токен загружает статичный HTML; JavaScript (JS) токен сначала отрисовывает страницу в реальном браузере. Поскольку AJAX-контент появляется только после выполнения скриптов, здесь используется JS-токен в паре с ajax_wait и page_wait, чтобы API удерживал соединение до завершения фоновых вызовов, а затем сохранял страницу.

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

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

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

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

Аккаунт Crawlbase и JS-токен. Зарегистрируйтесь, откройте панель управления и скопируйте JavaScript (JS) токен. Crawlbase включает до 20 000 бесплатных запросов для старта, чего более чем достаточно для этого руководства. Относитесь к токену как к паролю и не добавляйте его в систему контроля версий.

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

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

bash
python --version

python -m venv ajax_env
source ajax_env/bin/activate

pip install crawlbase beautifulsoup4

В Windows активируйте окружение командой ajax_env\Scripts\activate вместо строки с source. Две зависимости выполняют основную работу: crawlbase является официальным клиентом для Crawling API, а beautifulsoup4 парсит возвращаемый HTML при выборе маршрута с отрисованной страницей. Модули json и csv входят в стандартную библиотеку, поэтому для шага экспорта ничего дополнительно устанавливать не нужно.

Шаг 1: Определение AJAX-запроса

Прежде чем писать код, найдите фоновый вызов, который делает страница. Откройте цель в Chrome, щёлкните правой кнопкой мыши и выберите «Просмотр кода» (или нажмите Ctrl+Shift+I), затем переключитесь на вкладку «Сеть». Отфильтруйте по XHR, что изолирует XMLHttpRequest и fetch-вызовы от изображений и таблиц стилей, затем перезагрузите страницу. По мере заполнения контента появится запрос, который его передал. Кликните на него, чтобы увидеть URL запроса, его параметры и возвращённый JSON.

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

bash
https://example.com/api/items?page=1&limit=20

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

Шаг 2: Загрузка AJAX-эндпоинта через Crawlbase

Даже чистый JSON-эндпоинт может ограничить скорость или заблокировать автоматизированный трафик с IP-адреса дата-центра. Маршрутизация вызова через Crawlbase даёт вам доверенный IP и встроенную ротацию, поэтому запрос выглядит как от реального посетителя. Импортируйте класс CrawlingAPI, инициализируйте его с вашим токеном и запросите эндпоинт. Проверка cb_status (legacy pc_status) перед парсингом позволяет явно сообщать об ошибках, а не замалчивать их.

python
import json
from crawlbase import CrawlingAPI

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

endpoint = "https://example.com/api/items?page=1&limit=20"

def fetch_json(url):
    response = api.get(url)
    if response["headers"]["cb_status"] == "200":
        return json.loads(response["body"].decode("utf-8"))
    print(f"Request failed: {response['headers']['cb_status']}")
    return None

if __name__ == "__main__":
    data = fetch_json(endpoint)
    print(data if data else "No data returned")

Запустите скрипт командой python ajax_scraper.py, и вы должны увидеть сырой JSON, который страница загрузила бы в браузере, полученный в одном вызове без отрисовки чего-либо. Это подтверждает доступность эндпоинта до написания строки парсинга.

Crawlbase Crawling API

Приведённый выше запрос достиг AJAX-эндпоинта без запуска браузера или управления IP-адресами, что и обеспечивает Crawling API. Передайте ему обычный токен для чистого JSON-эндпоинта или JS-токен с ajax_wait и page_wait, когда нужна полная отрисовка страницы. Он ротирует жилые IP-адреса на стороне сервера и возвращает готовый контент, поэтому вам не нужно самостоятельно запускать headless-флот и пул прокси. Начните с бесплатного тарифа, направив скрапер на публичную страницу.

Шаг 3: Парсинг JSON-ответа

Эндпоинт возвращает структурированный JSON, поэтому HTML для парсинга нет. Пройдите по объекту к списку элементов и извлеките нужные поля. Точные имена ключей зависят от вашей цели, поэтому изучите ответ из шага 1 и сопоставьте их. В нашем заменителе элементы находятся под ключом items, каждый с полями name, price, category и url.

python
def parse_items(data):
    records = []
    for item in data.get("items", []):
        records.append({
            "name": item.get("name"),
            "price": item.get("price"),
            "category": item.get("category"),
            "link": item.get("url"),
        })
    return records

Использование dict.get вместо доступа по квадратным скобкам означает, что отсутствующий ключ вернёт None, а не выбросит исключение KeyError, поэтому один некорректный элемент не завершит выполнение. Передайте JSON из шага 2 в parse_items, и вы получите аккуратный список записей, готовых к экспорту.

Шаг 4: Отрисовка полной страницы при отсутствии чистого эндпоинта

Иногда AJAX-вызов подписан, привязан к cookie сессии или разбит на несколько запросов, и его воспроизведение оказывается сложнее, чем оно того стоит. В таком случае отрисуйте всю страницу с JS-токеном, позвольте Crawlbase подождать асинхронного контента, а затем парсите готовую разметку с помощью BeautifulSoup, как и любую статичную страницу.

python
from bs4 import BeautifulSoup

RENDER_OPTIONS = {
    "ajax_wait": "true",
    "page_wait": 5000,
}

def fetch_rendered(url):
    response = api.get(url, RENDER_OPTIONS)
    if response["headers"]["cb_status"] == "200":
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['headers']['cb_status']}")
    return None

def parse_cards(html):
    soup = BeautifulSoup(html, "html.parser")
    records = []
    for card in soup.select("div.item-card"):
        link = card.select_one("a.item-link")
        records.append({
            "name": text_of(card, "h2.item-name"),
            "price": text_of(card, "span.item-price"),
            "category": text_of(card, "span.item-category"),
            "link": link["href"] if link else None,
        })
    return records

Два параметра ожидания берут на себя основную нагрузку. ajax_wait указывает API удерживать соединение до завершения асинхронного контента, а page_wait добавляет фиксированную паузу в миллисекундах после загрузки, чтобы карточки с задержкой отрисовки появились до захвата. Пять секунд, разумная отправная точка; увеличивайте значение, если элементы возвращаются пустыми. Вспомогательная функция parse_cards затем читает каждый элемент div.item-card и сопоставляет те же четыре поля, поэтому её вывод полностью совпадает с выводом parse_items. Вспомогательная функция text_of, используемая здесь, определена в полном скрипте ниже.

Шаг 5: Обработка пагинации и сборка скрипта

Одна страница редко охватывает весь набор данных. Большинство AJAX-листингов пагинируются через параметр запроса (здесь page), поэтому вы перебираете номера страниц, собираете записи с каждой и останавливаетесь, когда страница возвращает пустой результат. Объедините этот цикл с шагами загрузки, парсинга и экспорта в один рабочий скрипт.

python
import csv
import json
import time
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})
BASE = "https://example.com/api/items?limit=20&page="

def fetch_json(url):
    response = api.get(url)
    if response["headers"]["cb_status"] == "200":
        return json.loads(response["body"].decode("utf-8"))
    print(f"Request failed: {response['headers']['cb_status']}")
    return None

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

def parse_items(data):
    records = []
    for item in data.get("items", []):
        records.append({
            "name": item.get("name"),
            "price": item.get("price"),
            "category": item.get("category"),
            "link": item.get("url"),
        })
    return records

def collect_all(max_pages=5):
    all_records = []
    for page in range(1, max_pages + 1):
        data = fetch_json(f"{BASE}{page}")
        if not data:
            break
        records = parse_items(data)
        if not records:
            break
        all_records.extend(records)
        time.sleep(2)
    return all_records

def save_outputs(records):
    with open("items.json", "w") as f:
        json.dump(records, f, indent=2)
    if not records:
        return
    with open("items.csv", "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=records[0].keys())
        writer.writeheader()
        writer.writerows(records)

def main():
    records = collect_all(max_pages=5)
    save_outputs(records)
    print(f"Saved {len(records)} items")

if __name__ == "__main__":
    main()

Скрипт обходит до пяти страниц AJAX-эндпоинта, парсит каждую в записи, останавливается, как только страница ничего не возвращает, и регулирует скорость цикла с двухсекундной паузой. save_outputs записывает файл JSON и CSV, используя ключи первой записи в качестве заголовка. Если у вашей цели нет чистого эндпоинта, замените пару fetch_json + parse_items на пару fetch_rendered + parse_cards из шага 4; шаг экспорта при этом не изменится.

Как выглядит результат

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

json
[
  {
    "name": "Wireless Keyboard",
    "price": "49.00",
    "category": "Accessories",
    "link": "https://example.com/items/wireless-keyboard"
  },
  {
    "name": "Standing Desk",
    "price": "299.00",
    "category": "Furniture",
    "link": "https://example.com/items/standing-desk"
  }
]

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

Сохранение работоспособности при масштабировании

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

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

Для более крупных обходов асинхронный Crawler ставит запросы в очередь и доставляет результаты на вебхук, что удобно для обработки многих AJAX-страниц без удержания открытых соединений. Более широкое руководство по теме читайте в статьях о парсинге сайтов без блокировок и о парсинге JavaScript-страниц с Python.

Ответственный парсинг

Ограничивайте эту работу публичными данными и относитесь к правилам целевого ресурса как к границе допустимого. Ознакомьтесь с условиями использования сайта и файлом robots.txt перед направлением скрапера на него, и собирайте только данные, доступные любому посетителю без аккаунта. Регулируйте скорость запросов, чтобы не перегружать сервер, и никогда не касайтесь ничего, требующего входа в систему, и не пытайтесь обойти аутентификацию. Когда данные касаются идентифицируемых людей, применяются законы о конфиденциальности, например GDPR или CCPA, поэтому избегайте персональных или контактных данных без чёткого правового основания для их сбора. Если цель предоставляет официальный API для нужных данных, это, как правило, более чистый и долгосрочный путь, чем парсинг отрисованной страницы.

Итоги

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

  • AJAX-контент загружается после оболочки. Обычный запрос останавливается на исходном HTML и никогда не выполняет скрипты, загружающие реальные данные, поэтому полученное тело в основном пустое.
  • Два пути ведут к одним данным. Воспроизведите фоновый XHR-эндпоинт напрямую для скорости или отрисуйте полную страницу с JS-токеном, когда эндпоинт подписан или неудобен для воспроизведения.
  • Дождитесь контента. При маршруте с отрисовкой параметры ajax_wait и page_wait удерживают соединение до завершения асинхронных вызовов, прежде чем Crawlbase захватит страницу.
  • Нормализуйте, затем экспортируйте. Приведите оба маршрута к одной форме записи, переберите страницы через параметр запроса и запишите результаты в JSON и CSV из одной функции.
  • Парсите ответственно. Соблюдайте условия использования и robots.txt, работайте только с публичными данными, регулируйте скорость запросов и применяйте правила GDPR или CCPA при наличии персональных данных.

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

Что такое AJAX и почему он усложняет парсинг?

AJAX (Asynchronous JavaScript and XML), это техника, позволяющая странице получать контент в фоновом режиме и обновлять часть DOM без перезагрузки. Она усложняет парсинг, поскольку данные отсутствуют в исходном HTML: они поступают только после того, как браузер выполнит JavaScript страницы и вернутся фоновые вызовы. Обычный HTTP-запрос никогда не выполняет этот JavaScript, поэтому он захватывает тонкую оболочку с отсутствующим реальным контентом.

Можно ли парсить AJAX-контент без отрисовки браузера?

Зачастую да. Отфильтруйте вкладку «Сеть» инструментов разработчика по XHR и найдите запрос, несущий данные. Если этот эндпоинт доступен, вы можете запросить его напрямую и парсить возвращаемый JSON, что быстрее, чем отрисовка страницы. Когда эндпоинт подписан, привязан к сессии или разбит на несколько вызовов, отрисовка с JS-токеном является более надёжным путём.

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

Это зависит от маршрута. Для чистого JSON-эндпоинта, найденного на вкладке «Сеть», обычного токена достаточно, поскольку отрисовывать нечего. Для загрузки полной страницы, контент которой появляется только после выполнения скриптов, используйте JS-токен вместе с ajax_wait и page_wait, чтобы Crawlbase ожидал завершения асинхронных вызовов перед захватом HTML.

Что на самом деле делают ajax_wait и page_wait?

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

Мой список из парсинга пустой. Что пошло не так?

Проверьте три вещи по порядку. Во-первых, убедитесь, что cb_status вернул 200; значение, отличное от 200, означает сбой запроса. Во-вторых, при маршруте через эндпоинт снова проверьте ключи JSON, поскольку они могут отличаться от имён заменителей, использованных здесь. В-третьих, при маршруте с отрисовкой увеличьте page_wait и проверьте CSS-селекторы на актуальной странице, поскольку имена классов в генерируемой разметке меняются без предупреждения.

Как масштабировать на много страниц?

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

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

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

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

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