대규모 웹 스크래핑 아키텍처: 실전 가이드

몇 백 페이지는 스크립트입니다. 수백만 페이지는 분산 시스템입니다. 여기에는 큐, 비동기 작업자, 프록시 계층화, 재시도, 중복 제거 및 데이터 품질 검사를 포함하는 아키텍처가 있으며, 작동하는 코드가 포함되어 있습니다.

몇 백 페이지를 스크래핑하는 것은 스크립트입니다. 수백만 페이지를 스크래핑하는 것은 분산 시스템입니다. 목표 수가 "내 노트북에서 밤새 실행"에서 "이번 주에 끝내야 하는데 녹지 않아야 함"으로 넘어가면, 어려운 부분은 파싱이 아니라 그 주변의 모든 것이 됩니다 — 작업을 큐에 넣고, 분산하고, 블록을 피하고, 실패를 재시도하고, 결과를 저장하고 확인하는 방법입니다. 이것은 아키텍처로서의 대규모 웹 스크래핑이며, 각 단계에서 작동하는 코드가 포함되어 있습니다.

확장은 더 큰 루프가 아니라 동시성입니다

한 가지 수치로 구체화됩니다. 20,000개의 목록 페이지와 각 20개의 항목이 있는 카테고리를 가져오면 — 400,000 페이지를 가져와야 합니다. 페이지당 현실적인 2.5초에서, 엄격히 순차적인 실행은 약 1,000,000초, 대략 11.5일 동안 페이지 로드를 기다린 후에야 단일 필드를 파싱할 수 있습니다. 200페이지를 병렬로 처리하면 그 11.5일이 한 시간의 벽 시계 시간으로 축소됩니다. 확장에서의 제약은 시간이며, 동시성은 그것을 되찾는 방법입니다. 아키텍처의 다른 모든 것은 그 동시성을 지속 가능하게 만드는 데 존재합니다.

400,000 페이지가 순차적으로 11.5일 걸리는 것과 200개 병렬로 약 한 시간 걸리는 통계 다이어그램, 백만 건당 1%에서 10,000 실패
동시성은 11.5일의 실행을 한 시간으로 줄입니다 — 그러나 백만 요청에서 1%의 실패율은 10,000개의 죽은 페이지입니다.

아키텍처 한눈에 보기

수백만 페이지를 견디는 스크래퍼는 몇 가지 명명된 부분이 있는 작은 분산 시스템으로, 각각은 대량에서만 나타나는 문제를 해결합니다:

큐 먼저: 발견을 가져오기에서 분리

가장 중요한 구조적 결정은 "무엇을 스크래핑할지"와 "스크래핑을 수행하는 것" 사이에 큐를 두는 것입니다. 프로듀서는 URL을 열거하고, 작업자 풀은 그것들을 소모합니다. 어느 쪽도 다른 쪽이 얼마나 빨리 실행되는지 모르며, 프로듀서를 건드리지 않고 작업자를 추가할 수 있습니다. 파이썬에서는 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에서 수십만 개의 요청을 보내면 속도 제한을 받고, 도전받고, 차단됩니다. 해결책은 여러 주소로 회전하는 것입니다. 계층화하십시오: 관대한 대상과 API에는 저렴한 데이터 센터 IP를 사용하고, 회전하는 주거용 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 세트 또는 Bloom 필터)를 유지하십시오:

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로 오프로드

반드시 필요할 때만 렌더링

헤드리스 브라우저는 파이프라인에서 가장 비싼 작업입니다 — CPU, 메모리, 페이지당 몇 초, 백만 페이지에서 모든 것을 지배합니다. 많은 사이트는 여전히 초기 HTML이나 JSON 엔드포인트에 데이터를 전송합니다; 단순한 가져오기와 파서는 비용이 한 자릿수 낮습니다. 저렴한 경로를 먼저 시도하고, 필드가 있는지 확인하고, JavaScript가 필요한 페이지에만 렌더링을 확장하십시오. 우리의 렌더링 비용 대 HTTP 분석은 그 차이에 실제 숫자를 제공합니다.

