본문 바로가기

Develop

Selenium 크롤러의 진화: 안티봇 탐지를 피하는 우아한 방법

웹 크롤링 시스템 개선: 안티봇 탐지 우회와 안정성 향상

최근 우리 팀은 웹 크롤링 시스템을 대폭 개선하는 작업을 진행했습니다. 이 글에서는 크롤링 시스템의 안정성을 높이고 안티봇 탐지를 우회하기 위해 적용한 다양한 전략들을 공유하고자 합니다.

 

1. 인프라 구성 개선

1.1 패키지 의존성 추가

크롤링 시스템의 안정성을 높이기 위해 필요한 시스템 패키지들을 추가했습니다. 특히 헤드리스 Chrome 브라우저가 정상적으로 동작하기 위해 필요한 다양한 의존성들을 식별하고 추가했습니다.

packages:
    cups-libs: []
    cups: []
    cups-client: []
    cups-devel: []
    libXScrnSaver: []
    nss: []

이러한 패키지들은 다음과 같은 목적으로 추가되었습니다:

  • CUPS 관련 패키지: PDF 생성 및 프린팅 기능 지원
  • libXScrnSaver: 화면 보호기 관련 기능 지원
  • NSS: 네트워크 보안 서비스 지원

1.2 ChromeDriver 설치 프로세스 개선

기존의 단순한 ChromeDriver 설치 과정을 더욱 견고하게 개선했습니다. 주요 변경사항은 다음과 같습니다:

# EPEL 저장소 활성화
amazon-linux-extras install epel -y

# Chrome 브라우저 설치 개선
if ! command -v google-chrome > /dev/null 2>&1; then
    echo "Google Chrome not found. Installing..."
    yum install -y cups-libs libX11 libXcomposite libXcursor libXdamage libXext libXi libXrandr libXScrnSaver libXss libXtst at-spi2-atk gtk3 alsa-lib mesa-libgbm xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc
    cd /tmp
    wget https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm
    sudo yum install -y ./google-chrome-stable_current_x86_64.rpm
fi

특히 주목할 만한 개선사항은 다음과 같습니다:

  1. 이미 설치된 경우 중복 설치 방지
  2. 필요한 폰트 및 그래픽 라이브러리 자동 설치
  3. 설치 과정의 로깅 개선

2. 크롤링 아키텍처 개선

2.1 WebDriverFactory 도입

크롬 드라이버 생성 로직을 중앙화하고 재사용성을 높이기 위해 WebDriverFactory 클래스를 도입했습니다:

class WebDriverFactory:
    @staticmethod
    def create_chrome_options() -> Options:
        options = Options()

        # 안티봇 탐지 방지 설정
        selected_ua = random.choice(USER_AGENTS)
        options.add_argument(f'user-agent={selected_ua}')

        # 헤드리스 모드 설정
        options.add_argument("--headless=new")
        options.add_argument("--no-sandbox")

        # 브라우저 동작 최적화
        options.add_argument('--disable-gpu')
        options.add_argument('--disable-blink-features=AutomationControlled')
        options.add_experimental_option('excludeSwitches', ['enable-automation'])

        return options

    @classmethod
    def create_driver(cls) -> webdriver.Chrome:
        return webdriver.Chrome(
            service=Service(executable_path='/usr/local/bin/chromedriver'),
            options=cls.create_chrome_options()
        )

2.2 BaseScraper 추상 클래스 구현

반복되는 크롤링 로직을 추상화하고 일관된 인터페이스를 제공하기 위해 BaseScraper 클래스를 구현했습니다:

class BaseScraper:
    def __init__(self, wait_timeout: int = 20):
        self.driver = WebDriverFactory.create_driver()
        self.wait = WebDriverWait(self.driver, wait_timeout)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.driver.quit()

    def scroll_page(self, scroll_pause: float = 1.5):
        """인간다운 스크롤 동작 구현"""
        last_height = self.driver.execute_script(
            "return document.body.scrollHeight"
        )

        while True:
            # 랜덤한 스크롤 동작
            scroll_amount = random.randint(300, 700)
            self.driver.execute_script(
                f"window.scrollBy(0, {scroll_amount});"
            )

            # 자연스러운 대기 시간
            time.sleep(random.uniform(1.0, scroll_pause))

            new_height = self.driver.execute_script(
                "return document.body.scrollHeight"
            )
            if new_height == last_height:
                break
            last_height = new_height

3. 쿠팡 크롤러 구현

3.1 Product 데이터 클래스

수집할 상품 데이터를 명확하게 정의하기 위해 데이터 클래스를 활용했습니다:

@dataclass
class Product:
    name: str
    link: str
    image: str

    def to_dict(self) -> Dict:
        return {
            "name": self.name,
            "link": self.link,
            "image": self.image
        }

3.2 CoupangScraper 구현

쿠팡 전용 크롤러는 BaseScraper를 상속받아 구현했습니다:

class CoupangScraper(BaseScraper):
    def scrape_products(self, query: str) -> Dict:
        try:
            encoded_query = quote(query)
            url = f"https://www.coupang.com/np/search?q={encoded_query}"
            self.driver.get(url)
            time.sleep(random.uniform(2.0, 4.0))  # 자연스러운 대기

            self._scrape_product_cards()

            return {
                'url': self.driver.current_url,
                'data': [product.to_dict() for product in self.products]
            }
        except Exception as e:
            logger.error(f"Error during scraping: {str(e)}")
            return {'error': str(e)}

4. 안티봇 탐지 우회 전략

4.1 User-Agent Rotation

다양한 User-Agent를 사용하여 봇 탐지를 우회합니다:

USER_AGENTS = [
    # 모바일 에이전트
    'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_2 like Mac OS X) AppleWebKit/605.1.15',
    'Mozilla/5.0 (Linux; Android 10; SM-G970F) AppleWebKit/537.36',
    # 데스크톱 에이전트
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
]

4.2 자연스러운 브라우저 동작 구현

봇 탐지를 피하기 위해 다음과 같은 전략들을 구현했습니다:

  • 랜덤한 스크롤 동작
  • 자연스러운 대기 시간
  • 실제 브라우저와 유사한 헤더 설정
  • 자동화 흔적 제거

5. 에러 처리 및 로깅

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

try:
    product_cards = self.wait.until(
        EC.presence_of_all_elements_located(
            (By.CSS_SELECTOR, "li.search-product")
        )
    )
except TimeoutException:
    logger.warning("Timeout while waiting for product cards")
except Exception as e:
    logger.error(f"Unexpected error: {str(e)}")

결론

이번 개선을 통해 다음과 같은 성과를 얻을 수 있었습니다:

  1. 크롤링 성공률 향상
    • 안티봇 탐지 회피율 증가
    • 안정적인 데이터 수집
  2. 코드 품질 개선
    • 재사용 가능한 컴포넌트 설계
    • 테스트 용이성 향상
  3. 유지보수성 향상
    • 명확한 에러 처리
    • 체계적인 로깅

향후에는 다음과 같은 개선을 계획하고 있습니다:

  • 추가 크롤러 개발 용이성 확보
  • 성능 모니터링 시스템 구축
  • 분산 크롤링 시스템 도입 검토

이러한 개선사항들은 GitHub에서 확인하실 수 있습니다. 여러분의 프로젝트에도 도움이 되었기를 바랍니다.

반응형