Архитектура крупномасштабного веб-скрейпинга: Полевое руководство
Несколько сотен страниц — это скрипт. Миллионы — это распределенная система. Вот архитектура, которая поможет вам достичь этого — очереди, асинхронные рабочие, уровни прокси, повторные попытки, дедупликация и проверки качества данных, с работающим кодом.
Скрейпинг нескольких сотен страниц — это скрипт. Скрейпинг миллионов — это распределенная система. Как только количество целей переходит от "запускается за ночь на моем ноутбуке" к "нужно завершить на этой неделе, не перегреваясь", сложность перестает быть в парсинге и становится во всем вокруг — как вы ставите задачи в очередь, распределяете их, избегаете блокировок, повторяете неудачные попытки и сохраняете и проверяете результат. Это крупномасштабный веб-скрейпинг как архитектура, а не фрагмент кода, с работающим кодом на каждом этапе.
Масштаб — это параллелизм, а не больший цикл
Одна цифра делает это конкретным. Возьмите категорию с 20,000 страницами объявлений, по 20 элементов каждая — 400,000 страниц для загрузки. При реалистичных 2.5 секундах на страницу строго последовательный запуск займет около 1,000,000 секунд, примерно 11.5 дней ожидания загрузки страниц до парсинга первого поля. Запустите 200 страниц параллельно, и эти 11.5 дней сокращаются до часа реального времени. Время — это ограничение на масштабе, и параллелизм — это способ его вернуть. Все остальное в архитектуре существует, чтобы сделать этот параллелизм жизнеспособным.

Архитектура с первого взгляда
Скрейпер, который выдерживает миллионы страниц, — это небольшая распределенная система с несколькими именованными частями, каждая из которых решает проблему, которая возникает только при большом объеме:
- Очередь хранит URL-адреса, которые еще предстоит загрузить, и отделяет обнаружение от работы.
- Асинхронные или распределенные рабочие извлекают из очереди и загружают параллельно — здесь и находятся экономия времени.
- Уровень прокси и антиботов меняет IP и представляет трафик реального браузера, чтобы ни один адрес не превышал лимит.
- Рендеринг, только когда необходимо, потому что безголовый браузер — самая дорогая вещь в конвейере.
- Повторные попытки с увеличением интервала, дедупликация, хранение и мониторинг плюс проверки качества данных завершают его.
Сначала очередь: отделите обнаружение от загрузки
Самое важное структурное решение — это поставить очередь между "что скрейпить" и "выполнением скрейпинга". Производитель перечисляет URL-адреса; пул рабочих их обрабатывает. Ни одна из сторон не знает, насколько быстро работает другая, и вы добавляете рабочих, не касаясь производителя. В Python это Celery или RQ на базе Redis; в Node — BullMQ; на большом масштабе — RabbitMQ или Kafka. Шаблон в одном файле:
import asyncio, aiohttp
CONCURRENCY = 50
queue = asyncio.Queue()
async def worker(session):
while True:
url = await queue.get()
try:
async with session.get(url, timeout=20) as resp:
await handle(url, await resp.text(), resp.status)
except Exception as err:
await on_failure(url, err)
finally:
queue.task_done()
async def run(urls):
for u in urls:
queue.put_nowait(u)
async with aiohttp.ClientSession() as session:
tasks = [asyncio.create_task(worker(session)) for _ in range(CONCURRENCY)]
await queue.join()
for t in tasks:
t.cancel()
Ручка, которая имеет значение, — это CONCURRENCY. Слишком низкое значение тратит впустую параллелизм, который делает масштаб возможным; слишком высокое перегружает как цель, так и ваш собственный исходящий трафик. Вы находите правильное значение, наблюдая за ростом уровня ошибок — именно поэтому мониторинг является важной частью системы, а не последующей мыслью.

