묵시적 실패 방지: LLM을 사용하여 Web Scraper 출력 검증

발행: (2026년 2월 9일 오후 02:43 GMT+9)
13 분 소요
원문: Dev.to

Source: Dev.to

위에 제공된 링크에 있는 전체 텍스트를 알려주시면, 해당 내용을 한국어로 번역해 드리겠습니다. 현재는 번역할 본문이 없으므로, 번역이 필요한 텍스트를 복사해서 제공해 주시기 바랍니다.

문제: 구조적 검증 vs. 의미적 검증

전통적인 데이터 파이프라인에서는 구조적 검증을 사용합니다. Python의 pydantic이나 JSON Schema와 같은 도구는 price라는 필드가 부동소수점이어야 하고 sku라는 필드가 문자열이어야 한다는 것을 훌륭하게 보장합니다.

from pydantic import BaseModel

class Product(BaseModel):
    title: str
    price: float
    sku: str

스크래퍼가 price 필드에 문자열 "Free Shipping"을 추출하면, Pydantic은 "Free Shipping"을 부동소수점으로 변환할 수 없기 때문에 오류를 발생시킵니다. 이는 도움이 되지만 의미적 문제는 해결하지 못합니다.

스크래퍼가 메인 제품 가격 대신 “추천 제품” 사이드바에서 "$19.99"를 추출한다면 어떨까요? 구조적으로는 유효한 부동소수점이지만, 의미적으로는 실패입니다. 전통적인 코드는 페이지를 “읽고” 특정 텍스트가 올바른 텍스트인지 판단하기가 쉽지 않습니다. 여기서 AI 판사가 등장합니다.

솔루션: “AI Judge” 아키텍처

AI Judge 패턴은 스크래핑 루프에 두 번째 검증 단계를 도입합니다. 파서를 무조건 신뢰하는 대신, 원시 HTML의 작은 샘플과 추출된 JSON을 LLM에 전달합니다.

워크플로우

  1. 추출 – 스크래퍼(Playwright, BeautifulSoup 등)가 셀렉터를 사용해 데이터를 추출합니다.
  2. 컨텍스트 샘플링 – 데이터가 발견된 HTML 블록을 분리합니다.
  3. 검증 – LLM이 원시 HTML과 JSON을 비교합니다.
  4. 결정 – LLM이 불일치를 감지하면 시스템이 개발자에게 알리거나 재시도를 트리거합니다.

LLM을 활용하면 수천 줄의 취약한 정규식이나 수동 검사를 작성하지 않고도 비구조화 텍스트와 시각적 계층 구조를 이해하는 능력을 이용할 수 있습니다.

1단계: 설정 (취약한 스크래퍼)

표준 추출 스크립트부터 시작해 보겠습니다. BestBuy.com‑Scrapers 저장소에 있는 것과 유사한 일반적인 전자상거래 제품 페이지를 대상으로 합니다.

import requests
from bs4 import BeautifulSoup

def extract_product_data(html_content):
    soup = BeautifulSoup(html_content, "html.parser")

    # These selectors break easily if the site updates
    return {
        "title": soup.select_one(".product-title").get_text(strip=True),
        "price": soup.select_one(".price-value").get_text(strip=True),
        "sku":   soup.select_one(".model-number").get_text(strip=True),
    }

# Imagine this HTML is fetched via requests
sample_html = (
    'Sony Alpha 7 IV'
    '$2,499.99'
)
data = extract_product_data(sample_html)
print(data)

이 코드는 현재는 동작하지만, 사이트가 .price-value.price-display-v2로 변경하면 스크래퍼가 None을 반환하거나 관련 없는 요소에서 데이터를 가져오게 됩니다.

단계 2: AI 검증기 구축

검증기를 만들기 위해서는 LLM에게 QA 엔지니어 역할을 수행하도록 요청하는 프롬프트를 구성합니다. LLM은 구조화된 응답—불리언 값과 실패 이유—을 반환해야 합니다.

