정규 표현식(Regex)에서 LLM까지: 비정형 웹 데이터를 추출하기 위한 나의 여정
요약
비정형 웹 데이터 추출을 위해 정규 표현식과 BeautifulSoup을 사용하던 방식에서 LLM을 활용한 방식으로 전환하는 과정을 다룹니다. HTML 전처리와 LLM의 구조화된 출력(JSON mode, Function calling)을 결합하여 비용과 정확도 문제를 해결하는 실무적인 가이드를 제공합니다.
핵심 포인트
- 전통적인 Regex/BeautifulSoup 방식은 웹사이트 레이아웃 변경에 취약함
- 가공되지 않은 HTML을 LLM에 직접 입력하면 비용과 환각 문제가 발생함
- HTML 전처리를 통해 토큰 사용량을 줄이고 핵심 정보만 추출해야 함
- JSON 모드나 함수 호출을 사용하여 LLM의 응답을 구조화하는 것이 필수적임
몇 달 전, 저는 한계에 부딪혔습니다. 수십 개의—정말 수십 개라고 말씀드리고 싶습니다—이커머스 사이트에서 제품 사양을 가져와야 하는 가격 비교 도구를 만들고 있었습니다. 사이트마다 HTML 구조가 제각각이었고, CSS 클래스는 무작위 문자열처럼 보였으며, 어떤 사이트는 JavaScript를 통해 콘텐츠를 제공하기도 했습니다. 저의 초기 계획은 무엇이었을까요? 바로 정규 표현식 (Regex)과 BeautifulSoup이었습니다. 아시다시피, 아주 고전적인 방식이죠.
실제로 어떤 일이 일어났는지—막다른 길, 깨달음의 순간, 그리고 마침내 제가 오늘날까지도 사용하고 있는 실용적인 절충안에 대해 말씀드리겠습니다.
첫 번째 시도: BeautifulSoup + Regex
저는 각 사이트별로 스크래퍼 (Scraper)를 작성하는 것부터 시작했습니다. BeautifulSoup은 DOM을 탐색하기 쉽게 해주었고, 저는 가격, 설명, 사양을 추출하기 위해 정규 표현식 (Regex) 패턴을 만들었습니다. 예를 들면 다음과 같습니다:
import re
from bs4 import BeautifulSoup
import requests
...
정확히 두 개의 사이트에서는 작동했습니다. 하지만 한 사이트가 레이아웃을 재설계하자 제 정규 표현식 (Regex)은 깨져버렸습니다. 또 다른 사이트는 동적 콘텐츠 (Dynamic content)를 사용하기 시작했습니다. 저는 데이터를 실제로 사용하는 시간보다 스크래퍼 (Scraper)를 수정하는 데 더 많은 시간을 보냈습니다. 다른 접근 방식이 필요했습니다.
두 번째 시도: 기성 LLM 활용 – 하지만 엉망진창이었습니다
"그냥 AI를 써보자"라고 생각했습니다. 저는 가공되지 않은 HTML을 LLM에 입력하고 관련 필드를 추출해 달라고 요청했습니다. 저의 첫 번째 프롬프트 (Prompt)는 대략 다음과 같았습니다 (여기서는 일반적인 API를 사용하지만, 어떤 제공업체로든 교체할 수 있습니다):
import openai
response = openai.chat.completions.create
...
출력 결과는 일관성이 없었습니다. 때로는 JSON을 반환했고, 때로는 문단을 반환했습니다. 사양 값을 환각 (Hallucination)하기도 했으며, 매 요청마다 수천 토큰(Tokens)의 HTML을 전송했기 때문에 비용이 치솟았습니다. 저에게는 구조화된 출력 (Structured output)과 더 낮은 비용이 필요했습니다.
결국 성공한 방법: 구조화된 출력 + 전처리
저는 두 가지 아이디어를 결합했습니다:
- HTML 전처리 (Preprocess) – 스크립트 (Scripts)와 스타일 (Styles)을 제거하고 가시적인 텍스트 라인으로 축소합니다.
- 함수 호출 (Function calling) 또는 JSON 모드 (JSON mode)를 사용하는 LLM 활용 – 구조화된 응답을 강제합니다.
현재 제가 사용하는 접근 방식의 핵심은 다음과 같습니다. 참고: 설정의 API 엔드포인트는 하나의 옵션일 뿐이며, 호환 가능한 모든 서비스로 지정할 수 있습니다. (제 설정에서는 몇 가지를 테스트한 후 https://ai.interwestinfo.com/의 엔드포인트를 사용했습니다.)
import json
import re
from bs4 import BeautifulSoup
...
오류 처리 및 비용 절감 (Handling Errors and Reducing Cost)
이 방식은 잘 작동했지만, 여전히 다음과 같은 문제들이 발생했습니다:
- 토큰 제한 (Token limits) – 많은 제품 페이지가 매우 방대합니다. 저는 처음 8,000자까지만 자릅니다 (보통 이 정도면 핵심 정보를 포함합니다).
- 속도 제한 (Rate limits) – 간단한 지수 백오프 (Exponential backoff) 재시도 로직을 추가했습니다.
- 비용 (Cost) – 동일한 페이지에 대해 다시 쿼리하지 않도록 URL별로 성공적인 추출 데이터를 캐싱 (Cache)했습니다.
- 불일치 (Inconsistency) – JSON이 스키마 (Schema)와 일치하지 않으면 최대 3번까지 재시도합니다.
다음은 제가 사용하는 재시도 데코레이터 (Retry decorator)의 스니펫입니다:
import time
from functools import wraps
...
교훈 및 트레이드오프 (Lessons Learned & Trade‑offs)
- 잘 구조화된 정적 페이지에는 여전히 정규 표현식 (Regex)이나 BeautifulSoup이 더 낫습니다. 사이트가 일관된 CSS 클래스를 사용하고 레이아웃이 거의 변하지 않는다면, 파서 (Parser)가 더 빠르고 비용도 들지 않습니다.
- LLM 추출은 구조를 예측할 수 없거나 자주 변경될 때 빛을 발합니다. 하지만 마법은 아닙니다. 비용을 합리적으로 유지하려면 반드시 입력을 전처리 (Preprocess)해야 합니다.
- 지표 (Metrics)가 필요합니다. 저는 추출당 평균 비용과 성공률을 모니터링합니다. 특정 사이트에서 지속적으로 실패할 경우, 타겟형 스크레이퍼 (Scraper)로 전환합니다.
- 지연 시간 (Latency)은 실제적인 문제입니다. 각 LLM 호출에는 2~5초가 소요됩니다. 많은 페이지를 처리할 때는 비동기 (Async) 방식을 사용하여 병렬화하되, 속도 제한 (Rate limits)을 주의해야 합니다.
- 출력을 맹목적으로 신뢰하지 마세요. 저는 추출된 가격이 실제 가격처럼 보이는지($, 숫자 포함 여부) 확인하는 검증 단계를 추가했습니다. 그렇지 않으면 수동 검토 대상으로 플래그 (Flag)를 지정합니다.
다음에 한다면 다르게 할 점 (What I’d Do Differently Next Time)
첫날부터 하이브리드 접근 방식 (hybrid approach)을 사용했을 것입니다. 쉬운 사이트들을 위한 가벼운 파서 (parser)와, 지저분한 사이트들을 위한 LLM 진공청소기 (LLM vacuum)를 병행하는 방식 말입니다. 또한 토큰 수 (token counts)를 낮게 유지하기 위해, 좋은 텍스트 추출 단계 (readability와 유사한 알고리즘 사용 등)를 구축하는 데 더 많은 시간을 투자했을 것입니다. 그리고 API 청구서 때문에 심장마비가 오기 전에 반드시 비용 알림 (cost alerts)을 설정해 두었을 것입니다.
이 접근 방식이 완벽하지는 않지만, 저의 정신 건강을 지켜주었습니다. 이제는 새로운 상점을 추가하는 데 하루가 아닌 한 시간도 채 걸리지 않습니다.
지저분한 웹 데이터를 추출하기 위한 여러분만의 전략은 무엇인가요? 특정 사례에서 LLM보다 더 효과적인 도구나 기술을 찾으셨나요? 여러분의 실전 경험담 (war stories)을 듣고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기