Python에서 요청 재시도를 구현하는 방법

댓글: 0

웹 스크래핑은 웹에서 데이터를 추출하는 효과적인 방법입니다. 많은 개발자가 웹 스크래핑 프로젝트를 수행할 때 간단하고 효과적인 Python 요청 라이브러리를 선호합니다. 하지만 요청 라이브러리에는 그 장점만큼이나 한계도 있습니다. 웹 스크래핑에서 발생할 수 있는 일반적인 문제 중 하나는 요청 실패이며, 이는 종종 불안정한 데이터 추출로 이어집니다. 이 글에서는 HTTP 오류를 처리하고 웹 스크래핑 스크립트를 안정적이고 신뢰할 수 있게 유지할 수 있도록 Python에서 요청 재시도를 구현하는 과정을 살펴보겠습니다.

요청 라이브러리 시작하기

먼저 환경을 설정해 보겠습니다. Python과 원하는 IDE가 설치되어 있는지 확인하세요. 그런 다음 요청 라이브러리가 설치되어 있지 않은 경우 설치합니다.

pip install requests

설치가 완료되면 Python의 요청 모듈을 사용하여 example.com으로 요청을 전송해 보겠습니다. 다음은 이를 수행하는 간단한 함수입니다:

import requests

def send_request(url):
    """
    지정된 URL로 HTTP GET 요청을 전송하고 응답 상태 코드를 인쇄합니다.
    
    Parameters:
        url (str): 요청을 보낼 URL입니다.
    """
    response = requests.get(url)
    print('Response Status Code: ', response.status_code)

send_request('https://example.com')

코드 출력은 아래와 같습니다:

How to implement request retries in Python.png

HTTP 상태 코드를 자세히 살펴보고 더 잘 이해해 보겠습니다.

HTTP 상태 코드 이해하기

서버는 요청의 결과를 나타내는 상태 코드와 함께 HTTP 요청에 응답합니다. 간단히 요약하면 다음과 같습니다:

  1. 1xx(정보): 요청이 수신되었으며 계속 처리 중입니다.
  2. 2xx(성공): 요청이 수신되어 이해되었으며 수락되었습니다.
    • 200 OK: 요청이 성공했습니다. HTTP 상태 코드의 녹색 표시등입니다.
  3. 3xx (리디렉션): 요청을 완료하려면 추가 작업이 필요합니다.
  4. 4xx(클라이언트 오류): 클라이언트 측의 문제로 인해 요청에 오류가 발생했습니다.
  5. 5xx(서버 오류): 서버 측의 오류로 인해 서버가 유효한 요청을 처리하지 못했습니다.
    • 500 내부 서버 오류: 서버가 요청을 완료하지 못했습니다. 이는 서버가 요청을 처리할 수 없는 예기치 않은 조건이 발생했음을 나타냅니다. 이는 빨간색 신호등에 해당하는 HTTP 상태 코드입니다.
    • 504 게이트웨이 시간 초과: 서버가 제시간 내에 업스트림 서버로부터 응답을 받지 못했습니다. 대기실 시간 초과 신호등에 해당하는 HTTP 상태 코드입니다.

이 예에서 상태 코드 200은 https://example.com 에 대한 요청이 완료되었음을 의미합니다. 이는 서버가 "모든 것이 정상입니다, 요청이 성공했습니다"라고 말하는 방식입니다.

이러한 상태 코드는 봇을 탐지하고 봇과 유사한 행동으로 인해 액세스가 제한되는 시기를 표시하는 데에도 중요한 역할을 할 수 있습니다.

