Cloudflare 우회 크롤링 삽질기
Cloudflare 우회 크롤링 삽질기
웹사이트를 크롤링하려고 했다. requests로 간단하게 될 줄 알았는데, Cloudflare가 막고 있었다. 삽질의 시작.
1단계: requests로 시작
import requests
resp = requests.get("https://example.com/api/posts")
print(resp.status_code) # 403
403 Forbidden. User-Agent를 추가해봤다.
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ..."
}
resp = requests.get(url, headers=headers)
# 여전히 403
안 된다. Cloudflare의 JS Challenge가 걸려있다.
2단계: cloudscraper
cloudscraper는 Cloudflare의 JS Challenge를 자동으로 풀어준다.
import cloudscraper
scraper = cloudscraper.create_scraper()
resp = scraper.get("https://example.com/api/posts")
print(resp.status_code) # 200
된다! 단순한 JS Challenge는 이걸로 충분하다.
동작 원리:
- Cloudflare가 보내는 JS 코드를 파싱
- 수학 연산이나 DOM 조작을 Python으로 에뮬레이션
- 올바른 답을 계산해서 쿠키 획득
WordPress REST API 같은 곳은 이걸로 해결됐다.
3단계: 더 강력한 보호
일부 페이지는 cloudscraper로도 안 됐다. Cloudflare Turnstile이나 더 복잡한 JS 검증이 있는 경우.
이럴 땐 실제 브라우저가 필요하다.
4단계: SeleniumBase UC Mode
SeleniumBase의 UC(Undetected Chrome) 모드를 사용했다.
from seleniumbase import SB
with SB(uc=True, incognito=True) as sb:
sb.uc_open_with_reconnect("https://example.com", 4)
sb.sleep(3)
# 페이지 로드 완료
html = sb.get_page_source()
uc=True가 핵심이다. 일반 Selenium은 Cloudflare가 탐지한다.
UC 모드가 하는 것들
-
webdriver 속성 숨김:
navigator.webdriver = false - Chrome DevTools Protocol 숨김
- User-Agent 정상화
- Canvas/WebGL 핑거프린팅 우회
- 자동화 플래그 제거
5단계: 팝업과 광고 처리
사이트에 광고가 덕지덕지 붙어있었다. 클릭하면 새 탭이 열리고, 전면 광고가 뜨고…
def _close_popup(sb):
"""광고 팝업 제거"""
sb.execute_script("""
// Google Vignette 광고 제거
document.querySelectorAll('iframe[src*="google"]').forEach(el => el.remove());
document.querySelectorAll('[id*="google_vignette"]').forEach(el => el.remove());
// z-index 높은 오버레이 제거
document.querySelectorAll('div[style*="position: fixed"]').forEach(el => {
if (parseInt(el.style.zIndex) > 1000) el.remove();
});
""")
def _handle_new_tabs(sb):
"""광고로 열린 새 탭 닫기"""
windows = sb.driver.window_handles
if len(windows) > 1:
main_window = windows[0]
for w in windows[1:]:
sb.driver.switch_to.window(w)
sb.driver.close()
sb.driver.switch_to.window(main_window)
6단계: 카운트다운 타이머 대기
“Please wait 10 seconds” 같은 타이머가 있는 사이트. 기다려야 한다.
def _wait_for_timer(sb):
"""카운트다운 타이머 대기"""
for _ in range(60): # 최대 60초
try:
# 타이머 텍스트 찾기
timer_text = sb.get_text("#timer")
seconds = int(timer_text)
if seconds <= 0:
break
except:
pass
sb.sleep(1)
7단계: 프록시 로테이션
같은 IP로 계속 요청하면 차단당한다. 프록시를 로테이션해야 한다.
from seleniumbase import SB
proxies = [
"proxy1.example.com:8080",
"proxy2.example.com:8080",
"socks5://proxy3.example.com:1080",
]
def resolve_with_proxy(url: str, proxy: str):
with SB(uc=True, proxy=proxy, headless=True) as sb:
sb.uc_open_with_reconnect(url, 4)
return sb.get_page_source()
병렬 처리 + 프록시 분배
from concurrent.futures import ThreadPoolExecutor
def process_urls(urls: list, proxies: list):
results = []
with ThreadPoolExecutor(max_workers=len(proxies)) as executor:
futures = []
for i, url in enumerate(urls):
proxy = proxies[i % len(proxies)] # 라운드 로빈
future = executor.submit(resolve_with_proxy, url, proxy)
futures.append(future)
for future in futures:
results.append(future.result())
return results
8단계: 헤드리스 모드 감지 우회
헤드리스 모드가 감지되는 경우가 있다.
# 감지됨
with SB(uc=True, headless=True) as sb:
...
# 덜 감지됨 - headless2 사용
with SB(uc=True, headless2=True) as sb:
...
SeleniumBase의 headless2는 --headless=new 플래그를 사용한다. Chrome 109+에서 지원하는 새로운 헤드리스 모드로, 일반 브라우저와 더 비슷하게 동작한다.
그래도 안 되면 Xvfb(가상 디스플레이)를 사용:
# Linux에서
apt-get install xvfb
xvfb-run python crawler.py
9단계: UC Driver 미리 다운로드
병렬 실행 시 여러 워커가 동시에 ChromeDriver를 다운로드하면 충돌이 난다.
from seleniumbase import SB
# 메인 스레드에서 미리 다운로드
with SB(uc=True) as sb:
pass # 초기화만 하고 종료
# 이후 병렬 실행
with ThreadPoolExecutor(max_workers=4) as executor:
...
최종 구조
import cloudscraper
from seleniumbase import SB
# 1단계: cloudscraper로 시도 (API 등 단순한 경우)
def collect_api_data(url):
scraper = cloudscraper.create_scraper()
return scraper.get(url).json()
# 2단계: SeleniumBase UC로 시도 (복잡한 JS 검증)
def resolve_protected_url(url, proxy=None, headless=False):
sb_kwargs = {"uc": True, "incognito": True, "headless": headless}
if proxy:
sb_kwargs["proxy"] = proxy
with SB(**sb_kwargs) as sb:
sb.uc_open_with_reconnect(url, 4)
sb.sleep(3)
# 팝업 제거
_close_popup(sb)
# 타이머 대기
_wait_for_timer(sb)
# 버튼 클릭 등
if sb.is_element_visible("#continue-btn"):
sb.click("#continue-btn")
return sb.get_current_url()
삽질 정리
| 문제 | 해결 |
|---|---|
| 403 Forbidden | cloudscraper |
| JS Challenge 복잡함 | SeleniumBase UC mode |
| 봇 탐지됨 |
uc=True, incognito=True
|
| 광고 팝업 | JS로 DOM 제거 |
| 새 탭 광고 |
window_handles로 닫기 |
| 카운트다운 | 폴링으로 대기 |
| IP 차단 | 프록시 로테이션 |
| 헤드리스 감지 |
headless2 또는 Xvfb |
| 병렬 시 충돌 | 미리 드라이버 다운로드 |
결론
Cloudflare 우회는 단계적으로 접근해야 한다:
- cloudscraper: 단순한 JS Challenge
- SeleniumBase UC: 복잡한 검증
- 프록시 로테이션: IP 차단 우회
- Xvfb: 헤드리스 감지 우회
그리고 예의 바르게 크롤링하자. rate limit 지키고, robots.txt 확인하고.
크롤링은 고양이와 쥐 게임이다. 막으면 뚫고, 뚫으면 막고.