Парсинг одной страницы прост. Парсинг тысяч страниц на множестве разных сайтов за один прогон, вот где ломается большинство решений: простой цикл блокируется на каждом запросе, IP-адреса банятся через несколько сотен вызовов, дубликаты URL тратят кредиты впустую, а одна медленная страница останавливает всю задачу. Проблема не в парсинге, а в пропускной способности, защите от блокировок и учёте в масштабе.
Это руководство покажет, как парсить несколько сайтов одновременно на Python так, как это работает в реальных производственных задачах. Вы будете использовать Crawling API для получения и рендеринга каждой страницы через настоящий браузер и доверенный IP, а асинхронный Crawler, для отправки тысяч URL в управляемую очередь, которая парсит их одновременно и доставляет результаты в вебхук. Мы рассмотрим постановку множества URL в очередь, управление параллельностью, дедупликацию работы и сбор строк в одном месте.
Почему единый цикл не масштабируется
Первый парсер почти всегда представляет собой один цикл: прочитать URL, получить страницу, распарсить, сохранить, повторить. Это работает для десяти страниц и ломается при десяти тысячах. Каждый запрос блокирует следующий, поэтому общее время равно сумме времени каждого запроса. Отправляйте эти запросы с одного IP, и сайт начнёт возвращать 403 и CAPTCHA через несколько сотен вызовов. И ничто в цикле не замечает, что вы уже парсили половину списка вчера, поэтому вы платите за повторное получение страниц, которые у вас уже есть.
Масштабирование по многим сайтам одновременно означает решение трёх отдельных задач. Вам нужна параллельность, чтобы медленные страницы не блокировали быстрые. Вам нужна защита от блокировок, чтобы ротация IP и реальный рендеринг браузера не допускали попадания в бан-листы. И вам нужен учёт, чтобы дубликаты URL пропускались, а готовые результаты попадали в одно хранилище. Остальная часть руководства сопоставляет каждую задачу с инструментом и показывает код.
Держите границу чёткой. Crawling API получает и рендерит одну страницу за вызов: запускает JavaScript, ротирует IP и возвращает готовый HTML. Асинхронный Crawler, это очередь поверх: вы отправляете множество URL, он парсит их одновременно, повторяет неудачные попытки и POST-ирует каждый результат в вебхук, который вы разворачиваете. Используйте API для ограниченного пакета, который вы ждёте, Crawler, для крупной задачи в режиме «отправил и собираю».
Что вы построите
Два исполняемых паттерна для списка URL, охватывающих разные сайты. Во-первых, конкурентный пакет, который прогоняет дедуплицированный набор URL через Crawling API и записывает каждый результат в JSON-файл, это подходящая форма для сотен и нескольких тысяч страниц, которые вы хотите получить по завершении прогона. Во-вторых, асинхронная отправка в Crawler для задач, уходящих в десятки тысяч, где ожидание каждой выборки больше неприемлемо. Оба используют официальный Python-клиент crawlbase.
Настройка среды
Вам нужен Python 3.8 или выше. Проверьте вашу версию, создайте виртуальное окружение, чтобы зависимости были изолированы, затем установите клиент.
python --version python -m venv scrape_env source scrape_env/bin/activate pip install crawlbase
На Windows активируйте окружение командой scrape_env\Scripts\activate вместо строки с source. Пакет crawlbase является официальным клиентом и оборачивает как Crawling API, так и асинхронный Crawler, поэтому вам не нужно вручную составлять HTTP-вызовы. Получите два токена из вашей панели управления Crawlbase после регистрации: обычный токен для статических страниц и токен JavaScript (JS) для страниц с клиентским рендерингом. Читайте их из переменных окружения, а не хардкодьте.
export CRAWLBASE_TOKEN=your_normal_token_here export CRAWLBASE_JS_TOKEN=your_js_token_here
Обычный токен получает статический HTML, работает быстрее и дешевле. JS-токен сначала рендерит страницу в настоящем браузере, что нужно для любого сайта, загружающего контент на стороне клиента. Когда вы парсите сразу много разных сайтов, вы встретите оба типа, поэтому распространённый паттерн, по умолчанию использовать JS-токен и переключаться на обычный для целей, которые вы точно знаете как статические.
Создание дедуплицированного набора URL
Перед любой выборкой очистите входные данные. Реальный список целей, собранный из карт сайтов, страниц категорий и предыдущих прогонов, полон дубликатов и устаревших записей. Дедупликация заранее, это самая дешёвая оптимизация, которую вы можете сделать, потому что запрос, который вы никогда не отправляете, ничего не стоит. Нормализуйте каждый URL и ведите учёт того, что вы уже спарсили.
import json import os from urllib.parse import urlparse, urlunparse def normalize(url): parts = urlparse(url.strip()) # Drop fragments and trailing slashes so near-duplicates collapse. path = parts.path.rstrip("/") or "/" return urlunparse((parts.scheme, parts.netloc, path, "", parts.query, "")) raw_urls = [ "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html", "https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html", "https://quotes.toscrape.com/page/1/", "https://quotes.toscrape.com/page/1/#top", # duplicate after normalizing ] targets = sorted({normalize(u) for u in raw_urls}) print(f"{len(targets)} unique URLs to crawl")
Set-comprehension сворачивает точные и почти-дубликаты URL в одной строке. Для прогона, который возобновляется в течение нескольких дней, сохраняйте спарсенный набор на диск и вычитайте его из targets в начале каждого прогона, чтобы никогда не повторно получать страницу, которая у вас уже есть. Это учётный слой в работе ещё до того, как вы потратите хоть один кредит.
Конкурентный парсинг пакета через Crawling API
Теперь получайте набор. Наивная версия, последовательный цикл, но именно последовательность не масштабируется, поэтому запускайте запросы через пул потоков. Каждый вызов Crawling API является I/O-ограниченным, ожидающим сети, это именно тот случай, который хорошо обрабатывает пул потоков. Умеренное число воркеров позволяет держать многие запросы в полёте, не перегружая при этом ни один сайт.
from concurrent.futures import ThreadPoolExecutor, as_completed from crawlbase import CrawlingAPI api = CrawlingAPI({"token": os.environ["CRAWLBASE_JS_TOKEN"]}) def fetch(url): options = {"ajax_wait": "true", "page_wait": 2000} response = api.get(url, options) status = response["headers"].get("cb_status") return { "url": url, "status": status, "html": response["body"].decode("utf-8", "ignore"), } results = [] with ThreadPoolExecutor(max_workers=10) as pool: futures = {pool.submit(fetch, u): u for u in targets} for future in as_completed(futures): url = futures[future] try: results.append(future.result()) except Exception as err: print(f"Failed {url}: {err}") print(f"Collected {len(results)} pages")
Две детали делают это решение надёжным. Заголовок cb_status (legacy pc_status) несёт оригинальный статус, который вернула цель, поэтому вы можете отличить реальный 200 от мягкого сбоя и решить, оставлять ли строку. А обёртывание future.result() в try/except означает, что один проблемный URL залогируется и продолжится вместо того, чтобы убить весь пакет. Crawling API обрабатывает рендеринг и ротацию IP за каждый вызов, поэтому ваш код управляет только параллельностью.
Один вызов получает и рендерит страницу через настоящий браузер и ротирующийся резидентный IP, поэтому пакет по многим разным сайтам остаётся незаблокированным без запуска собственного headless-парка или пула прокси. Начните с публичной страницы на бесплатном тарифе, затем масштабируйте тот же цикл до тысяч URL.
Парсинг и сохранение собранных результатов
Теперь у вас есть сырой HTML для каждой страницы в results. Парсинг варьируется в зависимости от сайта, но шаг сборки одинаков: извлеките нужные поля и запишите одну структурированную запись на страницу. Сохраняйте URL и временную метку захвата в каждой строке, чтобы вывод служил также журналом аудита того, что запускалось и когда.
from datetime import datetime, timezone from bs4 import BeautifulSoup def parse_title(html): soup = BeautifulSoup(html, "html.parser") title = soup.find("title") return title.text.strip() if title else None rows = [] for item in results: if item["status"] != "200": continue rows.append({ "url": item["url"], "title": parse_title(item["html"]), "captured_at": datetime.now(timezone.utc).isoformat(), }) with open("scraped.json", "w") as f: json.dump(rows, f, indent=2) print(f"Wrote {len(rows)} rows to scraped.json")
Для этого нужно установить beautifulsoup4 вместе с клиентом (pip install beautifulsoup4). Пропуск строк, не вернувших чистый 200, не позволяет тихим сбоям, пустому телу или странице с вызовом, попасть в ваш датасет, а это именно то тихое повреждение, которое незаметно отравляет большой краул. Для хорошо известных целей, таких как крупные ретейлеры или маркетплейсы, вы можете вообще пропустить написанный вручную парсинг и позволить Crawling API возвращать предварительно распарсенный JSON.
Масштабирование за пределы пакета с асинхронным Crawler
Пакет с пулом потоков, правильный инструмент для нескольких тысяч URL, которые вы хотите дождаться. После этого блокировать ваш процесс, пока парсятся десятки тысяч страниц, больше непрактично, и здесь на смену приходит асинхронный Crawler. Это управляемая очередь на основе push: вы отправляете URL через тот же клиент, каждый получает идентификатор запроса, система парсит их одновременно, повторяет неудачи за вас, затем POST-ирует каждую готовую страницу в вебхук на вашем сервере.
from crawlbase import CrawlingAPI crawler = CrawlingAPI({"token": os.environ["CRAWLBASE_JS_TOKEN"]}) # Push each URL to the async Crawler; results arrive at your webhook. for url in targets: response = crawler.post(url, { "callback": "https://your-app.example.com/webhook", "callback_headers": "X-Job-Id:bulk-run-01", }) body = json.loads(response["body"]) print(f"Queued {url} as request {body['rid']}")
Каждый вызов post возвращает идентификатор запроса (rid), который вы можете залогировать для отслеживания задачи. Crawler парсит очередь в фоновом режиме со своей собственной параллельностью и логикой повторов, поэтому ваш скрипт завершается в момент отправки каждого URL, а не ждёт краула. Когда страница завершена, система POST-ирует результат на ваш callback URL, а поле callback_headers позволяет тегировать прогон, чтобы принимающий обработчик знал, к какой задаче относится доставка.
Сбор доставок
Асинхронная модель инвертирует сбор: вместо вытягивания страниц вы их получаете. Ваш вебхук запускает ту же логику парсинга и сохранения из пакетной версии, меняется только триггер. Минимальный обработчик на Flask выглядит так.
from flask import Flask, request app = Flask(__name__) @app.route("/webhook", methods=["POST"]) def webhook(): rid = request.headers.get("rid") original_url = request.headers.get("original_url") html = request.get_data(as_text=True) row = { "url": original_url, "title": parse_title(html), "captured_at": datetime.now(timezone.utc).isoformat(), } with open("bulk.jsonl", "a") as f: f.write(json.dumps(row) + "\n") return "", 200
Добавление в файл JSON Lines означает, что каждая доставка, это одна самодостаточная запись, поэтому конкурентные callbacks никогда не затирают друг друга, как это было бы с единым пересериализованным JSON-массивом. Crawler доставляет оригинальный URL и идентификатор запроса в заголовках ответа, поэтому те же parse_title и форма строки из пакетной версии переходят напрямую. Именно это позволяет пайплайну расти от нескольких тысяч страниц до сотен тысяч без того, чтобы ваш процесс когда-либо сидел и ждал.
При больших объёмах вы не можете следить за краулом глазами, поэтому опирайтесь на встроенный мониторинг. Панель управления Crawlbase отслеживает объём запросов, показатели успехов и сбоев, а также использованные кредиты, а live-монитор показывает глубину очереди в реальном времени. Нарастающий рост сбоев обычно означает, что одна цель начала бросать вызовы трафику, и вы хотите поймать это в течение минут, а не после завершения прогона с половиной отсутствующих строк.
Параллельность, ограничения скорости и защита от блокировок
Больше воркеров, не всегда быстрее. Слишком высокая параллельность либо исчерпывает частоту запросов вашего плана, либо слишком нагружает один домен, провоцируя его защиту, что замедляет прогон из-за повторных попыток. Решение состоит в управлении параллельностью на уровне домена, а не глобально: десять параллельных запросов по десяти сайтам, мягко, а десять к одному сайту, агрессивно. Группируйте ваш набор URL по хосту и ограничивайте число параллельных запросов к любому из них.
Поскольку Crawling API и Crawler оба ротируют резидентные IP и рендерят через настоящий браузер на стороне сервера, самая тяжёлая часть защиты от блокировок решается за вас. Если вы предпочитаете направлять собственный клиент через ротирующийся пул, Smart AI Proxy предоставляет ту же ротацию резидентных IP как drop-in прокси-эндпоинт. В любом случае ограничивайте запросы, варьируйте цели и следите за кодами статусов, чтобы сразу откатиться, как только сайт начнёт отвечать отказами. Полный план действий находится в статье как парсить сайты без блокировок.
Ответственный парсинг
Парсинг в масштабе, это ответственность, а не только возможность. Ограничивайтесь публично доступными данными: не парсите контент за логином, платным доступом, а также любые персональные или защищённые авторским правом материалы без чёткого права на это. Читайте robots.txt и условия использования каждого сайта, соблюдайте правила доступа, которые они устанавливают. И ограничивайте скорость: расстановка запросов и ограничение параллельности на домен не позволяет попасть в бан-листы и снижает нагрузку на серверы сайта. Сдержанность, не только этичный выбор, но и операционный: задача, которая уважает лимиты, работает гораздо дольше той, которая их игнорирует.
Ключевые выводы
- Разделите проблему. Парсинг многих сайтов одновременно, это три задачи, а не одна: параллельность, защита от блокировок и учёт. Сопоставьте каждую с инструментом вместо того, чтобы втискивать их в один цикл.
- Дедуплицируйте перед выборкой. Нормализуйте URL и пропускайте уже спарсенные, потому что самый дешёвый запрос, тот, который вы никогда не отправляете.
- Используйте пул потоков для ограниченных пакетов. Вызов Crawling API является I/O-ограниченным, поэтому умеренный пул воркеров собирает сотни и тысячи страниц намного быстрее последовательного цикла.
- Используйте асинхронный Crawler в масштабе. Для десятков тысяч URL отправляйте их в очередь и получайте результаты в вебхук, чтобы параллельность, повторы и мониторинг шли в комплекте.
- Управляйте параллельностью на уровне домена. Распределяйте нагрузку по хостам и ограничивайте параллельные запросы на один сайт, чтобы оставаться незаблокированным.
- Парсите ответственно. Только публичные данные, соблюдайте robots.txt и условия использования, ограничивайте скорость, чтобы задача продолжала работать.
Часто задаваемые вопросы
Как парсить несколько сайтов одновременно на Python?
Создайте дедуплицированный набор URL, затем получайте их конкурентно, а не в последовательном цикле. Для ограниченного пакета запускайте Crawling API через ThreadPoolExecutor, чтобы медленные страницы не блокировали быстрые, и собирайте каждый результат в список, который затем записываете на диск. Для очень крупных задач отправляйте URL в асинхронный Crawler, который ставит их в очередь и парсит в фоновом режиме, доставляя каждую готовую страницу в вебхук, который вы развёртываете.
В чём разница между Crawling API и асинхронным Crawler?
Crawling API синхронен: вы отправляете один URL и ждёте отрендеренную страницу в ответе, это идеально для одного парсинга или небольшого конкурентного пакета. Асинхронный Crawler создан для масштаба: вы отправляете множество URL, он парсит их в фоновом режиме со своей параллельностью и повторами, POST-ируя каждый результат в ваш вебхук. Оба имеют одинаковый бэкенд рендеринга и защиты от блокировок, поэтому вы выбираете тот, что соответствует вашей пропускной способности.
Как избежать блокировок при парсинге многих сайтов?
Ротируйте IP и рендерите страницы через настоящий браузер, ограничивайте запросы, чтобы не перегружать ни один домен. Crawling API и Crawler обрабатывают ротацию IP и рендеринг на стороне сервера, поэтому большинство блокировок устраняется автоматически. Если вы маршрутизируете собственный клиент, используйте ротирующийся эндпоинт, такой как Smart AI Proxy, управляйте параллельностью на уровне домена и следите за кодами статусов, чтобы откатиться, когда сайт начинает бросать вызовы трафику.
Как обрабатывать дубликаты URL среди тысяч страниц?
Нормализуйте каждый URL, убирая фрагменты и завершающие слеши, затем храните их в множестве, чтобы точные и почти-дубликаты сворачивались автоматически. Для прогонов, которые возобновляются со временем, сохраняйте множество уже спарсенных URL на диск и вычитайте его из вашего целевого списка в начале каждого прогона. Этот учёт не даёт вам платить за повторное получение страниц, которые у вас уже есть.
Сколько конкурентных запросов нужно запускать?
Начните с умеренного числа, около десяти воркеров, и настраивайте исходя из частоты запросов вашего плана и того, как реагируют цели. Важна параллельность на уровне домена, а не глобальный итог: десять запросов по десяти сайтам, мягко, а десять к одному сайту, агрессивно. Группируйте URL по хосту и ограничивайте число параллельных запросов к любому одному, чтобы оставаться незаблокированным.
Законно ли парсить тысячи сайтов?
Парсинг публично доступных данных в целом допустим, но законность зависит от условий использования каждого сайта, авторского права и законов о защите данных, таких как GDPR и CCPA. Оставайтесь на публичных данных, избегайте контента за логинами или платным доступом и любого персонального или защищённого авторским правом материала, следуйте robots.txt и ограничивайте скорость. В случае сомнений относительно конкретной цели изучите её условия и получите юридическую консультацию перед запуском крупной задачи против неё.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.