Уровень прокси ломается первым
При низком объеме вы едва замечаете защиту от ботов; на масштабе они ломают запуск первыми. Отправьте несколько сотен тысяч запросов с одного IP, и вы получите ограничение скорости, затем вызов, затем блокировку. Исправление — это ротация по многим адресам. Уровень: дешевые IP-адреса дата-центра для мягких целей и API, ротация резидентных IP для сложных коммерческих целей, которые ожидают трафик реальных пользователей. Но одной ротации недостаточно — современные защиты также считывают отпечатки TLS и порядок заголовков, поэтому трафик должен выглядеть как браузер, а не просто исходить с нового IP.
Повторные попытки: сбой — это нормальное состояние
При миллионе запросов 1% временный уровень сбоев — это 10,000 неудачных страниц. Сбой не является краевым случаем при таком объеме — это рутина, и конвейер должен рассматривать неудачную загрузку как норму, а не как фатальную ошибку. Повторите с экспоненциальным увеличением интервала и ограничением, затем переместите URL в очередь неудачных сообщений вместо блокировки запуска. Прочтите почему это не удалось: тайм-аут или 503 стоит повторить, жесткий 404 — нет.
import asyncio, random
async def fetch_with_retry(session, url, tries=4):
for attempt in range(tries):
try:
async with session.get(url, timeout=20) as r:
if r.status == 404:
return None # don't retry a hard 404
if r.status < 400:
return await r.text()
except Exception:
pass
await asyncio.sleep(2 ** attempt + random.random()) # backoff + jitter
await dead_letter(url) # give up after the cap
return None
Дедупликация: не скрейпите одну и ту же страницу дважды
Обнаружение на масштабе постоянно производит дубликаты — один и тот же продукт доступен с трех путей, параметры отслеживания, которые делают одну страницу похожей на десять. Нормализуйте URL-адреса перед их добавлением в очередь, затем ведите учет уже просмотренных (множество Redis или фильтр Блума, когда множество достигает сотен миллионов):
from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode
seen = set()
def normalize(url):
s = urlsplit(url.lower())
q = [(k, v) for k, v in parse_qsl(s.query) if not k.startswith("utm_")]
return urlunsplit((s.scheme, s.netloc, s.path.rstrip("/"), urlencode(sorted(q)), ""))
def enqueue(url):
key = normalize(url)
if key not in seen:
seen.add(key)
queue.put_nowait(key)
Хранение и качество данных
Две привычки, специфичные для масштаба: записывайте пакетами, чтобы хранение не стало вашим узким местом, и отделяйте сырые данные от обработанных, чтобы вы могли перепарсить без повторного скрейпинга, когда селекторы изменяются. Затем добавьте ту часть мониторинга, которую большинство команд пропускает — проверки качества данных. Запуск может сообщить о 100% успехе HTTP и все равно выдать мусор, если макет изменился и ваши селекторы теперь не соответствуют ничему. Утверждайте, что обязательные поля не пусты, а значения разумны:
def validate(row):
assert row.get("title"), "empty title — selector may have drifted"
price = row.get("price")
assert isinstance(price, (int, float)) and 0 < price < 1_000_000, "bad price"
return row
# fail loud on page 5,000, not silently after 5,000,000 empty rows
Перенесите прокси, антиботы и рендеринг в один API
Рендерьте только когда необходимо
Безголовый браузер — самая дорогая операция в конвейере — ЦП, память и секунды на страницу, которые доминируют над всем при миллионе страниц. Многие сайты все еще отправляют данные в начальном HTML или на JSON-эндпоинте; простая загрузка плюс парсер на порядок дешевле. Попробуйте сначала дешевый путь, подтвердите, что поля присутствуют, и переходите к рендерингу только для страниц, которые в этом нуждаются. Наш анализ стоимости рендеринга против HTTP дает реальные цифры на этот разрыв.
Строить или покупать сложные уровни
Все вышеперечисленное можно построить, поэтому честный вопрос — какие части заслуживают вашего инженерного времени. Модель данных, логика парсинга, проверки качества и схема хранения специфичны для вашего проекта — только вы можете построить их хорошо. Пул прокси, обработка антиботов, флот безголового рендеринга и очередь повторных попыток и доставки — это общая инфраструктура, которую дорого строить и сложно поддерживать в здоровом состоянии по мере изменения целей. Это линия, на которой стоит управляемый Scraper API: арендуйте части, которые одинаковы для всех. Наш анализ "строить или покупать" подробно рассматривает налог на обслуживание, а запуск скрейперов как производственного программного обеспечения охватывает поддержание всей системы в наблюдаемом состоянии.
Часто задаваемые вопросы
Как скрейпить миллионы страниц?
С помощью параллелизма, а не большего цикла. Поставьте очередь между обнаружением URL и загрузкой, обрабатывайте её пулом асинхронных или распределенных рабочих, меняйте IP, чтобы избежать блокировок, повторяйте временные сбои с увеличением интервала, дедуплицируйте URL и записывайте данные в хранилище пакетами. Последовательный запуск миллиона страниц занимает дни; та же работа в пуле параллельных рабочих завершается за часы.
Какая архитектура лучше всего подходит для крупномасштабного веб-скрейпинга?
Конвейер "очередь и рабочие": производитель перечисляет URL в очередь (Redis, RabbitMQ или Kafka), рабочие загружают параллельно через ротационный уровень прокси, рендерят только страницы, которым нужен JavaScript, повторяют сбои в очередь неудачных сообщений, дедуплицируют с помощью множества просмотренных и хранят сырые и обработанные данные отдельно. Оберните это в мониторинг с проверками качества данных, чтобы отклонения проявлялись рано.
Сколько запросов можно запускать параллельно?
Это зависит от цели и вашего исходящего трафика, а не от фиксированного числа. Скрейпинг ограничен вводом-выводом, поэтому скромная машина может обрабатывать многие сотни запросов в процессе. Начните с параллелизма около 50, наблюдайте за уровнем ошибок и увеличивайте его, пока не начнут расти сбои — это ваш потолок. За пределами ограничений одной машины добавляйте распределенных рабочих, а не нагружайте один узел сильнее.
Как обрабатывать сбои на масштабе?
Предполагайте сбои — при миллионе запросов даже 1% уровень ошибок — это 10,000 неудачных страниц. Повторяйте временные ошибки (тайм-ауты, 503) с экспоненциальным увеличением интервала и джиттером, ограничивайте попытки и перемещайте постоянные сбои в очередь неудачных сообщений вместо блокировки запуска. Не повторяйте жесткие 404. Рандомизация порядка сбора также распределяет сбои, чтобы вы не сталкивались с одинаковыми страницами при каждом запуске.
Масштаб — это в основном те части, которые неинтересно строить: ротация IP, антиботы, безголовый рендеринг, очереди и повторные попытки. Владейте частями, специфичными для ваших данных — моделью, парсерами, проверками качества — и арендуйте общую инфраструктуру, которая одинаково сложна для всех. Сначала правильно настройте очередь и параллелизм; все остальное — это обеспечение выживания этого параллелизма при контакте с миллионом реальных страниц.