다음은 주로 봇 탐지 및 인증 문제로 인해 발생하는 HTTP 오류 코드에 대한 간략한 요약입니다.

  1. 429 너무 많은 요청: 이 상태 코드는 사용자가 주어진 시간 동안 너무 많은 요청을 보냈음을 나타냅니다("속도 제한"). 봇이 사전 정의된 요청 한도를 초과할 때 일반적으로 발생하는 응답입니다.
  2. 403 금지됨: 서버가 요청 처리를 거부할 때 이 코드가 반환됩니다. 서버가 사용자 에이전트 또는 기타 기준에 따라 요청이 봇으로부터 온 것으로 의심되는 경우 발생할 수 있습니다.
  3. 401 권한 없음: 봇이 가지고 있지 않은 인증이 필요한 경우 이 상태가 사용될 수 있습니다.
  4. 503 서비스 사용 불가: 서버가 일시적으로 요청을 처리할 수 없음을 나타내는 데 사용되기도 하며, 자동화된 트래픽 급증 시 발생할 수 있습니다.

파이썬에서 재시도 메커니즘 구현하기

이제 요청 라이브러리로 HTTP GET 요청을 하는 간단한 재시도 메커니즘을 파이썬으로 작성해 보겠습니다. 네트워크 문제나 서버 과부하로 인해 네트워크 요청이 실패하는 경우가 있습니다. 따라서 요청이 실패하면 이러한 요청을 다시 시도해야 합니다.

기본 재시도 메커니즘

send_request_with_basic_retry_mechanism 함수는 네트워크 또는 연결 오류와 같은 요청 예외가 발생한 경우에만 재시도하는 기본 재시도 메커니즘을 사용하여 지정된 URL로 HTTP GET 요청을 보냅니다. 이 메커니즘은 요청 최대 재시도 횟수를 최대로 재시도합니다. 이러한 예외로 인해 모든 시도가 실패하면 마지막으로 발생한 예외를 발생시킵니다.

import requests
import time

def send_request_with_basic_retry_mechanism(url, max_retries=2):
    """
    기본 재시도 메커니즘을 사용하여 URL에 HTTP GET 요청을 보냅니다.
    
    Parameters:
        url (str): 요청을 보낼 URL입니다.
        max_retries (int): 요청을 다시 시도할 수 있는 최대 횟수입니다.

    Raises:
        requests.RequestException: 모든 재시도가 실패하면 마지막 예외를 발생시킵니다.

    """
    for attempt in range(max_retries):
        try:
            response = requests.get(url)
            print('Response status: ', response.status_code)
            break  # 요청 성공 시 루프 종료
        except requests.RequestException as error:
            print(f"Attempt {attempt+1} failed:", error)
            if attempt < max_retries - 1:
                print(f"Retrying...")
                time.sleep(delay)  # 다시 시도하기 전에 대기
            else:
                print("Max retries exceeded.")
                # 최대 재시도에 도달하면 마지막 예외를 다시 발생시킵니다.
                raise
                send_request_with_basic_retry_mechanism('https://example.com')

사전 재시도 메커니즘

이제 스크래핑하려는 웹사이트가 차단을 초래할 수 있는 봇 탐지 메커니즘을 구현하는 시나리오를 처리하기 위해 기본 재시도 메커니즘을 조정해 보겠습니다. 이러한 시나리오를 해결하려면 봇 탐지 차단뿐만 아니라 네트워크 또는 서버 문제 때문일 수도 있으므로 요청을 여러 번 부지런히 재시도해야 합니다.

아래 send_request_with_advance_retry_mechanism 함수는 재시도 횟수 및 재시도 지연을 선택적으로 지정한 URL로 HTTP GET 요청을 전송합니다. 지정된 시도 횟수만큼 여러 번 요청을 전송하고 요청이 성공적으로 응답을 받으면 응답 상태 코드를 출력합니다. 요청 작업 중 오류가 발생하면 오류 메시지를 출력하고 재시도합니다. 각 시도 사이에 지정된 재시도 지연 시간을 기다립니다. 지정된 재시도 횟수 이후에도 요청이 실패하면 마지막으로 발생한 예외를 발생시킵니다.

