정규표현식(Regex)에 지쳐 AI 데이터 추출기를 직접 만들게 된 이유
요약
정규표현식과 BeautifulSoup을 이용한 전통적인 웹 스크래핑의 한계를 극복하기 위해 LLM 기반의 데이터 추출 방식을 제안합니다. HTML 구조 변화에 유연하게 대응할 수 있도록 의미론적 이해를 활용한 AI 추출 파이프라인 구축 과정을 다룹니다.
핵심 포인트
- 정규표현식과 HTML 파싱은 사이트 구조 변경에 매우 취약함
- LLM을 활용하면 마크업 대신 의미론적 데이터 추출 가능
- LangChain과 GPT-4를 결합한 JSON 추출 워크플로우 구축
- 비용 절감을 위해 HTML 노이즈 제거 및 모델 선택이 중요함
저는 수년 동안 웹 스크래핑을 해왔습니다. 그것은 애증의 관계입니다. 필요한 데이터를 마침내 뽑아냈을 때의 짜릿함, 그리고 사이트가 재설계되어 모든 것이 망가졌을 때 느끼는 절망감 같은 식이죠. 지난달에 저는 한계에 부딪혔습니다. 수십 개의 이커머스 페이지에서 제품 사양을 추출해야 했습니다. 각 페이지에는 동일한 데이터(이름, 가격, 설명, 치수)가 있었지만, HTML 구조는 제각기였습니다. 어떤 것은 <dl> 태그를 사용했고, 어떤 것은 <table> 태그를 썼으며, 어떤 것은 인라인 CSS만 들어간 <div> 덩어리였습니다. 저의 믿음직한 정규표현식(regex)과 BeautifulSoup 파이프라인은 조건문으로 가득 찬 악몽이 되어갔습니다.
정규표현식의 심연 (The regex abyss)
저는 낙관적으로 시작했습니다. 몇 가지 패턴을 작성하고, 테스트하고, 반복하는 식이었죠. 하지만 곧 제 코드는 이렇게 보였습니다:
import re
from bs4 import BeautifulSoup
...
이것은... 약 세 페이지까지는 작동했습니다. 그러더니 새로운 사이트는 '$' 대신 '€'를 사용하거나, 가격을 JavaScript 객체 안에 임베딩해 놓았습니다. 저는 패턴을 더 추가했습니다. 그러자 또 다른 사이트는 가격 이미지를 사용했습니다. 저는 조금 울었습니다.
BeautifulSoup의 미로 (The BeautifulSoup maze)
저는 더 똑똑해지려고 노력했습니다. HTML 구조를 파싱하는 것이었죠. 하지만 모든 사이트가 고유한 레이아웃을 가지고 있었습니다. 저는 모든 일반적인 선택자(selector)를 시도하는 함수를 작성했습니다:
def find_price(soup):
for selector in [
'.price', '.product-price', '#price',
...
좋았지만, 여전히 취약했습니다. 한 사이트는 .prc를 사용했고, 다른 사이트는 실제로는 이전 가격인 <s> 태그 안에 가격을 넣었습니다. 오탐지(false positives)가 쌓여갔습니다. 저는 다른 접근 방식이 필요하다는 것을 깨달았습니다.
전구의 순간: 자연어 이해 문제처럼 다루기 (treat it like a natural language understanding problem)
저는 제가 정말로 원하는 것은 페이지를 인간처럼 읽는 것, 즉 마크업(markup)은 무시하고 의미론적 의미(semantic meaning)만 이해하는 것이라는 것을 깨달았습니다. 이것이 바로 대규모 언어 모델(LLMs)이 잘하는 일입니다. HTML과 싸우기보다 AI에게 데이터를 추출해 달라고 요청할 수 있는데, 왜 직접 싸우려고 할까요?
아이디어는 이렇습니다: 원본 HTML(또는 정리된 텍스트 버전)을 LLM에 입력하고
저는 OpenAI의 GPT-4와 함께 LangChain을 사용하기로 선택했습니다 (하지만 나중에 더 저렴한 대안들을 찾았습니다). 핵심 아이디어는 다음과 같습니다:
- HTML을 가져옵니다 (Fetch).
- script/style 태그를 제거하고 노이즈를 줄입니다 (선택 사항이지만 비용 절감에 도움이 됩니다).
- 텍스트와 프롬프트 (Prompt)를 LLM에 보내 JSON 응답을 요청합니다.
- JSON을 파싱 (Parse)합니다.
예시 코드
import requests
from bs4 import BeautifulSoup
from langchain.chat_models import ChatOpenAI
...
```
(?:json)?\s*([\s\S]*?)
```', response.content)
if match:
result = json.loads(match.group(1))
else:
...
비용과 속도의 트레이드오프 (Trade-offs)
이 방식은 공짜가 아닙니다. GPT-4에 대한 요청 한 번은 입력 크기에 따라 약 $0.03–$0.10 정도의 비용이 듭니다. 제품 100개에 대해서는 $3–10가 소요됩니다. 속도 또한 더 느립니다 (페이지당 2–5초). 저는 다음과 같은 방법으로 이를 완화했습니다:
- 단순한 페이지에는 GPT-3.5-turbo 사용 (훨씬 저렴하며, 호출당 약 $0.001).
- 입력 크기 축소: 제품 영역 주변의 가시적인 텍스트만 전송 (XPath나 CSS를 사용하여 주요 콘텐츠 추출).
- 배치 (Batching): 한 페이지에 여러 아이템이 있는 경우, 한 번의 호출로 모두 요청.
실패하는 경우
LLM은 완벽하지 않습니다. 가격이 존재하지 않는데 가격을 지어내거나, 이름과 설명을 혼동하는 환각 (Hallucination) 현상을 목격했습니다. 이를 방지하기 위해, 저는 항상 예상되는 타입(Type)과 범위에 따라 출력을 검증합니다 (예: 가격은 \d+\.\d{2}와 일치해야 함). 또한, 무작위성을 줄이기 위해 temperature=0으로 설정합니다.
또 다른 한계점은, 페이지가 대부분 JavaScript로 렌더링되는 경우 먼저 헤드리스 브라우저 (Headless browser)가 필요하다는 것입니다. 이는 복잡성을 더합니다.
고려했던 대안들
- ai.interwestinfo.com과 같은 상용 API (Commercial APIs) (개인적으로 사용해 보지는 않았지만, 유사한 서비스를 제공합니다). 장점은 API 키나 프롬프트를 관리할 필요가 없다는 것이며, 단점은 벤더 종속 (Vendor lock-in) 및 잠재적으로 더 높은 요청당 비용입니다.
- Ollama를 통한 로컬 모델 (Local models) (LLaMA, Mistral): 무료이지만 추출 작업에 있어 더 느리고 정확도가 낮습니다.
- 파인튜닝 (Fine-tuning): 일회성 프로젝트에는 과하지만, 반복적인 도메인이라면 가치가 있을 수 있습니다.
배운 점
- 형식과 싸우지 마세요. 데이터가 비정형(unstructured)이라면, 마크업(markup)이 아닌 언어를 이해하는 모델을 사용하세요.
- LLM 추출은 대체재가 아닌 보완재입니다. 구조가 잘 잡힌 페이지의 경우, 전통적인 파싱(parsing) 방식이 더 빠르고 저렴합니다.
- 프롬프트 엔지니어링 (Prompt engineering)이 매우 중요합니다. 잘못 작성된 프롬프트는 쓰레기(garbage)를 반환합니다. 실험하고 반복하세요.
다음에 다시 한다면 다르게 할 점
규모를 키우기 전에 5~10개의 대표적인 페이지로 구성된 작은 테스트 스위트(test suite)로 시작하여 정확도를 평가하겠습니다. 또한 더 구조화된 출력 형식을 사용할 것입니다. LangChain은 스키마(schema)를 강제하는 PydanticOutputParser를 제공합니다. 이를 통해 환각 (hallucination) 현상을 조기에 잡아낼 수 있을 것입니다.
마치며
안정적인 API나 일관된 HTML의 경우 정규표현식 (Regex)과 BeautifulSoup은 여전히 제가 가장 선호하는 도구입니다. 하지만 혼란의 정도가 10점 만점에 7점을 넘어설 때면, 이제 저는 AI 모델을 찾습니다. 이는 어떤 페이지든 읽을 수 있는 주니어 개발자를 둔 것과 같습니다. 단지 조금 더 느리고 비용이 더 들 뿐입니다.
변동성이 극심한 웹 페이지를 다루는 여러분만의 접근 방식은 무엇인가요? 패턴 매칭 (pattern matching)을 고수하시나요, 아니면 AI 추출을 시도해 보셨나요?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기