openai 라이브러리와 JSON Mode를 사용하여 출력이 기계가 읽을 수 있도록 합니다.

import openai
import json

client = openai.OpenAI(api_key="YOUR_API_KEY")

def validate_extraction(html_snippet: str, extracted_data: dict) -> dict:
    prompt = f"""
    You are a Data Quality Auditor. Compare extracted JSON data 
    against a raw HTML snippet to ensure accuracy.

    RAW HTML:
    {html_snippet}

    EXTRACTED JSON:
    {json.dumps(extracted_data, ensure_ascii=False, indent=2)}

    Rules:
    1. Check if the 'title' in JSON matches the main product title in HTML.
    2. Check if the 'price' in JSON matches the actual product price.
    3. Ignore minor whitespace or formatting differences.
    4. If the data is missing or incorrect, set 'is_valid' to false.

    Return ONLY a JSON object with this structure:
    {{"is_valid": boolean, "reason": "string explaining the error if invalid"}}
    """

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        response_format={"type": "json_object"},
    )

    return json.loads(response.choices[0].message.content)

왜 이렇게 작동하는가

  • 컨텍스트 격리 – 전체 100 KB HTML 파일을 전송하면 비용이 많이 들고 잡음이 많아집니다. 우리는 관련 컨테이너만 전송합니다.
  • 의미 기반 비교 – LLM은 HTML에 있는 "$2,499.99"와 JSON에 있는 "2499.99"가 형식이 달라도 동일한 값임을 이해합니다.
  • 추론 제공 – 검증에 실패하면 "reason" 필드가 즉시 디버깅 힌트를 제공합니다.

단계 3: 피드백 루프 구현

이제 검증기를 스크래핑 로직에 통합해 보겠습니다. 실제 운영 환경에서는 단일 오류 때문에 전체 크롤링을 중단하지 않아야 하지만, 오류를 로그에 남기고 오류 비율이 특정 임계값을 초과하면 스파이더를 중단해야 합니다.

def run_scraper(url: str, error_threshold: float = 0.05):
    html = requests.get(url).text
    extracted_data = extract_product_data(html)

    # Grab only the relevant HTML snippet (e.g., the product container)
    # For demonstration we just reuse the whole page; replace with a proper selector.
    html_snippet = html  # TODO: narrow this down

    validation = validate_extraction(html_snippet, extracted_data)

    if not validation["is_valid"]:
        # Log the failure and optionally retry or flag for manual review
        print(f"Validation failed for {url}: {validation['reason']}")
        # Increment error counter, etc.
    else:
        # Persist the clean data
        print("✅ Data validated:", extracted_data)

    # Example of error‑rate handling (pseudo‑code)
    # if error_rate > error_threshold:
    #     raise RuntimeError("Error rate exceeded – stopping crawl")

운영 팁

설명
배치 검증여러 아이템을 한 번에 검증하여 API 호출 수를 줄입니다 (예: 10개의 스니펫을 한 요청에 전송).
캐싱동일한 HTML 스니펫에 대한 LLM 응답을 캐시하여 비용을 절감합니다.
속도 제한OpenAI 속도 제한을 준수하고 429 응답 시 지수 백오프를 사용합니다.
관측 가능성is_validreason 필드를 모니터링 대시보드에 저장해 드리프트를 조기에 감지합니다.
폴백LLM을 사용할 수 없을 경우 구조적 검증으로 대체하고 나중에 검토하도록 플래그를 지정합니다.

요약

  1. Structural validation catches type mismatches but not context errors.
    구조적 검증은 타입 불일치를 포착하지만 컨텍스트 오류는 잡지 못합니다.

  2. AI‑driven semantic validation lets an LLM verify that the extracted value truly belongs to the intended element.
    AI 기반 의미 검증은 LLM이 추출된 값이 실제로 의도된 요소에 속하는지 확인하도록 합니다.

  3. Integrate the validator as a lightweight, optional step in your pipeline, logging failures and acting on them only when a threshold is crossed.
    → 검증기를 파이프라인의 가벼운 선택적 단계로 통합하고, 실패를 로그에 기록하며 임계값을 초과했을 때만 조치를 취합니다.

