본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 28. 17:10

CSS 선택자만으로는 부족할 때: 데이터 추출을 위한 LLM 활용법

요약

기존 CSS 선택자나 정규 표현식을 이용한 웹 스크레이핑의 한계를 LLM을 활용한 구조화된 데이터 추출 방식으로 해결하는 방법을 소개합니다. HTML의 구조적 변화에 관계없이 콘텐츠의 의미를 파악하여 JSON 형태로 데이터를 추출하는 실무적인 접근법을 다룹니다.

핵심 포인트

  • CSS 선택자 기반 스크레이핑의 유지보수 어려움 해결
  • LLM을 활용한 비정형 HTML의 구조화된 JSON 추출
  • HTML 레이아웃 변경에 대한 높은 회복탄력성 확보
  • 비용 및 지연 시간(Latency) 측면의 트레이드오프 고려 필요

몇 달 전, 저는 단순해 보이는 사이드 프로젝트를 하나 맡았습니다. 몇몇 이커머스 사이트에서 제품 상세 정보를 스크레이핑(Scrape)하여 가격 비교 도구를 만드는 것이었습니다. 저는 이전에 BeautifulSoup, Scrapy와 같은 일반적인 도구들을 사용하여 웹 스크레이핑(Web Scraping)을 해본 경험이 있었습니다. 얼마나 어렵겠습니까?

결과적으로, 정말 어려웠습니다. 각 사이트마다 고유한 HTML 구조를 가지고 있었습니다. 어떤 곳은 JavaScript 렌더링(Rendering)을 사용했고, 다른 곳은 매주 클래스(Class) 이름을 변경했습니다. 제가 정성스럽게 만든 CSS 선택자(CSS Selectors)는 끊임없이 깨졌습니다. 데이터를 실제로 추출하는 시간보다 선택자를 디버깅하는 데 더 많은 시간을 보냈습니다.

정규 표현식(Regex)도 시도해 보았습니다. XPath도 시도해 보았습니다. Puppeteer를 이용한 헤드리스 브라우저(Headless Browsers)까지 사용해 보았습니다. 하지만 아무것도 통하지 않았습니다. 문제는 도구가 아니라, 데이터가 비정형적이고 사람이 읽기 쉬운 페이지 속에 파묻혀 있다는 점이었습니다. 저는 단순히 DOM 내에서의 위치가 아니라, 콘텐츠의 '의미'를 이해할 수 있는 방법이 필요했습니다.

한계점 (The Breaking Point)

한 사이트는 제품 가격이 클래스(Class)가 없는 <span> 태그 안에 숨겨져 있었고, 테이블 레이아웃의 3단계 깊이에 중첩되어 있었습니다. 또 다른 사이트는 가격을 이미지로 렌더링하는 커스텀 폰트를 사용했습니다. 포기하려던 찰나에 이런 생각이 들었습니다. '페이지 전체를 하나의 문서로 취급하고, LLM(Large Language Model)에게 내가 필요한 필드를 추출해 달라고 요청하면 어떨까?'

저는 코드 생성용으로 GPT-4를 사용해 왔지만, 추출용으로 사용해 본 적은 없었습니다. 이 아이디어는 과해 보였습니다. 하지만 사이트당 3시간씩 소비하는 것 또한 과했습니다.

접근 방식: 비정형 HTML로부터의 구조화된 추출 (Structured Extraction from Unstructured HTML)

핵심 아이디어는 간단합니다. 원하는 스키마(Schema)를 설명하는 프롬프트(Prompt)와 함께 원본 HTML(또는 렌더링된 텍스트)을 언어 모델에 입력하는 것입니다. 모델은 JSON을 반환합니다. 선택자도, 정규 표현식도, 깨지기 쉬운 파싱(Parsing)도 필요 없습니다.

다음은 Python과 OpenAI의 API를 사용한 최소한의 예시입니다:

import openai
from bs4 import BeautifulSoup

...

이 방식은 놀라울 정도로 잘 작동했습니다. 전형적인 제품 페이지의 경우, 다음과 같은 결과를 얻을 수 있었습니다:

