Scraping과 Crawling, 목적에 따라 갈리는 데이터 수집 아키텍처
웹 스크래핑과 크롤링의 구조적 차이, 파이프라인 구성 요소, 확장성·거버넌스 관점을 비교하고 Python 레퍼런스 구현으로 실무 적용 기준을 정리한다.
2026-08-12 · 최초 발행 2025-12-09
웹에서 데이터를 가져온다는 행위는 하나로 뭉뚱그려지곤 하지만, Scraping과 Crawling은 목적부터 다른 전략이다. Scraping(스크래핑)은 특정 페이지 또는 API로부터 필요한 필드만 파싱해 구조화 데이터로 추출하는 출력 중심 개념이고, Crawling(크롤링)은 링크를 따라가며 웹 리소스를 대규모로 수집·저장하는 탐색·수집 중심 개념이다. 데이터 추출과 데이터 다운로드, 제한된 규모와 대규모 지향, 선택적 중복 제거와 필수 중복 제거, 크롤 에이전트+파서 구성과 크롤 에이전트 중심 구성(다운로드·스케줄링·중복제거·저장)이 이 둘을 가르는 핵심 대비다.
파이프라인별로 요구하는 구성 요소는 다르다
Scraping 파이프라인은 크롤 에이전트(요청/세션/프록시)와 파서(HTML/XML/JSON, CSS Selector/XPath/정규표현식), 정규화 단계로 구성되며 타깃 스키마 정의와 필드 유효성 검증을 포함한다. 셀렉터 유지보수, 콘텐츠 변동 감지, 파싱 예외 처리가 품질 메커니즘의 축이고, 중복 제거는 보통 선택 사항이라 레코드 키 기반 Upsert로 해결할 수 있다.
Crawling 파이프라인은 URL Frontier(큐), 중복 제거(URL/콘텐츠 해시), 동시성 스케줄러, 저장소(원문/메타데이터)로 구성된다. 링크 추출과 politeness(robots.txt, rate limit) 준수가 필수이며, 재시도·백오프, 실패 큐, 콘텐츠 무결성(크기/해시) 검증이 품질을 좌우한다. 중복 제거는 저장 비용과 대역폭 관점에서 선택이 아니라 필수다.
확장·성능 관점에서 보면 Scraping은 타깃 수가 제한적일 때 효율이 높고 필드 정확도가 최우선이므로 셀렉터 취약성에 대비한 버전 관리가 필요하다. Crawling은 광범위 도메인·딥 링크 수집에 최적화되어 있고 고동시성·분산 환경을 전제로 하므로, Frontier 품질과 중복 제거 정확도가 곧 비용 절감의 핵심이 된다.
거버넌스 측면에서는 둘 다 robots.txt/TOS 준수, 개인정보·저작권·접근 속도 제한을 고려해야 하고 헤더·세션 관리의 투명성이 필요하다. 다만 Scraping은 스키마·목적이 명확해 합법성 판단이 상대적으로 쉬운 반면, Crawling은 포괄적 수집 특성상 차단·법적 리스크와 IP 평판 관리가 별도로 필요하다.
어디에 쓰는가
Scraping은 커머스 가격·재고 모니터링, 딜 탐색, 리드 수집과 CRM 유입 자동화에 쓰이고 공공·기업 공지 파싱, 뉴스·소셜 텍스트 정제, 실시간 대시보드 데이터 공급에도 활용된다. Crawling은 검색 인덱스·링크 그래프 구축, 데이터 레이크·학습 코퍼스 적재에 쓰이며 미러링·아카이빙, 경쟁사·시장 전수 조사, 콘텐츠 중복 탐지·저작권 모니터링에도 사용된다.
절감 효과와 처리량
중복 제거는 도메인·주기에 따라 다르지만 저장·대역폭을 2060% 절감할 수 있고, 스케줄러의 백오프는 실패 재시도 비용을 1030% 추가로 절감할 수 있다. Scraping의 스키마 검증은 필드 결측률을 30% 이상 줄이고, 파서 회귀 테스트를 갖추면 파싱 실패율을 1% 미만으로 유지할 수 있다. 분산 Frontier와 비동기 요청을 결합하면 노드당 QPS가 100~1,000 수준까지 나올 수 있는데, 네트워크와 대상 정책에 따라 변동 폭이 크다.
흐름을 하나의 다이어그램으로
방식별 지표 비교
| 지표 | Scraping(스크래핑) | Crawling(크롤링) |
|---|---|---|
| 성능 | 파싱 비용 포함, 타깃 제한 시 고효율 | I/O 중심, 대역폭·동시성 최적화로 고처리량 |
| 확장성 | 타깃/스키마 증가 시 유지보수 비용 증가 | 분산 Frontier·샤딩으로 선형 확장 용이 |
| 일관성 | 스키마 검증으로 필드 일관성 우수 | 원문 위주, 후처리 전까지 스키마 일관성 낮음 |
| 안정성 | 셀렉터 변경에 민감, 회귀 테스트로 완화 | 차단/429 대응 필요, 백오프·재시도로 안정화 |
| 운영 편의 | 소수 타깃 운영 용이, 배포 주기 짧음 | 큐·중복·스토리지·모니터링 등 운영 복잡도 높음 |
레퍼런스 구현
전제조건은 Python 3.11, 네트워크 접근 가능, 합법적·윤리적 사용이다.
Scraping: 단일 페이지에서 구조화 추출
# env: python3.11, pip install requests beautifulsoup4
import requests
from bs4 import BeautifulSoup
URL = "https://example.com/products/123"
headers = {"User-Agent": "scraper/1.0 (+contact@example.com)"}
resp = requests.get(URL, headers=headers, timeout=10)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")
data = {
"title": soup.select_one("h1.product-title").get_text(strip=True),
"price": soup.select_one(".price .amount").get_text(strip=True),
"sku": soup.select_one("meta[itemprop='sku']")["content"],
}
# 스키마 검증·정규화 예시
assert data["title"] and data["sku"]
print(data)
이 구현은 크롤 에이전트+파서 구성을 그대로 따른다. 중복 제거가 필요 없고 Upsert 키로 충돌을 처리할 수 있다.
Crawling: 소규모 비동기 크롤러(중복 제거·robots 지원)
# env: python3.11, pip install aiohttp
import asyncio, aiohttp, hashlib, urllib.parse
from urllib import robotparser
SEEDS = ["https://example.com/"]
CONCURRENCY = 10
TIMEOUT = 10
seen_urls = set()
robots_cache = {}
def canonicalize(url: str) -> str:
u = urllib.parse.urlparse(url)
# 스킴/호스트 소문자, 프래그먼트 제거
return urllib.parse.urlunparse((u.scheme.lower(), u.netloc.lower(), u.path, "", u.query, ""))
async def get_robots(session, base):
if base in robots_cache:
return robots_cache[base]
rp = robotparser.RobotFileParser()
robots_url = urllib.parse.urljoin(base, "/robots.txt")
try:
async with session.get(robots_url, timeout=TIMEOUT) as r:
text = await r.text(errors="ignore")
rp.parse(text.splitlines())
except Exception:
rp.disallow_all = False
robots_cache[base] = rp
return rp
async def fetch(session, url):
async with session.get(url, timeout=TIMEOUT) as r:
if r.status != 200:
return None, None
text = await r.text(errors="ignore")
return text, str(r.url)
def extract_links(base_url, html):
# 매우 단순한 링크 추출(실무는 파서 사용 권장)
out = set()
for token in html.split('"'):
if token.startswith("http"):
out.add(token)
elif token.startswith("/"):
out.add(urllib.parse.urljoin(base_url, token))
return out
async def crawl():
q = asyncio.Queue()
for s in SEEDS:
await q.put(s)
async with aiohttp.ClientSession(headers={"User-Agent": "crawler/1.0"}) as session:
sem = asyncio.Semaphore(CONCURRENCY)
async def worker():
while True:
url = await q.get()
try:
cu = canonicalize(url)
if cu in seen_urls:
continue
seen_urls.add(cu)
base = f"{urllib.parse.urlparse(url).scheme}://{urllib.parse.urlparse(url).netloc}"
rp = await get_robots(session, base)
if not rp.can_fetch("crawler/1.0", url):
continue
async with sem:
html, final_url = await fetch(session, url)
if not html:
continue
# 콘텐츠 해시 기반 중복 제거(선택)
content_hash = hashlib.sha256(html.encode("utf-8", "ignore")).hexdigest()
# TODO: 해시 저장소로 글로벌 중복 제거
# 링크 추출 → 큐 삽입
for link in extract_links(final_url, html):
await q.put(link)
finally:
q.task_done()
workers = [asyncio.create_task(worker()) for _ in range(CONCURRENCY)]
await asyncio.wait_for(q.join(), timeout=30) # 데모용 제한
for w in workers:
w.cancel()
if __name__ == "__main__":
asyncio.run(crawl())
이 구현은 크롤 에이전트 중심 구성으로, Frontier 큐와 URL·콘텐츠 중복 제거를 적용했다. robots.txt 준수, 동시성·타임아웃·간단한 링크 추출도 함께 넣었다. 실무에서는 백오프, 실패 큐, 영속 Frontier, 스토리지, 성능 계측을 추가해야 한다.
무엇을 기준으로 선택하는가
Scraping은 필드 정확도와 구조화를 최우선으로 하는 전략이라 크롤 에이전트+파서 구성에 중복 제거를 선택적으로 적용하는 편이 맞다. Crawling은 포괄적 수집과 확장성을 최우선으로 하는 전략이라 Frontier·중복 제거·정책 준수·분산 운영이 핵심이 된다. 실무에서는 목적(추출 vs 다운로드), 규모(제한적 vs 대규모), 중복 제거 요구, 구성 요소(crawl agent + parser vs crawl agent)를 기준으로 아키텍처를 결정하는 것을 권장한다.