By adding an AI Judge to your scraper, you turn silent failures into actionable alerts, dramatically reducing the time spent debugging broken selectors in production. Happy scraping!
AI 판사를 스크래퍼에 추가하면, 조용히 발생하는 실패를 실행 가능한 알림으로 전환하여 프로덕션에서 깨진 셀렉터를 디버깅하는 데 소요되는 시간을 크게 줄일 수 있습니다. 즐거운 스크래핑 되세요!

토큰

soup = BeautifulSoup(html, 'html.parser')
container = str(soup.select_one(".product-main-area"))

validation_result = validate_extraction(container, extracted_data)

if not validation_result['is_valid']:
    print(f"CRITICAL: Validation failed for {url}")
    print(f"Reason: {validation_result['reason']}")
    # Log to your monitoring system (e.g., Sentry or ScrapeOps)
    return None

return extracted_data

최적화: 비용 및 성능

모든 요청을 LLM에 보내면 스크레이퍼가 느려지고 비용이 많이 듭니다. 100,000 페이지를 스크레이핑한다면 페이지당 $0.01 API 호출이 $1,000이 됩니다. 통계적 샘플링을 사용해 이를 최적화하세요.

1. 샘플링

모든 행을 검증할 필요는 없습니다. 데이터의 1 %만 확인해도 사이트 전체 레이아웃 변경을 포착하기에 충분한 경우가 많습니다.

import random

def should_validate(rate=0.01):
    return random.random() < rate

# In your loop
if should_validate(rate=0.05):   # Validate 5 % of requests
    validation_result = validate_extraction(html, data)

2. 모델 선택

간단한 비교 작업에 GPT‑4o를 사용하지 마세요. gpt-4o-miniclaude-3-haiku 같은 모델은 훨씬 저렴하면서도 JSON을 HTML과 비교하는 데 충분히 능숙합니다. 또한 지연 시간도 크게 낮습니다.

3. 신뢰도 기반 트리거

로컬 코드가 **“확신이 없을 때”**만 AI 판사를 호출하세요. 예를 들어, 셀렉터가 빈 문자열을 반환하거나 정규식 패턴이 실패할 경우, 해당 HTML을 LLM에 전달해 누락된 데이터를 찾아 달라고 요청합니다.

마무리

AI를 활용한 스키마 검증 자동화는 웹 스크래핑을 “손가락을 교차하는” 접근 방식에서 엄격한 엔지니어링 дисциплина로 전환시킵니다. LLM을 의미론적 QA 레이어로 사용하면 데이터 세트를 손상시키기 전에 조용히 발생하는 실패를 포착할 수 있습니다.

핵심 요점

  • 구조적 검증(Pydantic)은 데이터 유형 오류를 잡아내고, 의미론적 검증(AI)은 컨텍스트 오류를 잡아냅니다.
  • 컨텍스트 격리가 필수입니다 – 비용을 절감하고 정확성을 높이기 위해 관련 HTML 조각만 LLM에 전송하세요.
  • 샘플링 사용으로 파이프라인을 효율적이고 비용 효과적으로 유지하세요.
  • 구조화된 출력을 통해 AI 피드백을 코드 로직에 직접 통합할 수 있습니다.

다음 단계

검증 프로세스를 시작하기 전에 대상 사이트에서 고품질 HTML을 받아오는지 확인하려면 ScrapeOps Proxy Provider를 사용해 보세요. 성공적인 데이터 추출은 올바른 도구로 시작해 신뢰할 수 있는 검증으로 마무리됩니다.

0 조회
Back to Blog

관련 글

더 보기 »

노트북 GPU의 숨겨진 힘을 풀어내기

개요 대부분의 최신 노트북은 강력한 GPU를 탑재하고 있지만, 이를 충분히 활용하지 못하는 경우가 많습니다. 소프트웨어 엔지니어로서 로컬 LLM을 실행하든, 데이터 사이언티스트이든...