{
  "name": "Wireless Bluetooth Headphones",
  "price": 49.99,
...

가격이 이미지의 alt 텍스트에 있거나 문단 속에 파묻혀 있는 경우에도, LLM은 종종 이를 정확하게 추론해 냈습니다.

현실적인 트레이드오프 (Real-World Trade-offs)

이 방식이 마법 같은 것은 아닙니다. 제가 배운 점은 다음과 같습니다:

장점 (Pros):

  • HTML 변경에 대한 회복탄력성 (Resilient to HTML changes): 사이트의 레이아웃이 재설계되더라도 LLM은 여전히 콘텐츠를 이해합니다.
  • JavaScript 렌더링 페이지 지원: Puppeteer 또는 Playwright를 통해 텍스트를 먼저 추출한다면, JavaScript로 렌더링되는 페이지에서도 작동합니다.
  • 다국어 처리: 여러 언어를 상당히 잘 처리합니다.

단점 (Cons):

  • 비용 (Cost): GPT-4는 3k 토큰당 요청당 약 0.03달러가 소요됩니다. 수백 페이지 정도라면 수용 가능하지만, 수백만 페이지라면 불가능합니다.
  • 지연 시간 (Latency): 요청당 2~5초가 소요됩니다. 실시간 스크래핑(Real-time scraping)에는 적합하지 않습니다.
  • 환각 (Hallucination): 프롬프트(Prompt)가 모호하면 모델이 데이터를 지어낼 수 있습니다. 항상 스키마(Schema)와 폴백(Fallback)을 통해 검증해야 합니다.
  • 토큰 제한 (Token limits): 페이지가 길면 잘라내기(Truncation)나 청킹(Chunking)이 필요하며, 이 과정에서 문맥(Context)을 잃을 수 있습니다.

이 방식을 사용하지 말아야 할 때

  • 구조가 잘 잡힌 단일 사이트를 스크래핑하는 경우라면, CSS 선택자(CSS selectors)가 더 빠르고 저렴합니다.
  • 실시간 추출(예: 실시간 가격)이 필요한 경우, 이 방식은 너무 느립니다.
  • 수백만 페이지를 처리해야 한다면, 비용이 빠르게 불어납니다.

저는 현재 하이브리드 접근 방식(Hybrid approach)을 사용합니다. 먼저 간단한 선택자를 시도하고, 선택자가 실패할 때 LLM으로 폴백(Fallback)합니다. 이렇게 하면 회복탄력성을 유지하면서 비용을 낮게 유지할 수 있습니다.

이 방식에 영감을 준 도구

조사하던 중, LLM을 사용하여 웹 페이지에서 구조화된 데이터를 추출하는 Interwest AI라는 서비스를 우연히 발견했습니다. 저는 완전한 제어권을 원했기 때문에 결국 사용하지는 않았지만, 이 방식이 실행 가능하다는 것을 확인시켜 주었습니다. 그들의 문서는 프롬프트 엔지니어링(Prompt engineering)과 스키마 설계(Schema design)에 대한 아이디어를 제공해 주었습니다.

다르게 시도했을 점

  1. 더 나은 프롬프트 엔지니어링 (Better prompt engineering): 날짜나 평점처럼 까다로운 필드에 대해 퓨샷 예시(Few-shot examples)를 추가했을 것입니다.
  2. 캐싱 (Caching): 변경되지 않은 페이지를 다시 처리하지 않도록 URL 해시(URL hash)별로 추출된 결과를 캐싱했을 것입니다.
  3. 비동기 처리 (Async): asyncio를 사용하여 요청을 병렬화하고 총 소요 시간을 줄였을 것입니다.
  4. 검증 (Validation): Pydantic을 사용하여 LLM 출력을 파싱하고 잘못된 형식의 JSON을 잡아냈을 것입니다.

배운 점 (Lessons Learned)

  • 가장 좋은 도구는 문제에 따라 다릅니다. 동적이고 지저분한 데이터의 경우, LLM은 게임 체인저 (game-changer)입니다 (죄송합니다, 이 단어는 쓰지 않겠다고 했는데—그냥 "매우 유용하다"라고 하죠).
  • 과도한 엔지니어링 (over-engineer)을 하지 마세요. 단순한 프롬프트 (prompt)로 시작하여 반복적으로 개선하세요.
  • 규모를 확장하기 전에 항상 비용과 지연 시간 (latency)을 측정하세요.

여러분의 차례

데이터 추출을 위해 LLM을 사용해 보셨나요? 여러분의 설정은 어떤 모습인가요? 혹시 이 작업을 위해 더 작은 모델을 미세 조정 (fine-tuning)해 본 분이 계신지 궁금합니다. 대량의 스크래핑 (scraping)을 위한 더 저렴한 대안이 될 수 있을 것 같습니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0