모든 텍스트를 스크래핑하는 것은 쉬운 10%입니다. 학습할 가치가 있는 코퍼스를 유지하는 것이 나머지 90%입니다 — 962회 실행
요약
LLM 학습을 위한 웹 스크래핑 시 단순 텍스트 추출보다 데이터 중복 제거와 품질 유지가 훨씬 중요함을 강조합니다. 반복적인 스크래핑 과정에서 발생하는 중복 데이터와 오래된 콘텐츠 문제를 해결하는 것이 고품질 코퍼스 구축의 핵심입니다.
핵심 포인트
- 단순 텍스트 추출은 전체 작업의 10%에 불과함
- 반복적 스크래핑 시 중복 데이터 관리가 핵심 과제
- 리뷰 사이트 등 필터링 구조가 복잡한 곳에서 중복 발생 빈도 높음
- 데이터 중복 제거는 언어 모델의 성능 향상에 필수적임
2026년 5월 19일, Ilya Krukowski가 작성한 ScrapingBee의 훌륭한 가이드가 있습니다 — _"LLM 학습을 위해 웹사이트에서 모든 텍스트를 스크래핑하는 방법"_입니다. 이 가이드는 사이트맵 (Sitemaps), 텍스트 추출 (Text extraction), 프록시 (Proxies), 동시성 (Concurrency), 재시도 (Retries) 과정을 안내합니다. 탄탄한 내용입니다. 만약 당신이 그 가이드가 말하는 대로 정확히 수행한다면, 당신은 텍스트로 가득 찬 폴더를 갖게 될 것입니다.
그리고 바로 그 지점이 사람들을 속이는 부분입니다.
텍스트를 추출하는 것은 쉬운 10%입니다. 저는 나머지 90%가 팀들을 집어삼키는 것을 지켜봐 왔으며, 그것은 추출과는 거의 관련이 없습니다. 그것은 첫 번째 성공적인 실행 _이후_에 발생하는 일입니다 — 즉, 동일한 스크래퍼가 다음 주에도, 그다음 주에도 다시 실행될 때, 당신이 수집하고 있는 것의 대부분이 이미 가지고 있는 데이터라는 사실을 서서히 깨닫게 되는 상황 말입니다.
저는 다소 낙담스럽게 들릴 수도 있지만 실제로는 해방감을 주는 주장을 하나 하려고 합니다: 반복적인 스크래핑 (Recurring scrape)에서 어려운 문제는 페이지를 가져오는 것이 아닙니다. 동일한 페이지를 새로운 데이터로 다시 계산하지 않는 것입니다. 서로 다른 URL 아래에 있는 중복 데이터 (Duplicates). 변경되지 않았지만 어쨌든 다시 다운로드된 페이지들. 실행 사이에 조용히 부패해가는 오래된 콘텐츠 (Stale content). "디스크에 저장"에서 끝나는 튜토리얼에는 이 중 어느 것도 나타나지 않습니다. 이 모든 문제는 실행 횟수가 50회 정도 되었을 때 나타납니다.
제가 이 이야기를 어디서 얻었는지 말씀드리겠습니다. 저는
왜 리뷰 사이트일까요? 리뷰 사이트는 20개의 서로 다른 URL 아래에서 동일한 콘텐츠를 보여주도록 설계되었기 때문입니다. 최신순 정렬, 평점순 정렬, 별점 1점 필터링 등을 적용해 보세요. 별점 5점 필터의 2페이지는 '최신순'의 1페이지와 겹칩니다. 동일한 리뷰, 동일한 텍스트가 5개의 URL에 걸쳐 존재합니다. 단순하게 "모든 텍스트를 스크래핑하는" 크롤러는 이 각각을 모두 새로운 문서로 취급합니다. 여기에 962회의 실행 횟수를 곱하면, 당신이 가진 것은 데이터셋이 아니라 거울의 방 (hall of mirrors)이 됩니다.
962회의 실행이 무엇이고 무엇이 아닌지에 대해 솔직해져야겠습니다. 이것은 962개의 _서로 다른 사이트_가 아닙니다. 동일한 행위자가 주로 동일한 종류의 페이지를 대상으로 반복해서 실행된 것입니다. 바로 그렇기 때문에 이것이 반복적인 (recurring) 수집에 있어 적절한 스승이 되는 것입니다. 제가 설명하고 있는 실패 모드 (failure mode)는 무언가를 한 번 이상 스크래핑할 때만 나타나기 때문입니다.
당신을 두렵게 만들어야 할 숫자는 제 것이 아닙니다
여기 제가 공을 세울 수 없는 부분이 있으며, 이 글 전체에서 가장 중요한 사실이 있습니다.
2021년, Google과 UPenn의 Katherine Lee, Daphne Ippolito, Nicholas Carlini 및 동료들은 "Deduplicating Training Data Makes Language Models Better" (arXiv:2107.06499, 이후 ACL 2022에서 발표)를 출판했습니다. 그들은 한 세대의 모델들이 학습에 사용했던 정제된 Common Crawl 코퍼스인 C4 내부의 중복성을 측정했습니다. 수상한 스크래핑 데이터가 아니라, 이미 정제된 플래그십 (flagship) 데이터셋을 대상으로 한 것입니다.
그들이 자체적인 측정 결과로 발견한 내용은 다음과 같습니다:
- C4의 약 3.04%가 근사 중복 (near-duplicates) (그들의 NearDup 측정 방식)입니다. 이는 정제 과정을 거친 후의 수치입니다.
- 단 하나의 61단어짜리 영어 문장이 C4에서 60,000번 이상 등장합니다.
- 하나의 근사 중복 클러스터(cluster)가 약 250,933개의 예시를 보유하고 있으며, C4에는 각각 5,000개 이상의 근사 중복을 가진 클러스터가 280개나 있습니다.
그들의 결론은 다음과 같습니다: 중복 제거 (deduplicated) 버전을 통해 학습된 모델은 "암기된 텍스트를 10배 적게 방출하며, 동일하거나 더 나은 정확도에 도달하기 위해 더 적은 학습 단계 (train steps)를 필요로 합니다."
이 사실을 곱씹어 보십시오. 중복 문제는 어떤 인턴이 주말 동안 스크래핑하며 발생시킨 반올림 오차 같은 것이 아니었습니다. 그것은 이미 진지한 사람들이 필터링한 코퍼스(corpus) 내에 이미 박혀 있었으며, 그 코퍼스로 학습된 모델들에게 측정 가능한 수준의 해를 끼쳤습니다. 수작업으로 큐레이션된 참조 데이터셋(reference dataset)조차 근사 중복(near-dupes)이 3.04%라면, 매주 월요일마다 다시 실행하는 여러분의 가공되지 않은(raw) 스크래핑 결과는 어떤 모습일 것 같습니까?
저 역시 제 코퍼스에 대한 정확한 백분율을 가지고 있지 않으며, 이를 지어내지도 않을 것입니다. 정직한 답변은 "사이트가 무엇인지, 그리고 얼마나 자주 다시 실행하는지에 따라 전적으로 다르다"입니다. 하지만 그 '방향성'만큼은 의심의 여지가 없으며, 이는 그 누구도 튜토리얼에서 언급하지 않는 방향성입니다.
보이지 않는 90%, 세 가지 실패 유형으로 분류
제가 "나머지 90%"라고 말할 때, 이는 모두 "더 많은 데이터"라는 가면을 쓰고 있는 세 가지 구체적인 사항을 의미합니다.
1. N개의 URL 아래에 있는 동일한 페이지 (URL 수준의 중복, URL-level duplication).
2. 아무것도 변하지 않았음에도 다시 다운로드된 동일한 콘텐츠 (낭비되는 재수집, wasted re-collection).
이 문제는 더 조용히 발생합니다. 페이지가 지난 실행 이후 실제로 변하지 않았음에도 불구하고, 여러분은 페이지를 가져오고(fetch), 파싱(parse)하고, 해시(hash)한 '후에야' 그것이 중복임을 결정합니다. 아무것도 배우지 못한 채 전체 왕복 과정에 대한 비용을 지불한 셈입니다. 962회의 실행 과정에서 이는 이미 알고 있는 사실을 재확인하기 위해 소비된 엄청난 양의 대역폭(bandwidth), 파싱 시간, 그리고 프록시(proxy) 예산입니다.
3. 콘텐츠 부패 (데이터셋이 제자리에서 썩어가는 현상, Content decay).
페이지가 변했지만, 잘못된 방향으로 변했습니다. 리뷰가 수정되었거나, 제품 페이지가 이제 404 오류를 내며 일반 카테고리로 리다이렉트(redirect)되거나, 기사에 쿠키 벽(cookie wall)이 추가되었습니다. 여러분의 예전 복사본은 이제 미묘하게 틀린 상태가 되었고, 새로운 복사본은 더 나쁠 수도 있습니다. 부패에 대해 아무도 이야기하지 않는 이유는 그것이 에러를 발생시키지 않기 때문입니다. 그저 서서히 우물을 오염시킬 뿐입니다.
앞의 두 가지는 중복 제거(dedup) 문제이며, 세 번째는 신선도(freshness) 문제입니다. 이 도구들은 서로 겹치기 때문에, 저는 이들을 함께 다룰 것입니다.
역설적인 부분: 조건부 GET(conditional GET)은 예의를 지키기 위한 도구가 아니라, 데이터 품질(data-quality)을 위한 도구이다
저는 최근에 조건부 GET(conditional GET) — ETag / If-None-Match, Last-Modified / If-Modified-Since, 304 Not Modified 응답 — 이 소스(source)에 대해 '예의'를 지키는 방법이라는 주장을 담은 다른 글을 썼습니다. 그것은 사실입니다. (실제 명세(spec)를 확인하고 싶다면 RFC 9110, §13 "Conditional Requests" — rfc-editor.org/rfc/rfc9110.html를 참조하세요.)
하지만 당시 제가 놓쳤던 관점이자, 이것이 각주가 아닌 별도의 글로 작성된 이유는 다음과 같습니다: 조건부 GET은 또한 중복 제거(dedup)의 첫 번째 방어선이기도 합니다. 304 응답은 단순히 서버의 작업을 줄여주는 것에 그치지 않습니다. 그것은 수집가인 당신에게 "이것은 당신이 이미 가지고 있는 것과 정확히 동일한 콘텐츠입니다 — 새로운 것으로 간주하지 마세요"라고 알려줍니다. 이것은 HTTP 명세에 의해 당신에게 제공되는 현존하는 가장 저렴한 중복 제거 신호이지만, 대부분의 "모든 텍스트를 스크래핑하는" 파이프라인(pipeline)은 매번 아무런 조건 없는 GET으로 가져오기 때문에 이 신호를 버려버립니다.
따라서 예의에 관한 이야기와 데이터 품질에 관한 이야기는 통신 회선의 양 끝단에서 바라본 동일한 메커니즘입니다. 하나는 소스를 보호하고, 다른 하나는 당신의 코퍼스(corpus)를 보호합니다. 당신은 단 하나의 헤더를 캐싱하는 비용으로 이 두 가지를 모두 얻게 됩니다.
그렇긴 하지만 — 이 부분은 주의해서 말씀드리고 싶습니다만 — 조건부 GET은 서버가 캐싱에 대해 정직하게 반응하는 경우에만 작동합니다. 수많은 사이트가 ETag나 Last-Modified를 보내지 않거나, 설정 오류로 인해 매 요청마다 새로운 값을 보냅니다. 그런 경우에는 304 계층이 아무런 역할을 하지 못하며, 당신은 직접 콘텐츠의 해시(hash)를 계산하는 방식으로 돌아가야 합니다. 이 지점에서 코드로 넘어가겠습니다.
40줄짜리 코퍼스 중복 제거기 (표준 라이브러리만 사용 — 지금 바로 실행 가능)
이것은 가져오기(fetch) 이후와 "데이터셋에 저장" 이전에 위치하는 계층입니다. 가장 저렴한 것부터 순서대로 세 단계를 거칩니다:
- 표준 URL (Canonical URL) — 트래킹 파라미터(tracking params), 프래그먼트(fragments), 마지막 슬래시(trailing slashes), 파라미터 순서를 통합합니다. 첫 번째 실패 사례를 무료로 잡아냅니다.
- 정확한 콘텐츠 해시 (Exact content hash) — 정규화된 텍스트의 SHA-256 값입니다. 진정으로 다른 URL을 통해 들어오는 동일한 콘텐츠를 잡아냅니다.
- MinHash를 통한 유사 중복 (Near-dup via MinHash) — "거의 동일한" 페이지들을 위한 것입니다 (단어 하나가 수정된 리뷰, 필드 하나가 바뀐 템플릿 페이지 등).
의존성 없음. Python 3.11. 복사하여 실행하세요:
import hashlib, re, urllib.parse
from typing import Iterable
...
저는 이를 합성 재실행(synthetic re-run) 데이터에 대해 실행해 보았습니다: 실제 리뷰 하나, 세 개의 추적 파라미터(tracking-param) URL 하에서 다시 수집된 동일한 리뷰, 새로운 경로 하에서 글자 하나 틀리지 않고 다시 스크래핑된 데이터, 단어 하나가 수정된 데이터, 그리고 완전히 다른 리뷰 하나를 포함했습니다. 실제 출력 결과는 다음과 같습니다:
in: 7 documents
kept: 3 unique
dropped -> url_dups=3 exact_dups=1 near_dups=0
7개가 들어와서 3개가 남았습니다. URL 계층에서 3개가 제거되었습니다. 해시(hash) 계층에서 1개가 제거되었습니다. 정직한 데이터 엔지니어링이며, 이 모든 과정은 귀하의 로컬 환경에서 1분 이내에 재현 가능합니다.
임계값(threshold) 때문에 낭패를 본 부분
near_dups=0에 주목하세요. 단어 하나를 수정한 경우 — "2일 이내에 환불"이 "3일 이내에 환불"로 바뀐 경우 — 잡아내지 못했습니다.
저는 잡아낼 것이라 예상했습니다. 제 첫 직감은 "코드가 잘못되었다"였습니다. 하지만 틀리지 않았습니다. 텍스트가 단 17단어였고, 짧은 문서에서는 단어 하나를 바꾸는 것만으로도 해당 단어가 포함된 모든 5-단어 셔글(shingle)이 깨집니다. 즉, 13개의 셔글 중 5개가 깨진 것입니다. 저는 두 문서 사이의 실제 자카드 추정치(Jaccard estimate)를 측정했고, 결과는 0.81이었습니다. 제 임계값은 0.85였습니다. 따라서 올바르게 유지된 것입니다.
그다음 저는 일반적인 길이의 리뷰 — 실제 리뷰의 길이인 약 90단어 — 에 대해 동일한 단어 수정을 실행해 보았고, 추정치는 0.89로 뛰어올라 임계값을 여유롭게 넘겼습니다. 잡아냈습니다.
교훈은 "데모가 잘 보일 때까지 임계값을 조정하라"가 아닙니다. 교훈은 다음과 같습니다: MinHash 근사 중복(near-dup)은 짧은 텍스트에서 신뢰할 수 없으며, 0.85는 마법의 숫자가 아니다. 긴 페이지에서는 관대하지만, 트윗 길이의 짧은 조각(snippet)에서는 매우 민감하게 반응합니다. 만약 귀하의 코퍼스(corpus)가 대부분 짧은 리뷰로 구성되어 있다면, 임계값을 낮추고 위양성(false positive)을 예상해야 합니다. 만약 긴 기사 위주라면 0.85로도 충분합니다. 저는 가짜 초록색 체크 표시를 보여주기보다 데모가 정직하게 실패하는 모습을 보여주는 쪽을 택하겠습니다. 왜냐하면 실패하는 데모야말로 귀하의 근사 중복 검사 과정이 "이상하게" 동작할 때 실제로 귀하를 구해줄 것이기 때문입니다.
반복적인 스크래핑 시 실제로 구축할 구성 요소
실패 양상이 비대칭적이기 때문에, 조언 또한 비대칭적입니다:
- 항상 가져오기(fetch) 전에 URL을 정규화(canonicalize)하십시오. 비용이 들지 않으면서도 가짜 "새로운" 문서가 발생하는 가장 큰 원인을 제거해 줍니다.
- 항상 조건부 GET(conditional-GET) 캐시(
ETag/Last-Modified→304)를 유지하십시오. 이는 가장 저렴한 중복 제거(dedup) 수단이자 동시에 예의를 갖춘 계층(politeness layer) 역할을 합니다. 정확한 의미론은 RFC 9110 §13에 명시되어 있습니다. - 캐시 헤더를 정직하게 제공하지 않는 사이트들을 위한 대비책(fallback)으로 **콘텐츠 해시(Hash content)**를 사용하십시오.
- MinHash 근사 중복 제거(near-dup)는 신중하게 사용하십시오 — 이는 가장 비용이 많이 드는 단계이며 오작동할 가능성이 가장 높습니다. 템플릿화되었거나 가볍게 수정된 페이지를 위해 아껴두고, 임계값(threshold)을 블로그 포스트의 기본값이 아닌 텍스트 길이에 맞춰 조정하십시오.
- 보관하는 모든 문서에 가져온 시간(fetch timestamp)과 콘텐츠 해시(content hash)를 찍어두십시오. 나중에 "이 코퍼스(corpus)가 오래되었는가?"라고 묻게 될 때, 두 정보가 모두 필요할 것입니다. 저는 초기 에이전트(actors) 작업 시 이를 수행하지 않아 후회했습니다. 사후에 "이것이 실제로 마지막으로 변경된 시점이 언제인가"를 다시 도출해 내는 과정은 매우 고통스럽습니다.
그게 전부입니다. 이 중 어느 것도 영리한 기술이 아닙니다. 이 모든 것들이 "웹사이트를 한 번 스크래핑해 본 사람"과 "동일한 스크래퍼를 962번 실행했음에도 데이터셋이 자기 자신의 복사본 40%로 변하지 않게 만든 사람"을 구분 짓는 요소들입니다.
그렇다면 "모든 텍스트를 스크래핑하라"는 말이 틀린 걸까요?
아니요. Krukowski의 가이드는 첫 10%를 위한 좋은 지도입니다. 저는 단지 진짜 업무의 제목이 다르다고 생각할 뿐입니다. 그것은 "모든 텍스트를 스크래핑하는 것"이 아니라, "모든 텍스트를 단 한 번 스크래핑하고, 나머지가 진정으로 새로운 것임을 증명하는 것"입니다. 추출(extraction)은 수많은 좋은 튜토리얼이 존재하는 이미 해결된 문제입니다. 중복 제거 및 노후화 계층(dedup-and-decay layer)이 코퍼스의 생사를 결정하는 지점이며, 튜토리얼들이 바로 그 직전에서 끝나는 부분입니다.
Lee와 그녀의 공동 저자들은 이것이 실제 모델 품질(model quality)을 떨어뜨린다는 것을 증명했습니다. 저의 962회 실행은 이미 가지고 있는 페이지를 재확인하는 데 실제 비용과 대역폭(bandwidth)이 소모된다는 것을 증명했습니다. 두 화살표는 모두 같은 방향을 가리키고 있습니다. 지루한 계층(boring layer)을 구축하십시오.
저는 Aleksey입니다 — 저는 962회의 실행 기록을 보유한 Trustpilot actor(apify.com/knotless_cadence)를 포함하여 프로덕션 스크래퍼(production scrapers)를 운영하고 있습니다. 만약 여러분이 반복적인 스크래핑 작업을 수행 중인데 그것이 자신과 똑같은 복사본들로 조용히 부풀어 오르고 있거나 — 혹은 데이터 추출이 아닌 데이터 품질이 병목 현상(bottleneck)이 되는 스크래핑→LLM 파이프라인을 가지고 있다면, 제가 여러분의 코퍼스(corpus)가 학습할 가치가 있도록 유지해 주는 중복 제거(dedup)/신선도(freshness) 계층을 구축해 드리겠습니다. 사이트와 실행 주기(cadence)를 알려주세요: spinov001@gmail.com.
AI의 도움을 받아 작성되었습니다; 모든 숫자, 코드, 그리고 데모 출력물은 저의 것이며 재현 가능합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기