지연 매개변수는 가까운 간격으로 여러 요청이 서버에 폭격을 가하는 것을 방지하기 때문에 중요합니다. 대신 서버가 요청을 처리할 충분한 시간을 확보할 때까지 기다리므로 서버는 봇이 아닌 사람이 요청을 하는 것으로 간주합니다. 따라서 재시도 메커니즘은 봇 방지 메커니즘을 트리거할 수 있는 서버 과부하 또는 느린 서버 응답을 피하기 위해 지연되어야 합니다.

import requests
import time

def send_request_with_advance_retry_mechanism(url, max_retries=3, delay=1):
    """
    고급 재시도 메커니즘을 사용하여 지정된 URL로 HTTP GET 요청을 보냅니다.
    
    Parameters:
        url (str): 요청을 보낼 URL입니다.
        max_retries (int): 요청을 다시 시도할 수 있는 최대 횟수입니다. 기본값은 3입니다.
        delay (int): 재시도 사이의 지연 시간(초)입니다. 기본값은 1입니다.

    Raises:
        requests.RequestException: 모든 재시도가 실패하면 마지막 예외를 발생시킵니다.
    """
    for attempt in range(max_retries):
        try:
            response = requests.get(url)
            # 4xx 또는 5xx 상태 코드에 대한 예외 발생
            response.raise_for_status()
            print('Response Status Code:', response.status_code)
        except requests.RequestException as e:
            # 요청이 실패하면 오류 메시지와 시도 번호를 인쇄합니다.
            print(f"Attempt {attempt+1} failed:", e)
            if attempt < max_retries - 1:
                # 재시도 메시지를 인쇄하고 재시도하기 전에 기다립니다.
                print(f"Retrying in {delay} seconds...")
                time.sleep(delay)
            else:
                # 최대 재시도 횟수를 초과하면 메시지를 출력하고 예외를 다시 발생시킵니다.
                print("Max retries exceeded.")
                raise

# 사용 예시
send_request_with_advance_retry_mechanism('https://httpbin.org/status/404')

이 구현의 단점은 다음과 같습니다:

  • 4xx 및 5xx 범위에 속하는 모든 상태 코드가 재시도됩니다. 그러나 404(찾을 수 없음) 상태 코드를 초래하는 요청은 다시 시도할 필요가 없습니다.
  • 일부 봇 탐지 서비스는 상태 코드 200(OK)으로 응답할 수 있지만 응답 내용이 다를 수 있습니다. 현재 구현에서는 이 상황을 처리하지 않습니다. 콘텐츠 길이 유효성 검사를 구현하면 이 문제를 해결할 수 있습니다.

다음은 수정된 코드와 단점을 설명하는 댓글입니다:

import requests
import time

def send_request_with_advance_retry_mechanism(url, max_retries=3, delay=1, min_content_length=10):
    """
    고급 재시도 메커니즘을 사용하여 지정된 URL로 HTTP GET 요청을 보냅니다.

    Parameters:
        url (str): 요청을 보낼 URL입니다.
        max_retries (int): 요청을 다시 시도할 수 있는 최대 횟수입니다. 기본값은 3입니다.
        delay (int): 재시도 사이의 지연 시간(초)입니다. 기본값은 1입니다.
        min_content_length (int): 유효한 것으로 간주할 응답 콘텐츠의 최소 길이입니다. 기본값은 10입니다.

    Raises:
        requests.RequestException: 모든 재시도가 실패하면 마지막 예외를 발생시킵니다.
    """
    for attempt in range(max_retries):
        try:
            response = requests.get(url)
            # 4xx 또는 5xx 상태 코드에 대한 예외 발생
            response.raise_for_status()
            
            # 응답 상태 코드가 404인지 확인
            if response.status_code == 404:
                print("404 Error: Not Found")
                break  # 404 오류에 대한 루프 종료
            
            # 응답 텍스트의 길이가 지정된 최소 콘텐츠 길이보다 짧은지 확인합니다.
            if len(response.text) < min_content_length:
                print("Response text length is less than specified minimum. Retrying...")
                time.sleep(delay)
                continue  # 요청 다시 시도
            
            print('Response Status Code:', response.status_code)
            # 조건이 충족되면 루프에서 벗어나기
            break
            
        except requests.RequestException as e:
            print(f"Attempt {attempt+1} failed:", e)
            if attempt < max_retries - 1:
                print(f"Retrying in {delay} seconds...")
                time.sleep(delay)
            else:
                print("Max retries exceeded.")
                # 최대 재시도에 도달하면 마지막 예외를 다시 발생시킵니다.
                raise

