Scrapling 완전 정복 가이드 | Claude Code와 n8n 연동까지
웹 스크래핑(Web Scraping)을 처음 시작하는 분들이나 기존 도구의 한계에 부딪힌 개발자들에게 새로운 대안이 등장했습니다. Scrapling은 2024년 Karim Shoair(D4Vinci)가 공개한 적응형(Adaptive) 웹 스크래핑 프레임워크로, 단순한 HTTP 요청부터 Cloudflare 우회, 쿠팡과 같은 강력한 봇 방어 시스템까지 단 하나의 라이브러리로 처리할 수 있습니다. 이 글에서는 Scrapling의 설치부터 실전 활용, 기존 도구와의 비교, 그리고 Claude Code와 n8n 워크플로우 자동화에 활용하는 방법까지 초보자도 이해할 수 있도록 처음부터 끝까지 상세하게 안내합니다.

목 차
1. Scrapling이란 무엇인가?
Scrapling은 Python으로 작성된 적응형(Adaptive) 웹 스크래핑 프레임워크입니다.
일반적인 웹 스크래핑 라이브러리와 달리, Scrapling은 웹사이트의 구조가 변경되어도 스크래퍼(Scraper)가 자동으로 해당 요소를 재탐지하는 지능형 메커니즘을 내장하고 있습니다. 이를 자기치유 스파이더(Self-healing Spider)라고 부르며, 웹사이트 리디자인 후에도 스크립트를 수정하지 않고 그대로 사용할 수 있다는 큰 장점이 있습니다.
Scrapling의 창시자인 Karim Shoair는 수년간 수백 개의 웹사이트를 수동으로 스크래핑하다가 같은 문제가 반복되는 데 지쳐 이 라이브러리를 개발했습니다. 특히 Akamai, Cloudflare Turnstile, DataDome 같은 고도화된 봇 방어 시스템을 우회하는 기능을 기본으로 탑재하여, 기존 도구들이 막히는 환경에서도 안정적으로 작동합니다. 현재 버전 기준으로 92%의 테스트 커버리지를 유지하며, 수백 명의 웹 스크래퍼들이 매일 실전에서 사용하는 검증된 라이브러리입니다.
Scrapling의 핵심 특징 요약
| 특징 | 설명 |
| 적응형 파싱 | 웹사이트 구조 변경 시 요소 자동 재탐지 (SQLite 기반) |
| 다중 Fetcher | HTTP 요청, 브라우저 자동화, 스텔스 모드 3단계 지원 |
| 반봇 우회 | Cloudflare Turnstile 기본 내장 우회 |
| Spider 프레임워크 | Scrapy 스타일의 동시 크롤링 및 프록시 회전 |
| MCP 서버 | Claude, Cursor 등 AI 도구와 직접 연동 |
| 빠른 성능 | BeautifulSoup 대비 400~600배 빠른 파싱 속도 |
| 비동기 지원 | 모든 Fetcher에 async/await 완전 지원 |
| Docker 이미지 | 모든 브라우저 포함된 공식 Docker 이미지 제공 |
2. 기존 웹 스크래핑 도구와의 비교
웹 스크래핑 도구의 선택은 작업의 복잡도와 대상 사이트의 특성에 따라 달라집니다. Scrapling이 기존 도구들과 어떻게 다른지 구체적으로 비교해보겠습니다.
BeautifulSoup vs Scrapling
BeautifulSoup은 HTML 파싱의 대표 라이브러리로, 단순하고 직관적인 API 덕분에 많은 초보자들이 첫 번째 스크래핑 도구로 선택합니다. 그러나 BeautifulSoup은 순수 파싱 엔진으로, 웹 페이지를 직접 가져오는 기능이 없어 Requests 라이브러리와 함께 사용해야 합니다. 또한 웹사이트 구조가 바뀌면 스크립트가 즉시 고장나며, 동적 JavaScript 렌더링을 지원하지 않습니다. Scrapling은 BeautifulSoup과 유사한 API를 제공하면서도 파싱 속도가 400~600배 빠르고, 자체 Fetcher와 적응형 요소 추적 기능을 모두 포함합니다.
Requests + BeautifulSoup 조합 vs Scrapling
많은 개발자들이 requests 라이브러리로 페이지를 가져오고 BeautifulSoup으로 파싱하는 조합을 사용합니다. 이 방식은 정적 웹사이트에는 잘 작동하지만, JavaScript로 렌더링되는 동적 페이지에서는 빈 HTML만 반환됩니다. 또한 User-Agent, TLS 핑거프린트, 쿠키 관리 등을 수동으로 처리해야 하므로 반봇 시스템을 우회하기 매우 어렵습니다. Scrapling의 Fetcher 클래스는 이 모든 작업을 자동으로 처리하며, TLS 지문 모방, 실제 브라우저 헤더 설정까지 내장되어 있습니다.
Selenium vs Scrapling
Selenium은 실제 브라우저를 자동화하는 도구로, 동적 사이트 처리에는 강하지만 속도가 매우 느리고 메모리 사용량이 높습니다. Scrapling의 DynamicFetcher는 Playwright 기반으로 Selenium보다 훨씬 빠르며, StealthyFetcher는 수정된 Firefox(Camoufox)를 사용하여 브라우저 자동화 감지 자체를 회피합니다. 또한 Selenium은 파싱을 위해 별도의 라이브러리가 필요하지만, Scrapling은 페이지 가져오기와 파싱을 하나의 통합된 API로 처리합니다.
Scrapy vs Scrapling
Scrapy는 대규모 크롤링을 위한 풀프레임워크(Full Framework)로, 강력하지만 학습 곡선이 가파르고 설정이 복잡합니다. 동적 페이지 처리를 위해 scrapy-playwright 등의 플러그인이 필요하며, 반봇 우회를 위해서는 scrapy-stealth 같은 추가 미들웨어가 필요합니다. Scrapling은 Scrapy와 유사한 Spider API를 제공하면서도 이러한 추가 플러그인 없이 적응형 파싱, 브라우저 자동화, 반봇 우회를 모두 기본으로 지원합니다.
Playwright/Puppeteer vs Scrapling
Playwright와 Puppeteer는 브라우저 자동화 도구로, 복잡한 상호작용이 필요한 스크래핑에 활용됩니다. 그러나 이 도구들은 봇 감지 시스템에 상당히 취약하며, playwright-stealth 같은 추가 패치가 필요합니다. Scrapling의 StealthyFetcher는 Camoufox라는 수정된 Firefox 브라우저를 사용하여 WebGL 비활성화, 캔버스 핑거프린팅 방지, 실제 브라우저 지문 주입 등을 통해 봇 감지 시스템을 근본적으로 우회합니다.
종합 비교표
| 기능 | Requests+BS4 | Scrapy | Selenium | Playwright | Scrapling |
| 정적 페이지 처리 | [v] | [v] | [v] | [v] | [v] |
| 동적 JS 렌더링 | [!] 불가 | 플러그인 필요 | [v] | [v] | [v] |
| 반봇 우회 | [!] 약함 | 미들웨어 필요 | [!] 약함 | 패치 필요 | [v] 내장 |
| 적응형 파싱 | [!] 없음 | [!] 없음 | [!] 없음 | [!] 없음 | [v] 내장 |
| 파싱 속도 | 보통 | 빠름 | 느림 | 보통 | 매우 빠름 |
| 메모리 효율 | 좋음 | 좋음 | 나쁨 | 보통 | 좋음 |
| 학습 난이도 | 쉬움 | 어려움 | 보통 | 보통 | 쉬움~보통 |
| AI(MCP) 연동 | [!] 없음 | [!] 없음 | [!] 없음 | [!] 없음 | [v] 내장 |
| Spider 프레임워크 | [!] 없음 | [v] 강력 | [!] 없음 | [!] 없음 | [v] 내장 |
3. Scrapling 설치 및 환경 구성
Scrapling은 모듈식(Modular) 설치 방식을 채택하고 있어, 필요한 기능에 따라 선택적으로 설치할 수 있습니다. Python 3.10 이상이 필요하며, 각 설치 옵션을 단계별로 살펴보겠습니다.
사전 준비 | Python 버전 확인
설치 전에 Python 버전을 반드시 확인해야 합니다. Scrapling은 Python 3.10 이상을 요구합니다.
# Python 버전 확인
python --version
# 또는
python3 --version
버전이 3.10 미만이라면 공식 Python 다운로드 페이지(python.org)에서 최신 버전을 설치하거나, pyenv를 이용하여 버전을 관리하는 것을 권장합니다.
가상 환경(Virtual Environment) 설정
여러 Python 프로젝트 간의 패키지 충돌을 방지하기 위해 가상 환경 사용을 강력히 권장합니다.
# 가상 환경 생성
python -m venv scrapling-env
# 가상 환경 활성화 (macOS/Linux)
source scrapling-env/bin/activate
# 가상 환경 활성화 (Windows)
scrapling-env\Scripts\activate
가상 환경이 활성화되면 터미널 프롬프트 앞에 (scrapling-env)와 같은 표시가 나타납니다. 이 상태에서 설치하면 해당 환경에만 패키지가 설치됩니다.
단계별 설치 옵션
Scrapling은 사용 목적에 따라 다음과 같이 단계적으로 설치할 수 있습니다.
옵션 1 | 기본 파서 엔진만 설치 (가장 가벼운 설치)
HTML을 직접 파싱하는 기능만 필요하다면 기본 패키지를 설치합니다. 이미 다른 방법으로 HTML을 가져온 경우에 적합합니다.
pip install scrapling
옵션 2 | HTTP 요청 기능 포함 설치 (일반 사이트용)
정적 웹사이트를 스크래핑하려면 Fetcher 의존성을 함께 설치합니다. curl-cffi를 사용하여 TLS 지문을 실제 브라우저처럼 모방할 수 있습니다.
# pip로 설치하는 경우 pip는 []를 인식하지 못합니다. 그래서 패키지+옵션을 " "로 감싸줍니다.
pip install "scrapling[fetchers]"
옵션 3 | 스텔스 브라우저 기능 포함 설치 (Cloudflare 우회용)
Cloudflare Turnstile과 같은 반봇 시스템을 우회해야 한다면 스텔스 옵션을 설치합니다.
pip install "scrapling[stealth]"
옵션 4 | 동적 페이지 자동화 포함 설치 (JavaScript 렌더링용)
Playwright 기반의 브라우저 자동화가 필요하다면 dynamic 옵션을 설치합니다.
pip install "scrapling[dynamic]"
옵션 5 | 전체 기능 설치 (추천)
모든 기능을 한 번에 설치하는 방법입니다. 실전 스크래핑에는 이 방법을 권장합니다.
pip install "scrapling[all]"
브라우저 바이너리 설치 (중요)
StealthyFetcher나 DynamicFetcher를 사용하려면 브라우저 실행 파일을 별도로 설치해야 합니다. 이 과정에서 Chromium과 수정된 Firefox(Camoufox)가 다운로드됩니다. 파일 크기가 크므로 안정적인 네트워크 환경에서 실행하세요.
# 브라우저 바이너리 설치
scrapling install
# 강제 설치
scrapling install --force
설치가 완료되면 다음과 같은 메시지가 출력됩니다:
Installing Playwright browsers...
Installing Playwright dependencies...
Installing Camoufox browser and databases...
[완료] All browsers installed successfully!
camoufox가 설치되지 않는다?
Scapling을 위한 브라우저 중 stealthfetch를 지원하는 camoufox가 설치되지 않는것 처럼 보이는 경우, 아래의 내용을 확인해보세요.
Camoufox가 이미 설치되어 있는지 확인
camoufox fetch
# 이 명령어가 작동하지 않는다면 pip install camoufox가 먼저 되어 있어야 합니다.
패키지 설치 시 [fetchers] 옵션 사용 여부
pip install "scrapling[fetchers]"
pip install "scrapling[all]"
# 그 후 다시 scrapling install을 실행하여 필요한 브라우저 환경을 구성합니다.
최근의 구조 변경 (중요)
설치 검증 테스트
설치가 정상적으로 완료되었는지 간단한 테스트로 확인합니다.
# test_install.py
from scrapling.fetchers import Fetcher
page = Fetcher.get('https://httpbin.org/get')
print(f"상태 코드: {page.status}") # 200이 출력되면 성공
print(f"응답 본문 일부: {page.body[:100]}")
위 스크립트를 실행하여 상태 코드: 200이 출력되면 설치가 정상적으로 완료된 것입니다.
[참고] uv를 사용하는 경우 : 최신 Python 패키지 관리자 uv를 사용한다면 uv add scrapling[all] 명령으로도 설치할 수 있습니다. scrapling-fetch-mcp와 같은 MCP 서버 설치 시에는 uv tool install scrapling-fetch-mcp를 사용합니다.
4. 핵심 개념 | Fetcher 클래스 이해하기
Scrapling의 가장 중요한 개념은 Fetcher 클래스입니다.
웹 페이지를 가져오는 방법에 따라 세 가지 Fetcher가 존재하며, 각각 다른 내부 엔진을 사용합니다. 올바른 Fetcher를 선택하는 것이 스크래핑 성공의 핵심입니다.
Fetcher / AsyncFetcher | 빠른 HTTP 요청
Fetcher는 curl-cffi 라이브러리를 엔진으로 사용하는 HTTP 요청 클래스입니다.
단순히 HTML을 가져오는 것 이상으로, 실제 브라우저의 TLS 지문(fingerprint)을 모방하고 HTTP/1.1, HTTP/2, HTTP/3을 모두 지원합니다. 봇 감지 시스템이 없거나 약한 일반 정적 웹사이트에 가장 적합하며, 속도가 빠르고 메모리 사용량이 낮습니다.
from scrapling.fetchers import Fetcher, AsyncFetcher
# 동기 방식 GET 요청
page = Fetcher.get('https://quotes.toscrape.com/')
print(page.status) # 200
# 최신 Chrome 브라우저의 TLS 지문 모방
page = Fetcher.get(
'https://example.com',
impersonate='chrome', # chrome, firefox, safari, edge 중 선택
stealthy_headers=True, # 실제 브라우저 헤더 자동 생성
follow_redirects=True, # 리다이렉트 자동 처리
timeout=30 # 타임아웃 (초)
)
# POST 요청
page = Fetcher.post(
'https://example.com/api',
data={'key': 'value'}, # form 데이터
json={'query': 'test'} # JSON 데이터
)
# 비동기 방식 (async/await)
import asyncio
async def fetch_async():
page = await AsyncFetcher.get('https://example.com')
return page
asyncio.run(fetch_async())
StealthyFetcher | 반봇 시스템 우회
StealthyFetcher는 Camoufox라는 수정된 Firefox 브라우저를 기반으로 합니다.
Camoufox는 WebGL 핑거프린팅, 캔버스 지문, 오디오 컨텍스트 등 브라우저 자동화를 탐지하는 수십 가지 신호를 정상 브라우저처럼 위장합니다. Cloudflare Turnstile, Cloudflare Interstitial 페이지를 기본적으로 우회하며, solve_cloudflare=True 옵션으로 더 강력한 우회도 가능합니다.
from scrapling.fetchers import StealthyFetcher
# 기본 스텔스 모드 (headless 브라우저)
page = StealthyFetcher.fetch(
'https://protected-site.com',
headless=True, # True: 창 없이 실행 (서버용)
network_idle=True # 모든 네트워크 요청 완료 후 반환
)
# Cloudflare 적극 우회 옵션
page = StealthyFetcher.fetch(
'https://cloudflare-protected.com',
headless=True,
solve_cloudflare=True, # Cloudflare Turnstile 자동 해결
google_search=True # Google 검색에서 온 것처럼 위장
)
print(page.status) # 200
DynamicFetcher | 완전한 브라우저 자동화
DynamicFetcher는 Playwright 기반의 Chromium 브라우저를 사용하며, 클릭, 스크롤, 폼 입력 등 사용자 상호작용이 필요한 복잡한 웹사이트에 적합합니다. JavaScript가 렌더링되는 SPA(Single Page Application), 무한 스크롤, 로그인이 필요한 페이지 등에 활용합니다.
from scrapling.fetchers import DynamicFetcher
# 기본 동적 브라우저 사용
page = DynamicFetcher.fetch(
'https://javascript-heavy-site.com',
headless=True,
network_idle=True, # 모든 JS 로딩 완료 후 반환
disable_resources=True # 이미지/폰트 로딩 비활성화 (속도 향상)
)
# 페이지 상호작용 (클릭 등)
page = DynamicFetcher.fetch('https://example.com')
button = page.css('button.load-more').first
if button:
button.click() # 버튼 클릭
Fetcher 선택 가이드
사용할 Fetcher를 결정하는 것은 매우 중요합니다. 잘못된 Fetcher를 선택하면 사이트가 차단되거나, 필요 이상의 리소스를 소비하게 됩니다.
flowchart TD
A[스크래핑 대상 사이트 분석] --> B{JavaScript 렌더링 필요?}
B -- 아니오 --> C{봇 방어 시스템 있음?}
B -- 예 --> D{Cloudflare / 봇 감지?}
C -- 아니오 --> E[Fetcher 사용\n가장 빠르고 가벼움]
C -- 약한 감지 --> F[Fetcher + impersonate 옵션]
D -- 강한 봇 감지 --> G[StealthyFetcher 사용\nCamoufox 브라우저]
D -- 복잡한 상호작용 --> H[DynamicFetcher 사용\nPlaywright Chromium]
5. HTML 파싱과 선택자(Selector) 활용법
Scrapling의 파싱 엔진은 lxml을 기반으로 하되, 대폭 최적화된 독자적 구현체를 사용합니다. BeautifulSoup과 유사한 API를 제공하면서도 훨씬 빠르고 다양한 선택 방식을 지원합니다.
CSS 선택자 사용법
CSS 선택자는 웹 개발자들에게 가장 친숙한 방법입니다. Scrapling에서는 .css() 메서드로 사용하며, Scrapy/Parsel과 동일한 의사 요소(pseudo-element)를 지원합니다.
from scrapling.fetchers import Fetcher
page = Fetcher.get('https://quotes.toscrape.com/')
# 단일 요소 선택 (.get() 메서드)
first_quote = page.css('.quote .text').get()
print(first_quote) # 첫 번째 인용구 텍스트 반환
# 모든 요소 선택 (.getall() 메서드)
all_quotes = page.css('.quote .text::text').getall()
print(all_quotes) # 모든 인용구 텍스트를 리스트로 반환
# 속성 값 추출 (::attr(속성명))
all_links = page.css('a::attr(href)').getall()
print(all_links) # 모든 링크의 href 속성값 리스트
# 요소의 전체 HTML 추출 (::html)
html_content = page.css('.quote::html').get()
# 부모 요소를 가져와서 자식 요소 탐색 (체이닝)
for quote_block in page.css('.quote'):
text = quote_block.css('.text::text').get()
author = quote_block.css('.author::text').get()
tags = quote_block.css('.tag::text').getall()
print(f"인용구: {text}")
print(f"작가: {author}")
print(f"태그: {tags}")
XPath 선택자 사용법
XPath는 CSS 선택자보다 더 강력한 표현력을 제공하며, 특히 복잡한 DOM 구조나 텍스트 기반 검색에 유용합니다.
# XPath 기본 사용
quotes = page.xpath('//span[@class="text"]/text()').getall()
# 조건부 XPath (특정 텍스트를 포함하는 요소)
einstein_quotes = page.xpath(
'//div[@class="quote"][.//small[@class="author" and contains(text(), "Einstein")]]'
)
# 부모 요소 탐색 (.. 사용)
author_element = page.xpath('//span[@class="author"]/..')
# 속성 값 추출
href_values = page.xpath('//a/@href').getall()
텍스트 기반 검색
Scrapling의 고유한 기능 중 하나는 텍스트 내용으로 요소를 검색하는 것입니다. 선택자를 모르더라도 텍스트 내용으로 요소를 찾을 수 있습니다.
# 정확한 텍스트로 검색
element = page.find_by_text('로그인', first_match=True)
# 부분 텍스트 포함 검색
elements = page.find_by_text('Einstein', partial=True)
# 정규식 패턴으로 검색
import re
elements = page.find_by_regex(r'\d{4}년\s\d{1,2}월')
유사 요소 탐색 (find_similar)
Scrapling은 발견한 요소와 유사한 모든 요소를 자동으로 찾는 기능을 제공합니다. 이 기능은 AutoScraper와 유사하지만 훨씬 빠르게 동작합니다.
# 첫 번째 상품 요소를 찾아서 유사한 모든 상품을 자동 탐색
first_product = page.css('.product-item').first
similar_products = first_product.find_similar()
print(f"발견된 유사 요소 수: {len(similar_products)}")
DOM 탐색 (Navigation API)
Scrapling은 부모, 형제, 자식 요소를 탐색하는 풍부한 Navigation API를 제공합니다.
element = page.css('.target-element').first
# 부모 요소 탐색
parent = element.parent
grandparent = element.parent.parent
# 형제 요소 탐색
next_sibling = element.next_sibling
prev_sibling = element.prev_sibling
all_siblings = element.siblings
# 자식 요소 탐색
children = element.children
first_child = element.children[0]
텍스트 정리 및 변환
Scrapling은 추출한 텍스트를 정리하는 내장 메서드를 제공합니다.
element = page.css('.messy-text').first
# 공백 및 줄바꿈 제거
clean_text = element.clean()
# 정규식으로 텍스트 추출
price = element.re(r'\d+,\d+원')
# 첫 번째 매치만 추출
first_price = element.re_first(r'\d+,\d+원')
6. 적응형 스크래핑(Adaptive Scraping) 심화 이해
Scrapling의 가장 독창적인 기능은 적응형 파싱(Adaptive Parsing)입니다. 웹사이트 구조가 변경될 때 기존 스크래퍼는 선택자가 더 이상 작동하지 않아 즉시 고장나지만, Scrapling은 이를 자동으로 감지하고 변경된 요소를 재탐지합니다.
적응형 파싱의 동작 원리
sequenceDiagram
participant S as 스크래퍼
participant W as 웹사이트
participant DB as SQLite DB
S->>W: 첫 번째 스크래핑 (auto_save=True)
W-->>S: HTML 반환
S->>DB: 요소 속성 저장 (태그명, 텍스트, 속성, DOM 경로 등)
Note over DB: 요소 고유 프로파일 저장
Note over W: 웹사이트 구조 변경!
S->>W: 두 번째 스크래핑 (adaptive=True)
W-->>S: 변경된 HTML 반환
S->>DB: 기존 프로파일 조회
DB-->>S: 저장된 요소 특성 반환
S->>S: 유사도 알고리즘으로 새 위치 계산
S-->>S: 변경된 요소 자동 재탐지 성공!
auto_save와 adaptive 옵션 사용법
적응형 파싱을 활성화하는 방법은 두 단계로 구성됩니다. 첫 번째 스크래핑 시 auto_save=True로 요소 프로파일을 저장하고, 이후 스크래핑에서 adaptive=True를 사용하면 저장된 프로파일을 기반으로 요소를 재탐지합니다.
from scrapling.fetchers import StealthyFetcher
# 1단계: 첫 번째 스크래핑 - 요소 프로파일 저장
StealthyFetcher.adaptive = True # 전역 설정
page = StealthyFetcher.fetch('https://shop.example.com')
# auto_save=True: 이 요소의 위치 정보를 SQLite에 저장
products = page.css('.product-card', auto_save=True)
print(f"상품 수: {len(products)}")
# 2단계: 웹사이트 구조 변경 후 - 요소 자동 재탐지
page2 = StealthyFetcher.fetch('https://shop.example.com')
# adaptive=True: 선택자가 실패하면 저장된 프로파일로 재탐지
products2 = page2.css('.product-card', adaptive=True)
# 선택자 .product-card가 .item-card로 바뀌었어도 자동으로 찾음!
print(f"재탐지된 상품 수: {len(products2)}")
유사도 알고리즘 이해
Scrapling이 변경된 요소를 재탐지할 때 사용하는 유사도 알고리즘은 다음 요소들을 종합적으로 고려합니다. 태그명(tag name)과 ID, 클래스명 등의 속성, 요소의 텍스트 내용, DOM 트리에서의 위치(depth, path), 부모 요소와 형제 요소의 특성을 모두 점수화하여 가장 유사한 요소를 선택합니다.
7. 간단한 사이트 스크래핑 실습
이제 실제로 사용해보겠습니다. 스크래핑 연습용으로 가장 많이 사용되는 quotes.toscrape.com을 예제로 시작합니다.
실습 1 | 인용구 사이트 스크래핑
# quotes_scraper.py
from scrapling.fetchers import Fetcher
import json
def scrape_quotes():
"""quotes.toscrape.com에서 인용구 데이터를 수집합니다."""
all_quotes = []
page_num = 1
while True:
url = f'https://quotes.toscrape.com/page/{page_num}/'
print(f"페이지 {page_num} 수집 중...")
page = Fetcher.get(url)
# 인용구 블록이 없으면 마지막 페이지
quotes = page.css('.quote')
if not quotes:
print("더 이상 데이터가 없습니다.")
break
for quote in quotes:
text = quote.css('.text::text').get()
author = quote.css('.author::text').get()
tags = quote.css('.tag::text').getall()
all_quotes.append({
'text': text,
'author': author,
'tags': tags
})
page_num += 1
# 10페이지까지만 수집 (데모 목적)
if page_num > 10:
break
# JSON 파일로 저장
with open('quotes.json', 'w', encoding='utf-8') as f:
json.dump(all_quotes, f, ensure_ascii=False, indent=2)
print(f"총 {len(all_quotes)}개의 인용구를 수집했습니다.")
return all_quotes
if __name__ == '__main__':
quotes = scrape_quotes()
실습 2 | 뉴스 사이트 헤드라인 수집
정적 뉴스 사이트에서 헤드라인을 수집하는 예제입니다. 실제 사이트 구조에 따라 선택자를 조정해야 합니다.
# news_scraper.py
from scrapling.fetchers import Fetcher
import csv
from datetime import datetime
def scrape_news_headlines(url: str, output_file: str = 'headlines.csv'):
"""뉴스 사이트에서 헤드라인을 수집합니다."""
# stealthy_headers=True: User-Agent 등 헤더를 실제 브라우저처럼 설정
page = Fetcher.get(
url,
stealthy_headers=True,
impersonate='chrome'
)
if page.status != 200:
print(f"[!] 오류: HTTP {page.status} 반환")
return []
headlines = []
# h1, h2, h3 태그에서 뉴스 헤드라인 추출
for tag in ['h1', 'h2', 'h3']:
elements = page.css(f'{tag} a')
for elem in elements:
text = elem.css('::text').get()
href = elem.css('::attr(href)').get()
if text and href:
headlines.append({
'headline': text.strip(),
'url': href,
'collected_at': datetime.now().isoformat()
})
# CSV로 저장
if headlines:
with open(output_file, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['headline', 'url', 'collected_at'])
writer.writeheader()
writer.writerows(headlines)
print(f"[완료] {len(headlines)}개 헤드라인을 {output_file}에 저장했습니다.")
return headlines
if __name__ == '__main__':
scrape_news_headlines('https://news.ycombinator.com/')
실습 3 | 상품 정보 수집 (정적 쇼핑몰)
# product_scraper.py
from scrapling.fetchers import Fetcher
from dataclasses import dataclass, asdict
import json
import re
@dataclass
class Product:
name: str
price: str
rating: str
review_count: str
url: str
def scrape_products(base_url: str) -> list[Product]:
"""상품 목록 페이지에서 상품 정보를 수집합니다."""
page = Fetcher.get(
base_url,
stealthy_headers=True,
impersonate='chrome',
follow_redirects=True
)
products = []
# 상품 카드 요소를 auto_save=True로 저장 (적응형 파싱 준비)
product_cards = page.css('.product-item', auto_save=True)
for card in product_cards:
name = card.css('.product-name::text').get(default='이름 없음')
price_raw = card.css('.price::text').get(default='0')
rating = card.css('.rating::text').get(default='0')
reviews = card.css('.review-count::text').get(default='0')
link = card.css('a::attr(href)').get(default='')
# 가격에서 숫자만 추출
price_clean = re.sub(r'[^\d]', '', price_raw)
products.append(Product(
name=name.strip(),
price=price_clean,
rating=rating.strip(),
review_count=reviews.strip(),
url=link
))
return products
if __name__ == '__main__':
products = scrape_products('https://books.toscrape.com/')
# JSON으로 저장
with open('products.json', 'w', encoding='utf-8') as f:
json.dump([asdict(p) for p in products], f, ensure_ascii=False, indent=2)
print(f"[완료] {len(products)}개 상품 수집 완료")
8. 복잡한 사이트 스크래핑 | 쿠팡 상품 데이터 수집
쿠팡(Coupang)은 국내 최대 이커머스 플랫폼 중 하나로, Akamai 기반의 강력한 봇 방어 시스템을 운영합니다. 일반적인 Requests나 Selenium으로는 차단될 확률이 매우 높으며, Scrapling의 StealthyFetcher가 이 상황에서 효과적입니다.
[주의] 쿠팡 및 모든 웹사이트의 스크래핑은 해당 사이트의 이용약관(Terms of Service)과 robots.txt를 반드시 확인하고 준수해야 합니다. 이 예제는 순수 교육 목적으로만 제공되며, 과도한 요청으로 서버에 부하를 주는 행위는 삼가야 합니다. Scrapling 라이브러리 자체도 교육 및 연구 목적의 사용을 명시하고 있습니다.
쿠팡 방어 시스템 이해
쿠팡은 다음과 같은 다층 방어 시스템을 운영합니다.
- TLS 지문 검사(비정상적인 curl이나 Python Requests의 TLS 지문 차단)
- User-Agent 및 헤더 검증(일반적인 봇 헤더 패턴 감지)
- JavaScript 실행 검증(JS를 실행하지 않는 HTTP 요청 차단)
- 분석(요청 패턴, 마우스 움직임, 스크롤 패턴 분석)
- IP 평판 시스템(데이터센터 IP 차단, 요청 빈도 제한)
이러한 방어를 우회하기 위해 StealthyFetcher를 사용합니다.
쿠팡 상품 검색 결과 수집
# coupang_scraper.py
from scrapling.fetchers import StealthyFetcher, StealthySession
import json
import time
import random
import re
def scrape_coupang_search(keyword: str, pages: int = 3) -> list[dict]:
"""
쿠팡 검색 결과에서 상품 정보를 수집합니다.
Args:
keyword: 검색 키워드 (예: '에어프라이어', '노트북')
pages: 수집할 페이지 수
Returns:
상품 정보 딕셔너리 리스트
"""
all_products = []
base_url = 'https://www.coupang.com/np/search'
# StealthySession: 브라우저를 계속 열어두고 여러 페이지를 처리
with StealthySession(
headless=True, # 화면 없이 실행
solve_cloudflare=True, # 클라우드플레어 자동 우회
network_idle=True # 네트워크 완전 로딩 후 파싱
) as session:
for page_num in range(1, pages + 1):
url = f'{base_url}?component=&q={keyword}&channel=user&sorter=scoreDesc&minPrice=&maxPrice=&priceRange=&filterType=&listSize=36&searchIndexingToken=&page={page_num}'
print(f"[{page_num}/{pages}] 페이지 수집 중: {url[:80]}...")
try:
page = session.fetch(url)
if page.status != 200:
print(f"[!] HTTP {page.status} 오류 발생, 건너뜁니다.")
continue
# 상품 리스트 컨테이너 탐색
# 쿠팡 상품 카드는 li.search-product 또는 li.baby-product 클래스 사용
product_items = page.css('li.search-product, li.baby-product')
if not product_items:
print("[!] 상품을 찾을 수 없습니다. 선택자를 확인하세요.")
# 적응형 재탐지 시도
product_items = page.css('li.search-product', adaptive=True)
for item in product_items:
try:
# 상품명 추출
name = item.css('div.name::text').get()
if not name:
name = item.css('span.name::text').get()
# 가격 추출 (정가 및 할인가)
sale_price = item.css('strong.price-value::text').get()
original_price = item.css('del.base-price::text').get()
# 할인율
discount = item.css('span.instant-discount-rate::text').get()
# 평점 및 리뷰 수
rating = item.css('em.rating::text').get()
review_count = item.css('span.rating-total-count::text').get()
if review_count:
review_count = re.sub(r'[^\d]', '', review_count)
# 로켓배송 여부
is_rocket = bool(item.css('span.rocket-badge'))
# 상품 URL
product_url = item.css('a::attr(href)').get()
if product_url and not product_url.startswith('http'):
product_url = 'https://www.coupang.com' + product_url
# 이미지 URL
img_url = item.css('img::attr(src)').get()
if name and sale_price:
all_products.append({
'name': name.strip() if name else '',
'sale_price': sale_price.strip() if sale_price else '',
'original_price': original_price.strip() if original_price else '',
'discount_rate': discount.strip() if discount else '',
'rating': rating.strip() if rating else '',
'review_count': review_count if review_count else '0',
'is_rocket_delivery': is_rocket,
'product_url': product_url or '',
'image_url': img_url or '',
'page_number': page_num,
'keyword': keyword
})
except Exception as e:
print(f"[!] 상품 파싱 오류: {e}")
continue
print(f"[완료] {len(product_items)}개 상품 파싱 완료 (누적: {len(all_products)}개)")
# 페이지 간 랜덤 지연 (서버 부하 방지 및 감지 회피)
if page_num < pages:
delay = random.uniform(2.0, 4.0)
print(f"[i] {delay:.1f}초 대기 중...")
time.sleep(delay)
except Exception as e:
print(f"[!] 페이지 {page_num} 처리 오류: {e}")
continue
return all_products
def scrape_coupang_product_detail(product_url: str) -> dict:
"""
쿠팡 상품 상세 페이지에서 추가 정보를 수집합니다.
"""
page = StealthyFetcher.fetch(
product_url,
headless=True,
network_idle=True,
solve_cloudflare=True
)
detail = {}
# 상품 상세 정보 추출
detail['full_name'] = page.css('h2.prod-buy-header__title::text').get('').strip()
detail['seller'] = page.css('span.vendor-name a::text').get('').strip()
# 상품 옵션 수집
options = page.css('li.prod-option__item span::text').getall()
detail['options'] = options
# 상품 설명
description = page.css('div.prod-description-content::text').getall()
detail['description'] = ' '.join(description).strip()
# 배송 정보
delivery_info = page.css('span.delivery-fee-txt::text').get('').strip()
detail['delivery_info'] = delivery_info
return detail
if __name__ == '__main__':
keyword = '에어프라이어'
print(f"[시작] '{keyword}' 검색 결과 수집")
products = scrape_coupang_search(keyword, pages=2)
# 결과 저장
output_file = f'coupang_{keyword}_products.json'
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(products, f, ensure_ascii=False, indent=2)
print(f"\n[최종 완료] 총 {len(products)}개 상품 수집 -> {output_file}")
# 간단한 통계 출력
if products:
print(f"\n[통계]")
print(f" 로켓배송 상품: {sum(1 for p in products if p['is_rocket_delivery'])}개")
print(f" 리뷰 있는 상품: {sum(1 for p in products if int(p['review_count']) > 0)}개")
쿠팡 스크래핑 시 주요 팁과 주의사항
쿠팡은 강력한 봇 방어 시스템을 운영하므로, 다음 사항을 반드시 준수해야 합니다.
요청 간 충분한 대기 시간(2초 이상)을 설정해야 하며, 너무 많은 페이지를 한 번에 수집하지 않아야 합니다. StealthySession을 사용하여 브라우저를 재사용하면 각 요청마다 브라우저를 새로 시작하는 것보다 훨씬 효율적입니다. 또한 웹사이트 구조는 수시로 변경되므로 auto_save=True와 adaptive=True 옵션을 활용하여 선택자 변경에 대비해야 합니다.
9. Spider 프레임워크로 대규모 크롤링하기
Scrapling 0.4 이후 버전에서는 Scrapy와 유사한 Spider 프레임워크를 내장하여 대규모 동시 크롤링을 지원합니다. 프록시 회전, 요청 중단/재시작, 실시간 통계를 몇 줄의 코드로 구현할 수 있습니다.
# spider_example.py
from scrapling.spiders import Spider, Response
class ProductSpider(Spider):
name = "product_spider"
# 시작 URL 목록
start_urls = [
'https://books.toscrape.com/catalogue/page-1.html',
'https://books.toscrape.com/catalogue/page-2.html',
'https://books.toscrape.com/catalogue/page-3.html',
]
# Spider 설정
concurrent_requests = 5 # 동시 요청 수
download_delay = 1.0 # 요청 간 지연 (초) — request_delay 아님
async def parse(self, response: Response):
"""각 페이지에서 상품을 추출합니다."""
for product in response.css('article.product_pod'):
name = product.css('h3 a::attr(title)').get('')
price = product.css('p.price_color::text').get('')
rating = product.css('p.star-rating::attr(class)').get('')
yield {
'name': name,
'price': price,
'rating': rating.replace('star-rating ', '') if rating else '',
'source_url': response.url
}
# 다음 페이지 링크 추출하여 자동 크롤링
next_page = response.css('li.next a::attr(href)').get()
if next_page:
base_url = 'https://books.toscrape.com/catalogue/'
yield response.follow(base_url + next_page, callback=self.parse)
if __name__ == '__main__':
# Spider 실행: start()는 CrawlResult 객체를 반환
result = ProductSpider().start()
# items는 to_json() / to_jsonl() 메서드를 가진 ItemList
print(f"\n총 {len(result.items)}개 상품 수집 완료")
result.items.to_json("products.json")
10. 세션(Session) 관리와 프록시 회전
실전 스크래핑에서는 쿠키 유지, 로그인 상태 관리, IP 차단 회피를 위한 프록시 회전이 필수적입니다. Scrapling은 이를 위한 Session 클래스와 ProxyRotator를 내장하고 있습니다.
FetcherSession으로 로그인 처리
from scrapling.fetchers import FetcherSession
# 세션 생성 및 로그인
with FetcherSession(impersonate='chrome') as session:
# 로그인 페이지 접속
login_page = session.get('https://example.com/login')
# CSRF 토큰 추출 (일부 사이트에서 필요)
csrf_token = login_page.css('input[name="csrf_token"]::attr(value)').get()
# 로그인 POST 요청 (쿠키가 세션에 자동 저장됨)
response = session.post(
'https://example.com/login',
data={
'username': 'your_username',
'password': 'your_password',
'csrf_token': csrf_token
}
)
# 로그인 후 페이지 접속 (쿠키 자동 포함)
protected_page = session.get('https://example.com/dashboard')
data = protected_page.css('.protected-data::text').getall()
print(data)
프록시 회전 설정
from scrapling.fetchers import FetcherSession
from scrapling.proxy import ProxyRotator
# 프록시 목록 정의
proxies = [
'http://user:pass@proxy1.example.com:8080',
'http://user:pass@proxy2.example.com:8080',
'http://user:pass@proxy3.example.com:8080',
]
# ProxyRotator: 순환식 또는 랜덤 방식으로 프록시 회전
rotator = ProxyRotator(proxies=proxies, strategy='cyclic') # 'random'도 가능
with FetcherSession(proxy=rotator) as session:
for url in url_list:
page = session.get(url)
# 매 요청마다 다른 프록시 IP 사용
print(f"현재 프록시: {session.current_proxy}")
11. 터미널에서 코드 없이 사용하기
Scrapling은 Python 코드를 작성하지 않고도 터미널 명령어로 바로 스크래핑할 수 있는 CLI(Command Line Interface)를 제공합니다. CLI는 네 가지 최상위 명령(install, shell, mcp, extract)으로 구성되어 있으며, 빠른 테스트나 간단한 데이터 수집에 매우 유용합니다.
CLI 명령 구조
scrapling
├── install # 브라우저 바이너리 설치
├── shell # 인터랙티브 IPython 쉘 시작
├── mcp # MCP 서버 시작 (AI 연동)
└── extract # 웹 페이지 수집 및 파일 저장 (하위 명령 그룹)
├── get # HTTP GET 요청 (Fetcher 사용)
├── post # HTTP POST 요청 (Fetcher 사용)
├── put # HTTP PUT 요청 (Fetcher 사용)
├── delete # HTTP DELETE 요청 (Fetcher 사용)
├── fetch # 동적 브라우저 GET (DynamicFetcher 사용)
└── stealthy-fetch # 스텔스 브라우저 GET (StealthyFetcher 사용)
extract 명령 | 파일로 저장하는 스크래핑
extract 명령은 URL을 가져와서 결과를 파일로 저장하는 것이 핵심입니다. 출력 파일의 확장자(.html, .md, .txt)에 따라 저장 형식이 자동으로 결정됩니다. URL과 출력 파일 경로 두 인수가 모두 필수입니다.
# [기본] HTTP GET으로 가져와서 마크다운으로 저장
scrapling extract get "https://example.com" output.md
# HTML 그대로 저장
scrapling extract get "https://example.com" output.html
# 텍스트만 추출하여 저장
scrapling extract get "https://example.com" output.txt
# CSS 선택자로 특정 요소만 추출
scrapling extract get "https://quotes.toscrape.com" quotes.md \
--css-selector ".quote .text"
# 브라우저 위장 (impersonate) 및 스텔스 헤더 사용
scrapling extract get "https://example.com" output.md \
--impersonate chrome \
--stealthy-headers
# 커스텀 헤더 추가 (-H 옵션은 여러 번 사용 가능)
scrapling extract get "https://api.example.com" data.txt \
-H "Authorization: Bearer mytoken" \
-H "Accept-Language: ko-KR"
# 쿠키 전달
scrapling extract get "https://example.com" output.md \
--cookies "session=abc123; user=john"
# 쿼리 파라미터 추가 (-p 옵션 반복 사용)
scrapling extract get "https://api.example.com" result.json \
-p "page=1" -p "limit=20"
# 타임아웃 설정 (초 단위)
scrapling extract get "https://slow-site.com" output.md \
--timeout 60
# 프록시 사용
scrapling extract get "https://example.com" output.md \
--proxy "http://user:pass@proxy.example.com:8080"
# POST 요청 (폼 데이터)
scrapling extract post "https://api.example.com/search" results.html \
--data "query=python&type=tutorial"
# POST 요청 (JSON 데이터)
scrapling extract post "https://api.example.com" response.json \
--json '{"username": "test", "action": "search"}'
Python의 버전문제로 인한 SSL인증서 오류가 발생하는 경우
CLI명령어 실행시 SSL 인증서 문제가 발생할 수 있습니다. macOS + pyenv 환경에서 Python 3.14 조합에서 자주 발생하는 문제입니다. `--no-verify` 옵션으로 임시 우회가 가능하고, 근본 해결책은 인증서를 설치하는 것입니다.
# 임시 우회: SSL 검증 비활성화
scrapling extract get "https://example.com" output.md --no-verify
근본 원인은 macOS의 Python이 시스템 SSL 인증서 번들을 자동으로 사용하지 않아서입니다. pyenv로 설치한 Python은 더욱 그렇습니다.
해결 방법 1 | certifi 기반 인증서 설치 (가장 간단)
# Python 3.14용 인증서 설치 스크립트 실행
/Users/denny/.pyenv/versions/3.14.0/bin/python3.14 \
/Users/denny/.pyenv/versions/3.14.0/lib/python3.14/site-packages/certifi/cacert.pem
macOS에서 Python을 공식 installer로 설치했을 때 생기는 `Install Certificates.command`와 동일한 역할입니다:
# Python 설치 디렉토리에 있는 인증서 설치 스크립트 실행
open /Applications/Python\ 3.*/Install\ Certificates.command
# 또는 직접 실행
/Applications/Python\ 3.14/Install\ Certificates.command
해결 방법 2 | pip certifi 재설치 + 환경변수 설정
pip install --upgrade certifi
# 현재 세션에 적용
export SSL_CERT_FILE=$(python -c "import certifi; print(certifi.where())")
export REQUESTS_CA_BUNDLE=$(python -c "import certifi; print(certifi.where())")
# 영구 적용 (~/.zshrc 또는 ~/.bashrc에 추가)
echo 'export SSL_CERT_FILE=$(python3 -c "import certifi; print(certifi.where())")' >> ~/.zshrc
echo 'export REQUESTS_CA_BUNDLE=$(python3 -c "import certifi; print(certifi.where())")' >> ~/.zshrc
source ~/.zshrc
해결 방법 3 | pyenv Python 재설치 시 OpenSSL 명시
# Homebrew openssl 설치
brew install openssl
# pyenv 재설치 시 openssl 경로 지정
CFLAGS="-I$(brew --prefix openssl)/include" \
LDFLAGS="-L$(brew --prefix openssl)/lib" \
pyenv install 3.14.0 --force
가장 빠른 확인은 방법 2의 환경변수 설정입니다. 터미널에서 `export SSL_CERT_FILE=...` 설정 후 바로 재시도해보세요. Python 3.14는 매우 최신 버전이라 일부 라이브러리와의 호환성 이슈도 있을 수 있으니, Scrapling 공식 권장 버전인 3.11~3.12 사용도 고려해볼 만합니다.
extract fetch / stealthy-fetch | 브라우저 기반 수집
JavaScript 렌더링이 필요한 동적 페이지는 fetch(DynamicFetcher)나 stealthy-fetch(StealthyFetcher) 서브커맨드를 사용합니다. 브라우저 기반이므로 별도의 옵션들이 추가됩니다.
# DynamicFetcher: JavaScript 렌더링 후 저장
scrapling extract fetch "https://spa-site.com" output.md
# 네트워크 유휴 상태까지 대기 (모든 JS 로딩 완료 후 수집)
scrapling extract fetch "https://spa-site.com" output.md \
--network-idle
# 특정 CSS 선택자가 나타날 때까지 대기
scrapling extract fetch "https://example.com" output.md \
--wait-selector ".product-list"
# 추가 대기 시간 설정 (밀리초 단위)
scrapling extract fetch "https://example.com" output.md \
--wait 2000
# 이미지/CSS 등 불필요한 리소스 차단 (속도 향상)
scrapling extract fetch "https://example.com" output.md \
--disable-resources
# StealthyFetcher: 반봇 우회 + 저장
scrapling extract stealthy-fetch "https://cloudflare-protected.com" output.md
# Cloudflare Turnstile 자동 해결
scrapling extract stealthy-fetch "https://protected.com" output.md \
--solve-cloudflare
# 광고 차단 (uBlock Origin 내장 사용)
scrapling extract stealthy-fetch "https://ad-heavy.com" output.md \
--disable-ads
# IP 기반 지역/시간대 자동 설정
scrapling extract stealthy-fetch "https://geo-protected.com" output.md \
--geoip
# 프록시 사용
scrapling extract stealthy-fetch "https://example.com" output.md \
--proxy "http://user:pass@proxy.example.com:8080"
MCP 서버 직접 실행
mcp 명령으로 Scrapling의 내장 MCP 서버를 직접 CLI에서 시작할 수 있습니다. stdio 모드(기본)와 HTTP 스트리밍 모드를 모두 지원합니다.
# stdio 모드로 MCP 서버 시작 (Claude Desktop 등 MCP 클라이언트 연동용)
scrapling mcp
# HTTP 스트리밍 모드로 시작 (원격 접속 허용)
scrapling mcp --http --host 0.0.0.0 --port 8000
인터랙티브 쉘(Interactive Shell) 활용
scrapling shell 명령은 IPython 기반의 인터랙티브 개발 환경을 시작합니다. 선택자를 실시간으로 테스트하거나, 브라우저 개발자 도구에서 복사한 curl 명령을 Scrapling 요청으로 즉시 변환하는 기능을 제공합니다. 스크래핑 스크립트 개발 초기에 선택자를 탐색하고 검증하는 데 특히 유용합니다.
# 인터랙티브 쉘 시작
scrapling shell
# 특정 코드를 실행하고 바로 종료
scrapling shell --code "page = fetch('https://example.com'); print(page.status)"
# 로그 레벨 지정 (debug, info, warning, error, critical)
scrapling shell --loglevel warning
쉘이 시작되면 Scrapling의 모든 클래스와 단축 함수가 미리 로드된 상태로 진입합니다:
# 쉘 내에서 사용 가능한 내장 단축 함수
>>> page = fetch('https://quotes.toscrape.com/') # Fetcher.get()
>>> page = stealthy_fetch('https://protected.com') # StealthyFetcher.fetch()
# CSS/XPath 선택자 실시간 테스트
>>> page.css('.quote .text::text').getall()
>>> page.xpath('//span[@class="text"]/text()').getall()
# 브라우저 개발자 도구 Network 탭에서 복사한 curl 명령을 Scrapling 코드로 변환
>>> curl2fetcher("curl 'https://example.com' -H 'Authorization: Bearer token' -H 'Cookie: session=abc'")
# 현재 페이지 결과를 브라우저로 열어서 확인
>>> page # 인터랙티브 쉘에서 Response 객체를 그대로 입력하면 브라우저에서 열림
[참고] 쉘 내 curl2fetcher 함수는 브라우저 개발자 도구 -> Network 탭 -> 요청 우클릭 -> "Copy as cURL"로 복사한 명령어를 붙여 넣으면 해당 요청을 재현하는 Scrapling 코드로 자동 변환해줍니다. 복잡한 인증 헤더나 쿠키가 포함된 요청을 스크래핑 스크립트로 빠르게 전환할 때 매우 유용합니다.
12. MCP 서버를 이용한 AI 연동
Scrapling은 내장 MCP(Model Context Protocol) 서버를 제공하여 Claude, Cursor, Windsurf 등의 AI 도구와 직접 통합할 수 있습니다. AI가 Scrapling을 도구로 사용하여 웹 데이터를 수집하고 분석하는 강력한 파이프라인을 구성할 수 있습니다.
Scrapling 공식 내장 MCP 서버
Scrapling v0.4부터 공식 MCP 서버가 내장되어 있습니다. AI 연동용 추가 패키지를 설치합니다:
# AI/MCP 의존성 포함 설치
pip install "scrapling[ai]"
# 브라우저 바이너리 설치 (필수)
scrapling install
Claude Desktop에 내장 MCP 서버 연결
Claude Desktop의 설정 파일(claude_desktop_config.json)에 다음을 추가합니다:
{
"mcpServers": {
"ScraplingServer": {
"command": "scrapling",
"args": ["mcp"]
}
}
}
[중요] 실행 경로 문제가 발생하면 scrapling 대신 절대 경로를 사용하세요.
예: "command": "/Users/username/.venv/bin/scrapling"
경로 확인: which scrapling (macOS/Linux) 또는 where scrapling (Windows)
내장 MCP 서버 제공 도구 (6가지)
내장 MCP 서버는 다음 6가지 도구를 제공합니다:
| 도구 | 설명 |
| get | HTTP 요청 + TLS 지문 위장 + 브라우저 헤더 |
| bulk_get | 여러 URL 비동기 동시 수집 |
| fetch | Chromium 브라우저로 동적 콘텐츠 수집 |
| bulk_fetch | 여러 URL 브라우저 탭으로 동시 수집 |
| stealthy_fetch | Camoufox 스텔스 브라우저 (Cloudflare 우회) |
| bulk_stealthy_fetch | 여러 URL 스텔스 브라우저 동시 수집 |
설정 후 Claude Desktop을 재시작하면 자연어로 스크래핑을 지시할 수 있습니다.
HTTP 모드로 MCP 서버 실행
네트워크를 통해 MCP 서버에 접속하거나 n8n 등 외부 도구와 연동할 때는 HTTP 모드를 사용합니다:
# HTTP 모드로 MCP 서버 시작 (기본 포트: 8000)
scrapling mcp --http --host 0.0.0.0 --port 8000
13. Claude Code에서 Scrapling 활용하기
Claude Code는 터미널 기반의 AI 코딩 어시스턴트로, Scrapling과 결합하면 자연어로 웹 스크래핑 스크립트를 생성하고 실행할 수 있습니다.
Claude Code 설치 및 Scrapling 환경 준비
# Claude Code 설치 (npm 필요)
npm install -g @anthropic-ai/claude-code
# Scrapling 설치 (Claude Code가 실행되는 환경에)
pip install scrapling[all]
scrapling install
# Claude Code 실행
claude
MCP를 이용한 Scrapling 연동
Claude Code에서 Scrapling MCP 서버를 직접 추가하면, Claude Code가 스크래핑 작업을 직접 수행할 수 있습니다.
# Claude Code에 Scrapling MCP 서버 추가
claude mcp add scrapling-fetch -- uvx scrapling-fetch-mcp
Claude Code에서 Scrapling 활용 실전 예시
Claude Code가 실행된 상태에서 다음과 같이 자연어로 지시할 수 있습니다:
사용자: 쿠팡에서 '에어프라이어'를 검색한 결과 첫 3페이지의 상품명, 가격,
리뷰 수를 수집하는 Python 스크립트를 Scrapling을 사용해서 작성하고 실행해줘.
결과는 products.csv로 저장해줘.
Claude Code: [scrapling을 사용하여 자동으로 스크립트 작성 및 실행]
Scrapling을 Claude Code의 커스텀 도구로 등록
Claude Code 프로젝트에 CLAUDE.md 파일을 만들어 Scrapling 사용 방법을 지시할 수 있습니다.
<!-- CLAUDE.md -->
# 프로젝트 설정
## 웹 스크래핑 도구
이 프로젝트에서는 웹 스크래핑에 Scrapling을 사용합니다.
### Fetcher 선택 기준
- 정적 사이트: `Fetcher.get(url, stealthy_headers=True)`
- 봇 방어 있는 사이트: `StealthyFetcher.fetch(url, headless=True)`
- 동적 JS 사이트: `DynamicFetcher.fetch(url, network_idle=True)`
### 필수 패턴
- 항상 auto_save=True로 첫 스크래핑 진행
- 요청 간 최소 1~2초 지연 설정
- 에러 처리 및 재시도 로직 포함
Claude Code에서 직접 Scrapling 스크립트 작성 및 실행
# Claude Code가 생성하고 실행하는 스크립트 예시
# claude_scrapling_agent.py
import asyncio
from scrapling.fetchers import AsyncFetcher
import json
async def collect_data(urls: list[str]) -> list[dict]:
"""여러 URL에서 비동기로 데이터를 수집합니다."""
tasks = []
for url in urls:
task = AsyncFetcher.get(
url,
stealthy_headers=True,
impersonate='chrome'
)
tasks.append(task)
# 동시에 모든 URL 처리
pages = await asyncio.gather(*tasks, return_exceptions=True)
results = []
for url, page in zip(urls, pages):
if isinstance(page, Exception):
print(f"[!] 오류 ({url}): {page}")
continue
results.append({
'url': url,
'title': page.css('title::text').get(''),
'description': page.css('meta[name="description"]::attr(content)').get(''),
'h1_tags': page.css('h1::text').getall(),
'links_count': len(page.css('a')),
'status': page.status
})
return results
# 실행
urls = [
'https://news.ycombinator.com/',
'https://reddit.com/',
'https://github.com/trending',
]
results = asyncio.run(collect_data(urls))
print(json.dumps(results, ensure_ascii=False, indent=2))
14. n8n 워크플로우에서 Scrapling 활용하기
n8n은 Scrapling과 결합하면 스크래핑 결과를 자동으로 데이터베이스에 저장하거나, 알림을 보내거나, 다른 서비스와 연동하는 완전한 자동화 파이프라인을 구성할 수 있습니다.
방법 1 | n8n Code 노드(Python)에서 Scrapling 직접 사용
n8n이 설치된 환경에 Scrapling이 설치되어 있다면, Code 노드에서 Python 코드로 직접 실행할 수 있습니다.
[주의] n8n의 Python Code 노드는 Python Task Runner를 사용합니다. Scrapling의 브라우저 기반 Fetcher(StealthyFetcher, DynamicFetcher)는 별도 프로세스를 실행하므로, n8n Code 노드에서는 기본 Fetcher만 사용하고, 브라우저 기반 작업은 방법 2(Execute Command 노드)를 사용하는 것을 권장합니다.
# n8n Code 노드 (Python) - 정적 사이트용
# 이 코드는 n8n의 Python Code 노드에서 실행됩니다
import subprocess
import json
# 방법: 외부 Python 스크립트 호출
def run_scrapling(url, selector):
script = f"""
import json
from scrapling.fetchers import Fetcher
page = Fetcher.get('{url}', stealthy_headers=True)
results = page.css('{selector}::text').getall()
print(json.dumps(results))
"""
result = subprocess.run(
['python3', '-c', script],
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
return json.loads(result.stdout)
else:
raise Exception(f"스크래핑 오류: {result.stderr}")
# n8n 입력 데이터 처리
input_data = $input.all()
url = input_data[0]['json'].get('url', 'https://quotes.toscrape.com/')
selector = input_data[0]['json'].get('selector', '.quote .text')
data = run_scrapling(url, selector)
return [{"json": {"scraped_data": data, "count": len(data), "url": url}}]
방법 2 | n8n Execute Command 노드 활용
Execute Command 노드를 사용하면 Python 스크립트를 전체 프로세스로 실행할 수 있어 StealthyFetcher와 DynamicFetcher도 사용 가능합니다.
# Execute Command 노드에서 실행하는 명령
python3 /opt/scrapling_scripts/scraper.py --url "{{ $json.url }}" --keyword "{{ $json.keyword }}" --output /tmp/scraping_result.json
# /opt/scrapling_scripts/scraper.py
#!/usr/bin/env python3
import argparse
import json
import sys
from scrapling.fetchers import StealthyFetcher
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--url', required=True)
parser.add_argument('--keyword', default='')
parser.add_argument('--output', required=True)
args = parser.parse_args()
try:
page = StealthyFetcher.fetch(
args.url,
headless=True,
network_idle=True
)
results = {
'url': args.url,
'status': page.status,
'title': page.css('title::text').get(''),
'data': page.css('.product::text').getall()
}
with open(args.output, 'w', encoding='utf-8') as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(json.dumps({'success': True, 'count': len(results['data'])}))
except Exception as e:
print(json.dumps({'success': False, 'error': str(e)}), file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()
방법 3 | n8n HTTP Request 노드 + Scrapling API 서버
가장 권장하는 방법입니다. Scrapling을 FastAPI 또는 Flask 기반의 별도 마이크로서비스로 구성하고, n8n의 HTTP Request 노드로 호출합니다.
# scrapling_api.py - FastAPI 기반 Scrapling API 서버
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from scrapling.fetchers import Fetcher, StealthyFetcher
import asyncio
import uvicorn
app = FastAPI(title="Scrapling API", version="1.0.0")
class ScrapeRequest(BaseModel):
url: str
selector: str = None
mode: str = "normal" # normal, stealthy, dynamic
headless: bool = True
network_idle: bool = True
class ScrapeResponse(BaseModel):
url: str
status: int
data: list
title: str
success: bool
@app.post("/scrape", response_model=ScrapeResponse)
async def scrape_url(request: ScrapeRequest):
"""URL에서 데이터를 스크래핑합니다."""
try:
if request.mode == "stealthy":
page = StealthyFetcher.fetch(
request.url,
headless=request.headless,
network_idle=request.network_idle
)
else:
page = Fetcher.get(
request.url,
stealthy_headers=True,
impersonate='chrome'
)
# 선택자가 있으면 적용, 없으면 전체 텍스트 반환
if request.selector:
data = page.css(f'{request.selector}::text').getall()
else:
data = [page.css('body').get('')]
return ScrapeResponse(
url=request.url,
status=page.status,
data=data,
title=page.css('title::text').get(''),
success=True
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
async def health_check():
return {"status": "healthy", "service": "scrapling-api"}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8888)
# docker-compose.yml - Scrapling API 서버 Docker 배포
version: '3.8'
services:
scrapling-api:
image: python:3.11-slim
container_name: scrapling-api
ports:
- "8888:8888"
volumes:
- ./scrapling_api.py:/app/scrapling_api.py
- ./requirements.txt:/app/requirements.txt
working_dir: /app
command: >
sh -c "pip install scrapling[all] fastapi uvicorn &&
scrapling install &&
python scrapling_api.py"
restart: unless-stopped
# requirements.txt
# scrapling[all]
# fastapi
# uvicorn
n8n 워크플로우 구성 예시
n8n에서 위 API를 활용하는 워크플로우 구성은 다음과 같습니다:
flowchart LR
A[Schedule Trigger\n매일 오전 9시] --> B[Set Node\nURL 목록 설정]
B --> C[Split In Batches\n5개씩 처리]
C --> D[HTTP Request\nScrapling API 호출]
D --> E{응답 성공?}
E -- 예 --> F[Code Node\n데이터 정제]
E -- 아니오 --> G[Slack\n오류 알림]
F --> H[PostgreSQL\n데이터 저장]
H --> I[Telegram\n수집 완료 알림]
n8n HTTP Request 노드 설정 방법입니다. Method는 POST, URL은 http://scrapling-api:8888/scrape(Docker 내부 네트워크), Body Content Type은 JSON으로 설정합니다.
{
"url": "{{ $json.target_url }}",
"selector": ".product-name",
"mode": "stealthy",
"headless": true
}
n8n에서 대규모 스크래핑 자동화 파이프라인
실제 업무에서 활용할 수 있는 완전한 자동화 파이프라인 예시입니다. 매일 지정된 시간에 여러 이커머스 사이트의 가격을 비교하고, 변동 사항을 알림으로 전송하는 워크플로우입니다.
flowchart TD
A[Schedule Trigger\n매일 오전 6시] --> B[PostgreSQL\n모니터링 상품 목록 조회]
B --> C[Split In Batches\n10개씩 처리]
C --> D[HTTP Request\nScrapling API - 가격 수집]
D --> E[Code Node\n이전 가격과 비교]
E --> F{가격 변동?}
F -- 상승 --> G[Telegram\n가격 상승 알림]
F -- 하락 --> H[Telegram\n가격 하락 알림 + 구매 추천]
F -- 변동없음 --> I[PostgreSQL\n수집 기록만 저장]
G --> I
H --> I
I --> J[완료]
15. Docker로 Scrapling 환경 구성하기
Scrapling은 공식 Docker 이미지를 제공하며, 특히 브라우저 바이너리 설치가 복잡한 서버 환경에서 Docker 이미지를 사용하면 훨씬 간편합니다.
공식 Docker 이미지 사용
# 공식 이미지 pull
docker pull ghcr.io/d4vinci/scrapling:latest
# 컨테이너 실행 (인터랙티브 모드)
docker run -it --rm ghcr.io/d4vinci/scrapling:latest python3
# 스크립트 실행
docker run --rm \
-v $(pwd):/workspace \
ghcr.io/d4vinci/scrapling:latest \
python3 /workspace/scraper.py
커스텀 Dockerfile 구성
# Dockerfile
FROM ghcr.io/d4vinci/scrapling:latest
WORKDIR /app
# 프로젝트 의존성 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 스크래핑 스크립트 복사
COPY scrapling_scripts/ ./scrapling_scripts/
COPY scrapling_api.py .
# API 서버 포트 노출
EXPOSE 8888
CMD ["python3", "scrapling_api.py"]
Docker Compose로 n8n + Scrapling API 통합 구성
# docker-compose.yml
version: '3.8'
services:
# n8n 워크플로우 자동화
n8n:
image: docker.n8n.io/n8nio/n8n:latest
container_name: n8n
ports:
- "5678:5678"
volumes:
- n8n_data:/home/node/.n8n
environment:
- N8N_HOST=localhost
- N8N_PROTOCOL=http
- WEBHOOK_URL=http://localhost:5678/
depends_on:
- scrapling-api
restart: unless-stopped
# Scrapling API 서버
scrapling-api:
build: .
container_name: scrapling-api
ports:
- "8888:8888"
volumes:
- scrapling_data:/app/data
environment:
- PYTHONUNBUFFERED=1
restart: unless-stopped
shm_size: '2gb' # 브라우저 실행을 위한 공유 메모리 (중요!)
# PostgreSQL - 스크래핑 결과 저장
postgres:
image: postgres:15
container_name: scrapling-db
environment:
- POSTGRES_DB=scrapling
- POSTGRES_USER=scrapling
- POSTGRES_PASSWORD=scrapling_pass
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
n8n_data:
scrapling_data:
postgres_data:
[주의] Docker 컨테이너에서 브라우저(Chromium, Firefox)를 실행할 때는 shm_size: '2gb' 옵션을 반드시 설정해야 합니다. 공유 메모리가 부족하면 브라우저가 즉시 충돌합니다.
16. 고급 기능 | 비동기(Async) 처리와 성능 최적화
대규모 스크래핑 작업에서는 비동기 처리와 성능 최적화가 핵심입니다. Scrapling은 모든 Fetcher에 대해 완전한 비동기 지원을 제공합니다.
비동기 대량 수집 패턴
# async_bulk_scraper.py
import asyncio
from scrapling.fetchers import AsyncFetcher
import time
import json
async def scrape_single(session, url: str, semaphore: asyncio.Semaphore) -> dict:
"""단일 URL을 비동기로 스크래핑합니다."""
async with semaphore: # 동시 요청 수 제한
try:
page = await AsyncFetcher.get(
url,
stealthy_headers=True,
impersonate='chrome',
timeout=30
)
return {
'url': url,
'status': page.status,
'title': page.css('title::text').get(''),
'success': True
}
except Exception as e:
return {
'url': url,
'status': 0,
'error': str(e),
'success': False
}
async def bulk_scrape(urls: list[str], concurrent_limit: int = 10) -> list[dict]:
"""여러 URL을 동시에 스크래핑합니다."""
# 세마포어로 동시 요청 수 제한 (서버 부하 방지)
semaphore = asyncio.Semaphore(concurrent_limit)
start_time = time.time()
tasks = [scrape_single(None, url, semaphore) for url in urls]
results = await asyncio.gather(*tasks)
elapsed = time.time() - start_time
success_count = sum(1 for r in results if r['success'])
print(f"\n[통계] {len(urls)}개 URL 처리 완료")
print(f" - 성공: {success_count}개")
print(f" - 실패: {len(urls) - success_count}개")
print(f" - 소요 시간: {elapsed:.2f}초")
print(f" - 평균 처리 속도: {len(urls)/elapsed:.1f} URLs/초")
return list(results)
# 실행 예시
if __name__ == '__main__':
urls = [
f'https://quotes.toscrape.com/page/{i}/'
for i in range(1, 21) # 20페이지 동시 처리
]
results = asyncio.run(bulk_scrape(urls, concurrent_limit=5))
with open('bulk_results.json', 'w', encoding='utf-8') as f:
json.dump(results, f, ensure_ascii=False, indent=2)
성능 최적화 팁
실전 스크래핑에서 성능을 극대화하기 위한 핵심 원칙들이 있습니다. 첫째로, 불필요한 리소스 로딩을 비활성화해야 합니다. 브라우저 기반 Fetcher에서 disable_resources=True를 설정하면 이미지, 폰트, CSS 등을 로딩하지 않아 속도가 크게 향상됩니다. 둘째로, 세션을 재사용하는 것이 중요합니다. 매 요청마다 새 브라우저를 시작하는 것보다 Session 클래스를 사용하여 하나의 브라우저로 여러 요청을 처리하면 최대 10배 빠른 속도를 낼 수 있습니다. 셋째로, 목적에 맞는 Fetcher를 선택해야 합니다. JavaScript 렌더링이 필요 없는 사이트에 DynamicFetcher를 사용하는 것은 불필요한 낭비입니다.
# 성능 최적화 예시
from scrapling.fetchers import StealthySession
with StealthySession(
headless=True,
disable_resources=True, # 이미지/CSS/폰트 로딩 비활성화
network_idle=False, # 완전 로딩 대기 안 함 (일부 사이트 가능)
max_pages=5 # 브라우저 탭 풀 크기 (동시 탭 수)
) as session:
urls = [f'https://example.com/product/{i}' for i in range(1, 101)]
for url in urls:
page = session.fetch(url)
data = page.css('.price::text').get()
print(f"{url}: {data}")
자주 묻는 질문(FAQ)
Q: Scrapling은 Python 몇 버전부터 지원하나요?
Python 3.10 이상이 필요합니다. 3.9는 이전 버전(0.2.x)에서는 지원되었지만, 최신 버전에서는 Python 3.10+의 타입 힌팅 기능을 사용하므로 반드시 3.10 이상으로 업그레이드하는 것을 권장합니다.
Q: StealthyFetcher로도 차단되는 사이트가 있나요?
네, 존재합니다. Akamai Bot Manager, DataDome, Kasada, Incapsula와 같은 엔터프라이즈급 봇 방어 시스템은 Scrapling의 StealthyFetcher로도 완전히 우회하기 어렵습니다. 이 경우에는 해당 시스템 전용 토큰 생성 API 서비스(Hyper Solutions 등)를 사용하거나, 정식 데이터 파트너십을 통해 접근하는 것이 현실적입니다. 쿠팡과 같은 사이트의 경우 network_idle=True와 충분한 대기 시간, 프록시 회전을 조합하여 안정적인 수집이 가능합니다.
Q: 수집한 데이터를 CSV, Excel로 저장하는 방법은?
import pandas as pd
# 수집한 데이터를 pandas DataFrame으로 변환
df = pd.DataFrame(products)
# CSV 저장
df.to_csv('products.csv', index=False, encoding='utf-8-sig') # BOM 포함으로 엑셀 한글 지원
# Excel 저장
df.to_excel('products.xlsx', index=False)
Q: 로봇 배제 프로토콜(robots.txt)을 자동으로 확인하는 방법은?
from urllib.robotparser import RobotFileParser
from urllib.parse import urljoin
def check_robots(url: str, user_agent: str = '*') -> bool:
"""해당 URL의 스크래핑 허용 여부를 확인합니다."""
from urllib.parse import urlparse
parsed = urlparse(url)
robots_url = f"{parsed.scheme}://{parsed.netloc}/robots.txt"
rp = RobotFileParser()
rp.set_url(robots_url)
rp.read()
return rp.can_fetch(user_agent, url)
# 사용 예시
if check_robots('https://example.com/products'):
page = Fetcher.get('https://example.com/products')
else:
print("이 URL은 스크래핑이 금지되어 있습니다.")
Q: 메모리 부족 오류가 발생할 때 해결 방법은?
브라우저 기반 Fetcher를 사용할 때 메모리 부족이 발생하면 다음을 시도해보세요. 첫째, max_pages 값을 줄여 동시 탭 수를 제한합니다. 둘째, disable_resources=True로 불필요한 리소스 로딩을 차단합니다. 셋째, 처리 후 세션을 명시적으로 닫고 새로 시작합니다. Docker를 사용하는 경우 shm_size를 최소 2GB로 설정합니다.
마무리 및 다음 단계
Scrapling은 단순한 파싱 라이브러리를 넘어 현대 웹의 복잡한 요구사항을 모두 처리할 수 있는 통합 프레임워크입니다. 적응형 파싱으로 웹사이트 변경에도 자동 적응하고, 다층적인 Fetcher 구조로 정적 사이트부터 Cloudflare 보호 사이트까지 단계적으로 대응할 수 있으며, 내장 MCP 서버로 Claude와 같은 AI 도구와 자연스럽게 통합됩니다.
이 가이드에서 배운 핵심 내용을 정리하면 다음과 같습니다. 설치는 pip install scrapling[all] 후 scrapling install로 브라우저를 설치하면 됩니다. Fetcher 선택은 정적 사이트에는 Fetcher, 봇 방어 사이트에는 StealthyFetcher, JavaScript 렌더링에는 DynamicFetcher를 사용합니다. 적응형 파싱은 첫 실행에 auto_save=True, 이후 adaptive=True를 사용하면 웹사이트 구조 변경을 자동으로 처리합니다. n8n 연동은 Scrapling API 서버를 별도로 구성하고 HTTP Request 노드로 호출하는 방식이 가장 안정적입니다. Claude Code 연동은 claude mcp add 명령으로 scrapling-fetch-mcp를 추가하면 자연어로 스크래핑 작업을 지시할 수 있습니다.
다음 단계로는 Scrapling 공식 문서(scrapling.readthedocs.io)에서 Spider 프레임워크의 상세 설정을 학습하고, n8n 워크플로우에서 스크래핑 결과를 AI로 분석하는 파이프라인을 구성해보시길 권장합니다. 또한 Scrapling의 MCP 서버와 Claude를 결합하면 자연어로 데이터 수집 및 분석까지 수행하는 강력한 에이전트 워크플로우를 구성할 수 있습니다.
참고 자료
- Scrapling 공식 GitHub: https://github.com/D4Vinci/Scrapling
- Scrapling 공식 문서: https://scrapling.readthedocs.io
- Scrapling PyPI 페이지: https://pypi.org/project/scrapling/
- scrapling-fetch-mcp (MCP 서버): https://github.com/cyberchitta/scrapling-fetch-mcp
- n8n 공식 문서: https://docs.n8n.io
- Claude Code 공식 문서: https://docs.anthropic.com/en/docs/claude-code
'AI 코딩' 카테고리의 다른 글
| cmux - AI 코딩 에이전트 시대의 새로운 터미널 (0) | 2026.03.22 |
|---|---|
| vLLM 완벽 가이드 - 대규모 언어 모델 서빙의 새로운 표준 (0) | 2026.03.19 |
| Tailscale 완벽 가이드 (2편) - 설치부터 Zero Trust 네트워크 구축 (0) | 2026.02.07 |
| Tailscale 완벽 가이드 (1편) - 아키텍처 이해 (0) | 2026.02.07 |
| [라즈베리파이] 4대 클러스터로 Qwen3-30B 구동하기 (0) | 2026.02.02 |