본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 09. 01:19

CSS 선택어를 포기했습니다: 웹 스크래핑(Web Scraping)을 위한 LLM 활용

요약

기존 CSS 선택어 기반 웹 스크래핑의 취약성을 극복하기 위해 LLM을 활용하는 새로운 접근 방식을 소개합니다. HTML 구조 변화에 민감한 전통적 방식 대신, LLM의 자연어 이해 능력을 이용해 데이터의 의도를 파악하고 추출하는 방법을 다룹니다.

핵심 포인트

  • CSS 선택어 기반 스크래핑은 사이트 구조 변경에 매우 취약함
  • LLM을 활용하면 HTML 구조 대신 데이터의 의도를 파악해 추출 가능
  • html2text 등을 사용해 텍스트를 간소화하여 토큰 제한 문제 해결
  • 비용 증가와 토큰 제한은 LLM 스크래핑의 주요 트레이드오프

몇 달 전, 저는 수십 개의 이커머스 사이트 간의 가격을 비교해야 하는 작은 사이드 프로젝트를 진행하고 있었습니다. 간단해 보이죠? 맞나요? 저는 이전에도 스크래퍼(Scraper)를 작성해 본 적이 있습니다. BeautifulSoup, 몇 가지 영리한 선택어(Selector), 그리고 정규 표현식(Regex) 한두 개 정도면 충분하니까요.

하지만 실제로는 그렇지 않았습니다. 사이트마다 각기 다른 HTML 구조를 가지고 있었습니다. 어떤 사이트는 JavaScript를 통해 콘텐츠를 로드했고, 다른 사이트들은 의도적으로 클래스(Class) 이름을 엉망으로 만들어 놓았습니다. 저는 실제 로직을 작성하는 시간보다 선택어를 업데이트하는 데 더 많은 시간을 소비했습니다. 저는 해당 상점의 개발자가 div 이름을 바꿀 때마다 무너져 내리는 취약한 CSS 선택어의 탑을 유지보수하고 있었습니다.

좌절한 저는 다음과 같은 생각을 하기 시작했습니다. 'HTML 구조를 파싱(Parsing)하려고 애쓰는 대신, 사람에게 페이지를 읽고 데이터를 달라고 요청하면 어떨까?' 하지만 저에게는 사람이 없습니다. 저에게는 API가 있습니다.

시도했지만 실패했던 것들

먼저, 당연한 방법인 requests + BeautifulSoup 조합입니다. 이는 약 40%의 사이트에서만 작동했습니다. 나머지는 JavaScript 렌더링(Selenium)이 필요하거나, 마크업(Markup)이 너무 혼란스러워 선택어가 계속 깨졌습니다. CSS 선택어 체이닝(Chaining), XPath, 심지어 페이지상의 위치를 기준으로 스크래핑하는 방법도 시도해 보았습니다. 그 어떤 것도 견고하지 않았습니다.

그 후 헤드리스 브라우징(Headless browsing)을 위해 Playwright로 전환했습니다. 이는 JavaScript 문제를 해결해 주었지만, 파싱은 여전히 취약했습니다. .product-price와 같은 선택어를 작성하면, 다음 주에는 .price--main으로 바뀌어 있었습니다. 저에게는 구조가 아닌 의도(Intent)를 이해하는 무언가가 필요했습니다.

깨달음: LLM에게 물어보기

저는 다른 작업들을 위해 OpenAI의 API를 만져보고 있었는데, 문득 생각이 스쳤습니다. '왜 원본 HTML(또는 가시적인 텍스트)을 언어 모델(Language Model)에 입력하고 내가 필요한 필드를 추출해 달라고 요청하지 않을까?' LLM은 자연어(Natural language)를 이해하는 데 능숙합니다. 만약 제가

저는 몇몇 페이지를 대상으로 이를 테스트해 보았습니다. HTML을 가시적인 텍스트로만 간소화하고(html2text를 사용하거나 모든 텍스트 노드(text nodes)를 추출하는 방식), 이를 프롬프트(prompt)와 함께 전송했습니다. 결과는 놀라울 정도로 좋았습니다. 모델은 "$49.99"와 "49.99 USD" 같은 변형을 처리할 수 있었고, 존재하지 않는 데이터를 만들어내는 환각(hallucination) 현상도 거의 발생하지 않았습니다.