# 사용 예
send_request_with_advance_retry_mechanism('https://httpbin.org/status/404')

프록시로 특정 HTTP 오류 처리하기

429 너무 많은 요청과 같은 특정 오류의 경우, 로테이션 프록시를 사용하면 요청을 분산하고 속도 제한을 피하는 데 도움이 될 수 있습니다.

아래 코드는 프록시 사용과 함께 고급 재시도 전략을 구현합니다. 이렇게 하면 파이썬 요청 재시도 메커니즘을 구현할 수 있습니다. 고품질 웹 스크래핑 프록시를 사용하는 것도 중요합니다. 이러한 프록시는 프록시 로테이션을 위한 좋은 알고리즘과 안정적인 풀을 갖추고 있어야 합니다.

import requests
import time

def send_request_with_advance_retry_mechanism(url, max_retries=3, delay=1, min_content_length=10):
    """
    고급 재시도 메커니즘을 사용하여 지정된 URL로 HTTP GET 요청을 보냅니다.

    Parameters:
        url (str): 요청을 보낼 URL입니다.
        max_retries (int): 요청을 다시 시도할 수 있는 최대 횟수입니다. 기본값은 3입니다.
        delay (int): 재시도 사이의 지연 시간(초)입니다. 기본값은 1입니다.
   
    Raises:
        requests.RequestException: 모든 재시도가 실패하면 마지막 예외를 발생시킵니다.
    """
    
    proxies = {
        "http": "http://USER:PASS@HOST:PORT",
        "https": "https://USER:PASS@HOST:PORT"
    }
    
    for attempt in range(max_retries):
        try:
            response = requests.get(url, proxies=proxies, verify=False)
            # 4xx 또는 5xx 상태 코드에 대한 예외 발생
            response.raise_for_status()
            
            # 응답 상태 코드가 404인지 확인합니다.
            if response.status_code == 404:
                print("404 Error: Not Found")
                break  # Exit loop for 404 errors
            
            # 응답 텍스트의 길이가 10자 미만인지 확인합니다.
            if len(response.text) < min_content_length:
                print("Response text length is less than 10 characters. Retrying...")
                time.sleep(delay)
                continue  # 요청 다시 시도
            
            print('Response Status Code:', response.status_code)
            # 조건이 충족되면 루프에서 벗어나기
            break
            
        except requests.RequestException as e:
            print(f"Attempt {attempt+1} failed:", e)
            if attempt < max_retries - 1:
                print(f"Retrying in {delay} seconds...")
                time.sleep(delay)
            else:
                print("Max retries exceeded.")
                # 최대 재시도에 도달하면 마지막 예외를 다시 발생시킵니다.
                raise

send_request_with_advance_retry_mechanism('https://httpbin.org/status/404')

Python의 요청 재시도는 효과적인 웹 스크래핑을 위해 매우 중요합니다. 재시도를 관리하는 방법은 차단을 방지하고 데이터 수집의 효율성과 안정성을 높이는 데 도움이 될 수 있습니다. 이러한 기술을 구현하면 웹 스크래핑 스크립트를 더욱 강력하게 만들고 봇 보호 시스템의 탐지에 덜 취약하게 만들 수 있습니다.

댓글:

0 댓글