파이썬 웹 스크래핑: 403 Forbidden 오류 해결하기 (헤더 설정, API 우회, 속도 제한)

403 Forbidden 오류를 나타내는 컴퓨터 화면
< 403 Forbidden 오류 >

웹 스크래핑을 처음 시작했을 때, 403 Forbidden 오류는 누구나 한 번쯤 마주치는 장벽입니다. Reddit의 r/learnpython에서도 이 오류를 해결하려는 질문이 자주 올라오죠. "왜 내 코드는 403 에러를 뱉는 걸까?", "헤더를 추가했는데도 안 돼!", "속도 제한 때문에 차단된 걸까?" 같은 고민들 말이죠.

이 글에서는 파이썬으로 웹 스크래핑할 때 403 Forbidden 오류의 원인과 해결 방법을 초보자도 이해할 수 있도록 자세히 다룹니다. 헤더 설정, API 우회, 속도 제한 문제 해결법 등 실용적인 팁과 코드 예제를 포함했으니, 천천히 따라오세요!


403 Forbidden 오류란?

403 Forbidden은 서버가 요청을 거부했을 때 발생하는 HTTP 상태 코드입니다. 즉, "이 페이지에 접근할 권한이 없어요!"라는 뜻이죠. 웹 스크래핑 중 이 오류가 발생하는 주요 원인은 다음과 같습니다:

  1. 봇 감지: 웹사이트가 당신의 요청을 사람이 아닌 봇으로 인식.
  2. 헤더 누락: 브라우저처럼 보이는 요청 헤더가 없음.
  3. 속도 제한: 짧은 시간 내에 너무 많은 요청을 보냄.
  4. IP 차단: 특정 IP에서 비정상적인 트래픽 감지.
  5. API 제한: 스크래핑 대신 API 사용을 요구하는 사이트.

이제 각 원인별로 해결 방법을 하나씩 살펴보겠습니다.


해결법 1: 사용자 에이전트(User-Agent)와 헤더 설정

왜 헤더가 중요한가?

대부분의 웹사이트는 요청에 포함된 헤더를 확인해 요청이 브라우저에서 온 것인지, 봇에서 온 것인지 판단합니다. 특히 User-Agent는 요청이 어떤 브라우저(Chrome, Firefox 등)에서 왔는지 알려주는 핵심 정보입니다. 기본적으로 requests 라이브러리로 요청을 보내면 User-Agentpython-requests/2.28.1처럼 설정되어 봇으로 간주되기 쉽습니다.

해결 방법

User-Agent를 실제 브라우저처럼 설정하고, 추가적인 헤더를 포함하세요. 예를 들어, Chrome 브라우저의 헤더를 모방할 수 있습니다.

코드 예제

import requests

# 사용자 에이전트와 헤더 설정
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'en-US,en;q=0.5',
    'Connection': 'keep-alive',
}

url = 'https://example.com'
response = requests.get(url, headers=headers)

if response.status_code == 200:
    print("성공!")
    print(response.text)
else:
    print(f"실패: {response.status_code}")
  • 다양한 User-Agent 사용: fake-useragent 라이브러리를 사용하면 랜덤한 User-Agent를 쉽게 생성할 수 있습니다.
  • 브라우저 헤더 확인: Chrome 개발자 도구(F12) → Network 탭에서 실제 요청의 헤더를 복사해 사용하세요.


해결법 2: 속도 제한(Rate Limiting) 회피

속도 제한을 나타내는 시계와 데이터 흐름
< 데이터 흐름을 표현한 속도 제한 >

속도 제한이란?

웹사이트는 짧은 시간 내에 너무 많은 요청이 들어오면 봇으로 간주하고 403 오류를 반환하거나 IP를 차단합니다. 예를 들어, 1초에 10번 요청을 보내는 스크립트는 쉽게 차단당할 수 있습니다.

해결 방법

  1. 요청 간 딜레이 추가: time.sleep()을 사용해 요청 사이에 대기 시간을 둡니다.
  2. 랜덤 딜레이: 고정된 딜레이 대신 랜덤 시간을 설정해 더 자연스럽게 보이게 합니다.

코드 예제

import requests
import time
import random

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}

urls = ['https://example.com/page1', 'https://example.com/page2', 'https://example.com/page3']

for url in urls:
    response = requests.get(url, headers=headers)
    if response.status_code == 200:
        print(f"성공: {url}")
    else:
        print(f"실패: {url}, 상태 코드: {response.status_code}")
    
    # 1~3초 사이 랜덤 딜레이
    time.sleep(random.uniform(1, 3))
  • 사이트별 제한 확인: 웹사이트의 robots.txt 파일이나 이용 약관에서 요청 빈도 제한을 확인하세요.
  • 프록시 사용: IP 차단을 피하려면 아래에서 설명할 프록시를 고려하세요.


해결법 3: API 우회 및 대체 접근

API가 필요한 경우

일부 웹사이트는 스크래핑을 방지하기 위해 데이터를 API를 통해서만 제공합니다. 예를 들어, Reddit, Twitter(X), YouTube 같은 사이트는 API를 사용하도록 유도합니다. 이 경우 HTML 스크래핑 대신 API를 호출하는 것이 더 효율적입니다.