코드 예시: 간단한 LLM 기반 스크래퍼

다음은 OpenAI의 API를 사용한 최소한의 Python 구현 예시입니다. 핵심은 토큰 제한(token limits)을 피하기 위해 텍스트를 잘라내는(truncate) 것프롬프트에 명확한 지침을 제공하는 것입니다.

import openai
import requests
from bs4 import BeautifulSoup
...

이 방식은 사이트마다 코드 한 줄을 수정할 필요 없이 제가 목표로 한 사이트의 대부분에서 작동했습니다. 저는 그저 텍스트를 입력하기만 하면 모델이 필드(fields)를 추출해 냈습니다.

트레이드오프(Trade-offs) 및 사용하지 말아야 할 경우

솔직히 말씀드리면, 이것이 만능 해결책(silver bullet)은 아닙니다. 제가 겪은 단점들은 다음과 같습니다:

  • 비용 (Cost): GPT-3.5-turbo를 사용하면 페이지 스크래핑당 1센트 미만의 비용이 들지만, 매일 수천 개의 페이지를 스크래핑한다면 비용이 쌓이게 됩니다. 개인 프로젝트나 작은 데이터셋의 경우에는 괜찮습니다.
  • 지연 시간 (Latency): API 호출은 페이지당 1~3초가 소요됩니다. 10,000개의 페이지를 스크래핑해야 한다면 몇 시간이 걸립니다. 전통적인 파싱(parsing) 방식은 거의 즉각적입니다.
  • 정확도 (Accuracy): LLM은 여전히 실수를 할 수 있습니다. 때때로 할인된 가격을 정상 가격으로 오해하거나, 제품이 일시적으로 품절된 상태일 때 "품절(out of stock)"이라고 반환하기도 합니다. 저는 검증 단계(예: 가격에 숫자가 포함되어 있는지 확인)를 추가해야 했습니다.
  • 개인정보 보호 (Privacy): 페이지 콘텐츠를 제3자 API로 전송하게 됩니다. 데이터가 민감한 경우(예: 내부 대시보드), 이 방식을 사용하지 마세요. 로컬 모델(아래 대안 참조)을 사용하십시오.
  • 토큰 제한 (Token limits): OpenAI 모델에는 컨텍스트 창(context window)이 있습니다. 100KB 전체 HTML을 보낼 수는 없습니다. 텍스트를 자르면 중요한 문맥(context)을 잃을 수 있습니다. 더 스마트한 접근 방식은 전체 HTML을 포함하되 더 큰 컨텍스트를 가진 모델(예: 128k 토큰을 지원하는 GPT-4-1106)을 사용하는 것이지만, 이는 비용이 더 많이 듭니다.

대안 및 개선 사항

특정 벤더 종속(vendor lock-in)을 피하거나 비용을 절감하고 싶다면 다음 사항을 고려해 보세요:

  • 로컬 모델 (Local models): Ollama를 사용하여 Llama 3.1 8B 또는 Phi-3와 같은 작은 LLM을 로컬에서 실행합니다. 속도와 개인정보 보호는 향상되지만, 정확도는 떨어질 수 있습니다. 저는 간단한 추출 작업에 Llama 3.1을 사용하여 괜찮은 결과를 얻었습니다.
  • 하이브리드 접근 방식 (Hybrid approach): 구조가 안정적인 사이트에는 CSS 선택자 (CSS selectors)를 사용하고, 파싱(parsing)에 실패할 때만 LLM으로 전환합니다. 이는 속도와 유연성을 결합한 방식입니다.
  • 프롬프트 엔지니어링 (Prompt engineering): 정확도를 높이기 위해 프롬프트에 퓨샷 예시 (few-shot examples)를 제공합니다. 저는 또한

AI 보조 스크래핑 (AI-assisted scraping)을 시도해 보신 적이 있나요? 여러분에게는 어떤 방식이 효과적이었나요 (혹은 효과가 없었나요)? 스크래핑을 방해하는 사이트들을 어떻게 처리하고 계신지 정말 궁금합니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0