본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 15. 05:50

정규표현식(Regex)이 내 스크레이퍼를 망쳤다: 견고한 데이터 추출을 위한 LLM 활용법

요약

정규표현식과 CSS 선택자를 이용한 전통적인 웹 스크레이핑 방식의 한계를 LLM을 활용해 극복하는 방법을 다룹니다. HTML을 간소화하여 LLM에 전달함으로써 웹사이트 구조 변경에 관계없이 견고하게 데이터를 추출하는 워크플로우를 소개합니다.

핵심 포인트

  • 정규표현식과 CSS 선택자는 웹사이트 레이아웃 변경에 취약함
  • LLM을 활용하면 비정형 HTML 데이터에서 구조화된 JSON 추출 가능
  • 비용 절감을 위해 BeautifulSoup으로 HTML을 사전 정제하는 과정이 필수적임
  • LLM은 문맥을 이해하여 통화 기호나 가용성 상태를 정확히 파악함

저는 수년간 스크레이퍼(Scraper)를 구축해 왔습니다. 작업 방식은 익숙합니다. CSS 선택자(Selector)를 찾고, 정규표현식(Regex)을 작성하고, 테스트하고, 배포한 뒤, 다음 주에 웹사이트의 마크업(Markup)이 바뀌지 않기를 기도하는 것이죠. 하지만 지난달, 저는 벽에 부딪혔습니다. 200개가 넘는 공급업체 웹사이트에서 제품 가격과 재고 상태를 추출하는 과제를 맡게 되었습니다. 각 사이트는 저마다의 레이아웃을 가지고 있었고, 어떤 곳은 JavaScript로 렌더링되었으며, 어떤 곳은 일반 HTML이었습니다. 저의 초기 접근 방식은 BeautifulSoup, XPath, 그리고 정규표현식(Regex)이라는 기존의 조합을 사용하는 것이었습니다. 그것은... 약 일주일 동안은 잘 작동했습니다.

그러던 중 가장 큰 공급업체 중 하나가 디자인을 개편했습니다. 제가 정성스럽게 만든 선택자인 .price--current가 사라져 버렸습니다. 가격은 이제 _3xj0a _2v9j3와 같은 동적 클래스 이름을 가진 중첩된 <span> 태그 안에 들어가 있었습니다. 정규표현식(Regex)요? 꿈도 꾸지 마세요. 이틀 동안 패치를 하며 고생했지만, 바로 다음 주에 또 다른 사이트가 변경되었습니다. 저는 패배가 예정된 싸움을 하고 있었습니다.

제가 시도했던 것들 (그리고 왜 실패했는가)

먼저, :contains()nth-of-type을 사용하는 더 정교한 CSS 선택자(Selector)를 시도했습니다. 하지만 사이트가 가격 앞에 배너 광고를 추가하면서 작동을 멈췄습니다. \$\d+\.\d{2}와 같은 패턴 매칭을 시도해 보았지만, 어떤 가격은 EUR(유로)로 되어 있었고, 어떤 것은 할인이 적용되어 있었으며, 어떤 것은 JavaScript로 렌더링된 콘텐츠 안에 숨겨져 있었습니다. 심지어 요소가 나타날 때까지 기다리기 위해 헤드리스 브라우저(Headless Browser, Playwright)를 사용하기도 했지만, 이는 속도를 늦출 뿐만 아니라 DOM 구조가 바뀌면 여전히 깨졌습니다.

시각적 테스트 도구와 스크린샷 기반의 객체 탐지(Object Detection)와 같은 머신러닝(Machine Learning) 접근 방식도 살펴보았지만, 텍스트 추출용으로는 너무 무겁고 신뢰할 수 없었습니다.

전환점: LLM에게 힘든 일을 맡기기

한 동료가 PDF에서 비정형 데이터(Unstructured Data)를 파싱하기 위해 GPT를 사용했다고 언급했습니다. 저는 생각했습니다. '왜 원본 HTML에 적용해 보지 않을까?' 아이디어는 간단했습니다. 선택자(Selector)를 추측하는 대신, 관련 HTML 스니펫(Snippet) 또는 텍스트 콘텐츠를 구조화된 필드로 추출하라는 명확한 지침과 함께 LLM에 보내는 것이었습니다.

전체 페이지를 그대로 보내는 것은 비용이 많이 들고 속도가 느릴 것이라는 점을 알고 있었기에, 먼저 BeautifulSoup을 사용하여 HTML을 간소화했습니다. 스크립트(scripts), 스타일(styles), 내비게이션(navigation)을 제거한 다음, 일부 구조적 마커(예: h1, table)는 유지하면서 본문(body)을 깨끗한 텍스트로 평탄화(flatten)했습니다. 그런 다음 LLM에 name, price, availability, sku와 같은 필드를 포함한 JSON 객체를 반환하도록 프롬프트(prompt)를 작성했습니다.

제가 구축한 핵심 내용은 다음과 같습니다:

import requests
from bs4 import BeautifulSoup
import json
...