해결 방법

  1. 공식 API 사용: 웹사이트의 공식 문서를 확인해 API 엔드포인트를 찾습니다.
  2. 비공식 API 탐색: 공식 API가 없으면 Postman이나 브라우저 개발자 도구를 사용해 비공식 API를 찾아볼 수 있습니다.
  3. API 키 요청: API 사용에 키가 필요하다면, 개발자 포털에서 등록하세요.

코드 예제 (Reddit API)

Reddit API를 사용하려면 먼저 Reddit 개발자 포털에서 클라이언트 ID와 시크릿을 발급받아야 합니다.

import requests

# Reddit API 인증
client_id = 'YOUR_CLIENT_ID'
client_secret = 'YOUR_CLIENT_SECRET'
user_agent = 'my-scraper/0.1'

auth = requests.auth.HTTPBasicAuth(client_id, client_secret)
data = {
    'grant_type': 'client_credentials'
}

headers = {'User-Agent': user_agent}
response = requests.post('https://www.reddit.com/api/v1/access_token', auth=auth, data=data, headers=headers)

if response.status_code == 200:
    token = response.json()['access_token']
    headers['Authorization'] = f'bearer {token}'
    
    # 데이터 요청
    response = requests.get('https://oauth.reddit.com/r/learnpython/hot', headers=headers)
    print(response.json())
else:
    print("인증 실패")
  • API 사용 제한 확인: API는 호출 횟수 제한이 있을 수 있으니 문서를 꼼꼼히 읽으세요.
  • 비공식 API 주의: 비공식 API는 갑자기 변경될 수 있으니 주기적으로 확인하세요.


해결법 4: 프록시 및 IP 회피

IP 차단 문제

속도 제한을 지켰음에도 403 오류가 계속 발생한다면, 서버가 당신의 IP를 차단했을 가능성이 있습니다. 특히 대량 스크래핑 시 자주 발생합니다.

해결 방법

  1. 프록시 사용: 요청을 다른 IP를 통해 보내도록 설정합니다.
  2. 프록시 풀: 여러 프록시를 순환 사용해 차단 위험을 줄입니다.

코드 예제

import requests

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}

# 프록시 설정
proxies = {
    'http': 'http://your-proxy-ip:port',
    'https': 'http://your-proxy-ip:port'
}

url = 'https://example.com'
response = requests.get(url, headers=headers, proxies=proxies)

if response.status_code == 200:
    print("성공!")
else:
    print(f"실패: {response.status_code}")

프록시 서비스 추천

  • 무료 프록시: Free Proxy List (품질이 낮을 수 있음)
  • 유료 프록시: Bright Data, Smartproxy, Oxylabs (안정적이고 빠름)
  • 프록시 품질 확인: 무료 프록시는 느리거나 차단될 가능성이 높으니 테스트 후 사용하세요.
  • VPN 고려: 간단한 스크래핑이라면 NordVPN, Surfshark 같은 VPN도 유용합니다.
프록시 서버를 나타내는 네트워크 다이어그램
< 프록시 서버와 네트워크 다이어그램 >

해결법 5: JavaScript 렌더링 문제 (Dynamic Content)

동적 콘텐츠 문제

일부 웹사이트는 JavaScript로 콘텐츠를 동적으로 로드합니다. requests는 JavaScript를 실행하지 않으므로 빈 페이지나 403 오류를 받을 수 있습니다.

해결 방법

  1. Selenium 사용: 실제 브라우저를 자동화해 JavaScript를 렌더링합니다.
  2. Playwright: Selenium보다 가볍고 빠른 대안.

코드 예제 (Selenium)

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

# 헤드리스 모드 설정
options = Options()
options.add_argument('--headless')
options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36')

driver = webdriver.Chrome(options=options)
driver.get('https://example.com')

# 페이지 소스 가져오기
print(driver.page_source)

driver.quit()
  • 렌더링 시간 고려: JavaScript 로딩이 완료될 때까지 대기하도록 time.sleep() 또는 WebDriverWait를 사용하세요.
  • Playwright 추천: 최신 프로젝트라면 Playwright가 더 간편합니다.


추가 팁: 403 오류 예방하기

  1. robots.txt 확인: 웹사이트의 /robots.txt를 확인해 스크래핑이 허용된 페이지를 파악하세요.
  2. 최소한의 데이터 요청: 필요한 데이터만 요청해 서버 부담을 줄이세요.
  3. 에러 처리 추가: try-except로 403 오류를 감지하고 대처하세요.
try:
    response = requests.get(url, headers=headers)
    response.raise_for_status()  # 4xx, 5xx 에러 감지
except requests.exceptions.HTTPError as e:
    print(f"HTTP 에러 발생: {e}")
  1. 윤리적 스크래핑: 웹사이트의 이용 약관을 준수하고, 과도한 요청으로 서버에 부담을 주지 마세요.


마무리

파이썬으로 웹 스크래핑 중 403 Forbidden 오류는 다양한 원인으로 발생하지만, 올바른 방법으로 접근하면 대부분 해결할 수 있습니다. 헤더 설정, 속도 제한 관리, API 활용, 프록시 사용, JavaScript 렌더링 같은 기술을 조합해 문제를 극복해보세요. Reddit r/learnpython에서도 비슷한 고민을 하는 분들이 많으니, 이 글이 여러분의 스크래핑 여정에 도움이 되길 바랍니다!


© 2025 Your Name. All rights reserved.