Значительная часть данных, достойных сбора на современном вебе, никогда не появляется в исходном коде страницы. Сетка товаров, заполняемая при прокрутке, таблица, обновляющаяся при изменении фильтра, панель мониторинга, загружающая числа с небольшой задержкой после отображения макета: все они используют 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-сайтов подробно описывает сторону отрисовки.
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 бесплатных запросов для старта, чего более чем достаточно для этого руководства. Относитесь к токену как к паролю и не добавляйте его в систему контроля версий.
Настройка проекта
Создайте виртуальное окружение, чтобы зависимости проекта оставались изолированными, затем установите необходимые библиотеки.
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-эндпоинта, который выглядит следующим образом:
https://example.com/api/items?page=1&limit=20
Этот эндпоинт возвращает те же данные, что и страница, только в виде чистого JSON вместо отрисованного HTML. Если такой вызов существует и доступен, запрос к нему напрямую является наиболее простым путём. Когда он подписан, привязан к сессии или иным образом неудобен для воспроизведения, вместо этого выполняется отрисовка страницы. Оба маршрута описаны ниже.
Шаг 2: Загрузка AJAX-эндпоинта через Crawlbase
Даже чистый JSON-эндпоинт может ограничить скорость или заблокировать автоматизированный трафик с IP-адреса дата-центра. Маршрутизация вызова через Crawlbase даёт вам доверенный IP и встроенную ротацию, поэтому запрос выглядит как от реального посетителя. Импортируйте класс CrawlingAPI, инициализируйте его с вашим токеном и запросите эндпоинт. Проверка cb_status (legacy pc_status) перед парсингом позволяет явно сообщать об ошибках, а не замалчивать их.
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, который страница загрузила бы в браузере, полученный в одном вызове без отрисовки чего-либо. Это подтверждает доступность эндпоинта до написания строки парсинга.
Приведённый выше запрос достиг AJAX-эндпоинта без запуска браузера или управления IP-адресами, что и обеспечивает Crawling API. Передайте ему обычный токен для чистого JSON-эндпоинта или JS-токен с ajax_wait и page_wait, когда нужна полная отрисовка страницы. Он ротирует жилые IP-адреса на стороне сервера и возвращает готовый контент, поэтому вам не нужно самостоятельно запускать headless-флот и пул прокси. Начните с бесплатного тарифа, направив скрапер на публичную страницу.
Шаг 3: Парсинг JSON-ответа
Эндпоинт возвращает структурированный JSON, поэтому HTML для парсинга нет. Пройдите по объекту к списку элементов и извлеките нужные поля. Точные имена ключей зависят от вашей цели, поэтому изучите ответ из шага 1 и сопоставьте их. В нашем заменителе элементы находятся под ключом items, каждый с полями name, price, category и url.
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, как и любую статичную страницу.
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), поэтому вы перебираете номера страниц, собираете записи с каждой и останавливаетесь, когда страница возвращает пустой результат. Объедините этот цикл с шагами загрузки, парсинга и экспорта в один рабочий скрипт.
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, и вы получите чистую структурированную запись для каждого элемента, готовую к анализу, загрузке в базу данных или таблицу.
[ { "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 запросов бесплатно, без карты.