저는 이를 10개의 서로 다른 이커머스(e-commerce) 페이지에서 테스트했습니다. 클래스(classes)가 변경된 사이트를 포함하여 모든 사이트에서 작동했습니다. LLM은 문맥(context)을 이해했습니다. 달러 기호($)가 없어도 "€39,99"를 가격으로 인식했고, "ships in 3-5 days"가 가용성(availability)이 "In stock"이 아님을 의미한다는 것을 파악했습니다.

이 접근 방식이 빛을 발하는 부분 — 그리고 그렇지 않은 부분

장점 (Pros):

  • 레이아웃 변경에 견고함 (Robust to layout changes) – 사이트들이 업데이트되는 동안에도 몇 주 동안 코드를 전혀 건드리지 않았습니다.
  • 적응성 (Adaptable) – 평점이나 설명을 추출하고 싶나요? 프롬프트(prompt)만 바꾸면 됩니다.
  • 더 이상 정규표현식 지옥이 없음 (No more regex hell) – LLM이 통화 형식, 약어, 누락된 데이터를 유연하게 처리합니다.

단점 (Cons):

  • 비용 (Cost) – 요청당 비용은 1센트의 아주 작은 일부이지만, 규모가 커지면 합계가 늘어납니다. 대량 스크레이핑(high-volume scraping, 시간당 수천 페이지)의 경우, 캐싱(caching)이나 로컬 모델(local models) 없이는 실행 가능하지 않습니다.
  • 지연 시간 (Latency) – LLM 호출에는 1~3초가 소요됩니다. 빠른 정규표현식(regex) 매칭과 비교하면 느립니다.
  • 환각 (Hallucinations) – 때때로 LLM이 SKU를 지어내거나 할인을 원래 가격으로 잘못 읽기도 합니다. 항상 보조 규칙(예: 가격이 \d+\.\d{2}와 일치하는지 확인)으로 검증해야 합니다.
  • 프롬프트 엔지니어링 (Prompt engineering) – 과도한 추출을 방지하려면 프롬프트를 미세 조정(fine-tune)해야 합니다. 저는 예시를 수정하는 데 하루를 보냈습니다.

다음에 제가 다르게 할 일

첫째, 페이지를 더 공격적으로 사전 필터링(pre-filter)하겠습니다. 어떤 페이지들은 수천 줄에 달했는데, 전체를 보내는 것은 토큰(tokens)을 낭비하는 일이었습니다. 저는 공통적인 패턴을 식별한 후, BeautifulSoup을 사용하여 주요 콘텐츠 영역만 남겼습니다. 둘째, 검증(validation) 단계를 추가하겠습니다. LLM으로부터 JSON을 받은 후, 가격(price) 필드를 정규표현식(regex)으로 파싱하여 유효한 숫자 형태인지 확인합니다. 만약 그렇지 않다면, 프롬프트를 다시 입력(re-prompt)하거나 수동 검토를 위해 플래그(flag)를 지정합니다.

또한, 첫 번째 패스(first pass)를 위해 소규모 로컬 모델(Phi-3 등)을 사용하는 방법을 탐색했고, 신뢰도가 낮을 때만 클라우드 모델로 전환(fallback)하도록 했습니다. 이를 통해 비용을 80% 절감했습니다.

마지막으로, LLM은 표의 행(row) 개수를 세거나 정밀한 숫자 추출(예: "5 items"와 "5,00"의 차이)에 매우 취약하다는 것을 깨달았습니다. 그런 경우에는 여전히 LLM이 식별한 스니펫(snippet)에 정규표현식(regex)을 사용합니다.

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

안정적인 API나 명확한 사이트맵(sitemap)이 있는 단일 사이트를 스크레이핑하는 경우라면 LLM을 건너뛰십시오. 실시간 데이터(1초 미만)가 필요한 경우라면 이 방식은 너무 느립니다. 데이터가 순수하게 수치적이고 구조화되어 있다면(로그 파일처럼), 정규표현식(regex)이 더 빠르고 저렴합니다. 그리고 예산이 매우 한정적이라면, gpt-4o-mini조차 비용이 쌓이게 되므로 로컬 모델이 더 나을 수 있습니다.

하지만 저의 사용 사례, 즉 혼란스러운 웹 페이지 세트에서 반구조화된 데이터(semi-structured data)를 추출하는 데 있어서는 이 접근 방식이 구원 투수가 되었습니다. 사이트들이 여러 번 변경되었음에도 불구하고, 저는 3주 동안 스크레이핑 코드를 건드리지 않았습니다. LLM이 취약한 DOM을 추상화(abstract)해주기 때문입니다.

이제 궁금합니다. 여러분은 스크레이퍼에서 웹사이트의 변경 사항을 어떻게 처리하시나요? 여전히 셀렉터(selectors)와 씨름하고 계신가요, 아니면 언어 모델(language model) 접근 방식을 시도해 보셨나요? 여러분의 설정(setup)은 어떤 모습인가요?

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0