모두가 대비하는 스크래핑 실패는 뻔한 종류다. 403, 429, 타임아웃되는 연결. 눈에 보이고, 셀 수 있고, 재시도할 수 있다. 실제로 데이터셋을 오염시키는 실패는 그 반대다. 완벽하게 성공한 것처럼 보이지만 원하던 내용이 전혀 없는 200 OK다. 정상적인 응답으로 위장한 차단 페이지. 챌린지 화면. 빈 껍데기. 봇에게 잘못된 정보를 주기 위해 일부러 조작된 데이터 페이지.
에러를 내는 스크래퍼는 성가시다. 쓰레기 데이터로 “성공”하는 스크래퍼는 위험하다. 잘못된 데이터가 이미 데이터 웨어하우스에, 보고서에, 혹은 모델에 들어간 뒤에야 알게 되기 때문이다. 이것이 스크래퍼가 차단되는 이유의 또 다른 절반이다. 최신 안티봇 시스템은 조용히 속이는 쪽을 점점 더 선호한다. 눈에 띄는 실패보다 침묵하는 실패가 그들에게 더 가치 있기 때문이다. 이를 잡아내는 방법을 알아본다.
상태 코드는 진실이 아니다
깨야 할 첫 번째 습관은 HTTP 상태 코드를 성공 신호로 신뢰하는 것이다. 200은 서버가 응답을 보냈다는 뜻이지, 요청한 응답을 보냈다는 뜻이 아니다. 사이트는 200과 함께 차단 페이지, CAPTCHA, 동의 화면을 일상적으로 반환한다. 순진한 스크래퍼를 조용하고 만족한 상태로 두면서 실제로는 아무것도 주지 않기 때문이다. 상태 코드를 하나의 약한 신호로만 취급하고 모든 요청에서 본문을 검증하라.
기만적인 200의 범주는 이름을 붙여둘 만하다. 각각 다른 징후를 가지고 있기 때문이다.
- 소프트 블록과 인터스티셜.
200으로 반환되는 챌린지 또는 “당신이 인간임을 확인하세요” 페이지로, 실제 페이지보다 훨씬 작은 경우가 많다. - 챌린지 및 CAPTCHA 화면. 콘텐츠가 있어야 할 자리에 인라인으로 제공된다. 안티봇 방어가 어떻게 진화했는지와 관련이 있다.
- 저하되거나 벗겨진 콘텐츠. 로그인 벽, “자바스크립트를 활성화하세요” 스텁, 또는 데이터가 렌더링되어야 할 빈 뼈대.
- 레이트 리밋 소프트 실패. 특정 임계값을 넘으면 에러 대신 오래되거나 캐시되거나 빈 결과가 반환된다.
- 잘못된 지역 또는 개인화. 잘못된 국가, 통화, 또는 로그아웃 상태에 대한 올바른 페이지로, 기술적으로는 유효하지만 조용히 쓸모없다.
- 허니팟 및 오염된 데이터. 봇에게 일부러 제공되는 콘텐츠. 가짜 가격, 트랩 링크, 또는 불가능한 값을 가진 그럴듯해 보이는 레코드.
전달이 아니라 콘텐츠를 검증하라
가장 효과적인 단일 방어책은 모든 응답에 대한 콘텐츠 검증이다. 페이지가 반드시 포함해야 할 것을 포함하고 있는지 확인하는 저렴한 체크다. 실제 결과에는 항상 있고 차단 페이지에는 절대 없는 불변 조건, 즉 특정 요소, 필수 필드, 최소한의 그럴듯한 길이를 골라, 그것이 없으면 요청을 실패로 처리하라.
def is_valid_product_page(html, parsed): # A real product page always has these. A block page has none of them. if len(html) < 2000: # block pages are usually tiny return False if parsed.select_one("h1.product-title") is None: return False # the anchor element is gone if parsed.select_one('[data-price]') is None: return False # the field we came for is missing return True핵심적인 전환은 앵커 요소가 없는 것을 빈 결과가 아니라 실패로 취급하는 것이다. 의존하는 셀렉터가 없다면, 빈 행을 기록하고 넘어가지 말고, 그 응답을 소프트 블록으로 취급하여 403을 대할 때와 마찬가지로 새로운 아이덴티티로 재시도하라. 빈 값을 기록하는 것이 바로 차단이 아무도 알아채지 못한 채 수천 개의 빈 행으로 조용히 변하는 방식이다.
개별 응답이 아니라 응답의 분포를 관찰하라
개별 체크는 명백한 쓰레기를 잡아낸다. 분포 체크는 미묘한 변화를 잡아내며, 이것이 견고한 파이프라인과 취약한 파이프라인을 가르는 요소다.
- 응답 크기. 차단 및 챌린지 페이지는 작고 균일한 경향이 있다. 배치 전체의 평균 페이지 크기가 갑자기 붕괴하거나, 정확히 하나의 바이트 수에 응답이 몰리는 급증은, 각각이
200을 반환하더라도 차단의 특징이다. - 응답 해싱. 각 응답 본문의 정규화된 버전을 해싱하라. 서로 다른 많은 URL에서 동일한 해시가 갑자기 반복되면, 다양한 실제 콘텐츠가 아니라 하나의 차단 페이지를 계속 받고 있는 것이다.
- 필드 채움율. 각 필드가 실제로 채워진 레코드의 비율을 추적하라. 어제 98퍼센트 채워졌던 필드가 오늘 4퍼센트만 채워졌다면 그 데이터가 줄어든 것이 아니라 벗겨진 페이지를 받기 시작한 것이다.
- 호스트별 성공률. 다른 대상은 안정적인데 특정 대상 하나에서만 하락이 있다면, 그 대상이 당신에 대한 태도를 바꾼 것이다. 전체 실행을 낭비하기 전에 잡아낼 가치가 있다. 이는 대규모로 스크래핑 파이프라인을 모니터링할 때 중요한 대상별 신호와 동일하다.
이 중 어느 것도 머신러닝이 필요하지 않다. 이것들은 카운터와 단순한 기준선이며, 첫 100개 요청에서 차단을 알아채는 것과 백만 개 요청 후에 알아채는 것의 차이를 만든다.
알려진 차단 페이지 시그니처
차단, 챌린지, 또는 에러 페이지에서만 등장하는 문구와 마커의 작은 라이브러리를 유지하고, 상태 코드와 무관하게 이를 포함하는 응답에 플래그를 지정하라.
BLOCK_MARKERS = ( "verify you are human", "unusual traffic", "access denied", "enable javascript to continue", "request blocked",)
def looks_blocked(text): low = text.lower() return any(marker in low for marker in BLOCK_MARKERS)해당 주제를 우연히 다루는 실제 콘텐츠에서 오탐이 발생하지 않도록 짧고 구체적으로 유지하고, 새로운 방어 수단을 만날 때마다 늘려가라. 인식할 수 있는 챌린지 페이지는 저장하는 대신 재시도할 수 있는 챌린지다.
허니팟과 오염된 데이터
가장 골치 아픈 범주는 진짜처럼 보이도록 설계된 콘텐츠다. 여기서는 두 가지 방어가 중요하다.
첫째, 트랩 링크를 따라가지 마라. 허니팟 링크는 흔히 display:none, visibility:hidden, 크기 0, 화면 밖 배치, 또는 aria-hidden으로 사람에게는 숨겨져 있으며, 모든 앵커를 따라가는 봇을 잡기 위해서만 존재한다. 가시성을 존중하고 사람이 클릭할 수 없는 링크를 무시하는 크롤러는 이런 대부분을 피해간다.
둘째, 데이터 자체를 상식적으로 검증하라. 오염된 레코드는 순진한 파서를 통과하도록 만들어졌지만 기본적인 도메인 규칙은 어기는 경우가 많다. 0이거나 터무니없이 높은 가격, 미래 날짜, 존재할 수 없는 수량, 그럴듯한 범위를 벗어난 값 등이다. 도메인이 실제로 허용하는 범위에 대해 검증하고, HTML이 깔끔하게 파싱되었다는 이유로 신뢰하지 말고 규칙을 위반하는 레코드는 격리하라.
카나리아 요청
가장 신뢰할 수 있는 조기 경보는 카나리아다. 정답 콘텐츠를 이미 알고 있는 페이지를 주기적으로 가져와서 여전히 일치하는지 확인하라. 카나리아가 차단 페이지나 잘못된 데이터를 반환하기 시작하면, 데이터 품질의 완만한 저하로부터 추론할 필요 없이 대상이 즉시, 명확하게 태도를 바꿨다는 것을 알 수 있다. 사이트가 한 국가의 종료 IP는 차단하면서 다른 국가는 그대로 둘 수 있으므로 대상별, 지역별로 카나리아를 운영하라.
프록시가 맞아떨어지는 지점
깨끗한 IP는 애초에 소프트 블록되는 빈도를 줄여준다. 좋은 평판을 가진 풀은 플래그된 주소가 조용히 챌린지 페이지를 받는 곳을 순조롭게 통과한다. 이는 잡아내야 할 기만적 응답의 비율을 낮추지만, 그것을 잡아낼 필요를 없애지는 못한다. 차단은 설계상 점점 더 눈에 보이지 않게 되고 있으며 어떤 IP도 면역이 아니기 때문이다. 이 두 가지는 함께 작동한다. 탐지는 응답이 소프트 블록이었음을 알려주고, 로테이팅 레지덴셜 풀은 하드 실패를 대할 때와 정확히 똑같이 재시도할 새로운 아이덴티티를 제공한다. 탐지된 모든 소프트 블록을 재시도 및 로테이션 로직에 반영하고(고정 대 로테이팅), 대상별 차단율의 지속적인 급증을 밀어붙일 신호가 아니라 물러설 신호로 취급하라(차단 피하기와 책임 있는 스크래핑 둘 다 해당된다).
결론
응답이 증명할 때까지는 거짓이라고 가정하라. 실제 페이지가 항상 가지고 있는 불변 조건에 대해 모든 요청의 콘텐츠를 검증하고, 앵커가 없는 것을 저장할 빈 행이 아니라 재시도할 실패로 취급하라. 응답 크기, 해시, 채움율의 분포를 관찰하여 200을 반환하는 차단도 이상 징후로 드러나도록 하라. 알려진 차단 페이지에 대한 짧은 시그니처 목록을 유지하고, 숨겨진 트랩 링크를 따라가기를 거부하고, 도메인 규칙에 대해 데이터를 상식적으로 검증하고, 카나리아를 운영하여 대상이 태도를 바꾸는 순간을 즉시 알아채라.
이렇게 하면, 몇 주에 걸쳐 데이터셋을 조용히 오염시키는 침묵하는 실패가 더 이상 침묵하지 않게 된다. 애초에 소프트 블록을 덜 당하도록 깨끗한 레지덴셜 풀과 결합하라. 그리고 GB당 요금제를 이용하면 선불로 약정하지 않고도 당신의 대상에서 직접 검증해볼 수 있다.