어려운 레이어를 구축할 것인가 구매할 것인가

위의 모든 것은 구축 가능합니다, 그래서 솔직한 질문은 어떤 부분이 귀하의 엔지니어링 시간을 가치 있게 하는가입니다. 데이터 모델, 파싱 로직, 품질 검사 및 저장소 스키마는 귀하의 프로젝트에 특화되어 있습니다 — 오직 귀하만이 그것들을 잘 구축할 수 있습니다. 프록시 풀, 안티봇 처리, 헤드리스 렌더링 플릿, 재시도 및 전달 큐는 구축하기 비싸고 목표가 발전함에 따라 건강을 유지하는 것이 고통스러운 일반 인프라입니다. 이것이 관리되는 Scraper API가 위치하는 선입니다: 모든 사람에게 동일한 부분을 임대하십시오. 우리의 구축 대 구매 분석은 유지보수 세금을 자세히 다루고, 프로덕션 소프트웨어로 스크래퍼 실행은 전체를 관찰 가능하게 유지하는 것을 다룹니다.

자주 묻는 질문

수백만 페이지를 어떻게 스크래핑합니까?

더 큰 루프가 아니라 동시성으로. URL 발견과 가져오기 사이에 큐를 두고, 비동기 또는 분산 작업자 풀로 그것을 소모하고, IP를 회전시켜 블록을 피하고, 백오프로 일시적 실패를 재시도하고, URL을 중복 제거하고, 저장소에 일괄로 씁니다. 순차적인 백만 페이지 실행은 며칠이 걸립니다; 동시 작업자 풀을 통해 동일한 작업은 몇 시간 내에 완료됩니다.

대규모 웹 스크래핑에 가장 적합한 아키텍처는 무엇입니까?

큐와 작업자 파이프라인: 프로듀서는 큐(Redis, RabbitMQ 또는 Kafka)에 URL을 열거하고, 작업자는 회전 프록시 레이어를 통해 동시에 가져오고, JavaScript가 필요한 페이지만 렌더링하고, 실패를 데드레터 큐로 재시도하고, 본 세트로 중복 제거하고, 원본 및 파싱된 데이터를 별도로 저장합니다. 데이터 품질 검사와 함께 모니터링으로 감싸서 변동이 일찍 표면화되도록 하십시오.

병렬로 몇 개의 요청을 실행할 수 있습니까?

대상과 귀하의 출구에 따라 다르며, 고정된 숫자가 아닙니다. 스크래핑은 IO에 의존하므로, 적당한 박스는 수백 개의 진행 중인 요청을 처리할 수 있습니다. 약 50의 동시성으로 시작하고, 오류율을 관찰하고, 실패가 증가할 때까지 올리십시오 — 그것이 귀하의 한계입니다. 한 기계의 한계를 넘어서면, 단일 노드를 더 강하게 밀기보다는 분산 작업자를 추가하십시오.

규모에서 실패를 어떻게 처리합니까?

실패를 가정하십시오 — 백만 요청에서 1%의 오류율이라도 10,000개의 죽은 페이지입니다. 지수 백오프와 지터로 일시적 오류(타임아웃, 503)를 재시도하고, 시도를 제한하고, 지속적인 실패를 데드레터 큐로 이동하여 실행을 차단하지 마십시오. 단단한 404는 재시도하지 마십시오. 수집 순서를 무작위화하여 동일한 페이지에서 매번 실패하지 않도록 실패를 분산시키십시오.

확장은 대부분 구축하기 재미없는 부분입니다: IP 회전, 안티봇, 헤드리스 렌더링, 큐 및 재시도. 데이터에 특화된 부분을 소유하십시오 — 모델, 파서, 품질 검사 — 그리고 모든 사람에게 동일한 고통인 일반 인프라를 임대하십시오. 큐와 동시성을 먼저 맞추십시오; 나머지는 백만 개의 실제 페이지와 접촉하여 그 동시성을 지속 가능하게 만드는 것입니다.

회전하는 주거용 IP로 가져오기 레이어를 강화하십시오