모든 모니터링 작업은 결국 같은 질문에 도달한다. 이 페이지가 변경되었는가? 해시 비교처럼 들리지만 그렇지 않다. 최신 페이지는 로드할 때마다 변경되는데, 로테이팅 광고, 타임스탬프, 추천 블록, 세션 토큰, 실시간 카운터 때문이다. 그래서 순진한 비교는 매번 변경을 보고하고, 알림 채널은 노이즈로 가득 차서 결국 모두가 음소거하게 된다. 반대의 실패는 더 조용하지만 더 나쁘다. 노이즈를 무시하도록 지나치게 조정된 파이프라인은 정작 잡아야 할 가격 인하나 삭제된 인증서를 놓친다.
이 가이드는 대규모로 실제 변경을 감지하는 방법을 다룬다. 페이지 전체가 아니라 필드를 비교하는 방법, 불가피하게 비교해야 하는 것을 정규화하는 방법, 페이지가 변경되었는지 여부가 아니라 얼마나 변경되었는지를 측정하는 방법, 그리고 각 종류의 변경을 올바른 곳으로 전달하는 방법이다.
핵심 요약
- 대부분의 사이트에서 바이트 단위 비교는 쓸모가 없다. 우리는 뉴스 홈페이지를 45초 간격으로 두 번 가져왔는데, 바이트가 달랐고 정리된 텍스트조차 달랐다.
- 가능하면 페이지가 아니라 필드를 비교하라. 가격, 제목, 재고 상태는 변경되었거나 변경되지 않았거나 둘 중 하나다.
- 페이지 전체를 비교해야 하는 경우, 먼저 정규화한 다음 유사도를 측정하라. Google이 80억 개 페이지에서 유사 중복 감지를 위해 설명한 지문 기법인 Simhash는 “변경되었는가?”라는 질문을 “얼마나 변경되었는가?”라는 질문으로 바꾼다.
- 모든 변경을 분류하라: 필드 변경, 콘텐츠 변경, 노이즈. 각각은 다른 대응을 받을 자격이 있다.
- 사이트별, 페이지 유형별로 조정하라. 제품 페이지에 맞는 임계값은 뉴스 홈페이지에는 맞지 않는다.
순진한 diff가 실패하는 이유
문제를 구체적으로 보기 위해, 우리는 대형 뉴스 사이트의 국제판 첫 페이지를 45초 간격으로 두 번 가져왔다. 예상대로 원본 HTML은 달랐다. 더 흥미로운 점은, 스크립트와 스타일과 마크업을 제거하고 타임스탬프와 토큰을 없앤 후에도 눈에 보이는 텍스트가 여전히 달랐다는 것이다. 실시간 페이지는 계속해서 업데이트된다. 조금이라도 차이가 있으면 알림을 보내는 모니터는 이 페이지에 대해 실행될 때마다 알림을 보냈을 것이다.
노이즈의 원인은 예측 가능하다:
| 노이즈 | 예시 | 일반적인 해결책 |
|---|---|---|
| 내장 스크립트와 데이터 | 애널리틱스 페이로드, JSON 상태, CSRF 토큰 | 비교 전 스크립트와 스타일 블록 제거 |
| 시간 기반 텍스트 | ”3분 전 업데이트됨”, 시계 시간, 날짜 | 패턴으로 제거하거나, 이를 제외한 필드 비교 |
| 로테이팅 모듈 | 광고, “지금 인기”, 추천 | 관심 있는 영역이나 필드만 비교 |
| 개인화 및 실험 | A/B 테스트 변형, 위치별 블록 | 일관된 설정으로 고정된 관측 지점에서 수집 |
| 순서 | 로드할 때마다 섞이는 목록 | 비교 전 정렬 |
일부 노이즈는 사실 누가 요청했는지의 차이다. 독일에서 로드한 페이지는 미국에서 로드한 동일한 페이지와, 변경과는 무관한 이유로 다를 수 있다. 모니터링하는 각 페이지의 관측 지점을 고정하여, 실행 간에 변하는 유일한 요소가 시간이 되도록 하라. 레지덴셜 게이트웨이를 사용하는 경우, 이는 해당 작업의 모든 실행에서 국가를 고정하는 것을 의미한다. 예를 들어 customer-USERNAME-country-de처럼.
원칙 1: 페이지가 아니라 필드를 비교하라
가장 신뢰할 수 있는 변경 감지기는 페이지를 아예 diff하지 않는다. 가격, 재고 여부, 제목, 인증서 목록, 주소처럼 중요한 필드를 추출하여 그것들을 비교한다. 필드는 변경되었거나 변경되지 않았거나 둘 중 하나이며, 페이지의 다른 곳에 있는 노이즈는 무관하다.
구조화된 데이터는 생각보다 이를 쉽게 만들어준다. 많은 페이지가 핵심 필드를 JSON-LD로 게시하는데, 이는 주변 레이아웃보다 훨씬 안정적이다. 이를 추출하는 방법은 HTML 파싱을 멈춰라에서 다룬다. 필드가 선택자에서 나오는 경우, 주변 HTML이 아니라 추출된 값을 비교하라.
필드 비교는 또한 알림을 유용하게 만든다. “가격이 89.00에서 79.00으로 변경됨”은 실행 가능하다. “페이지가 변경됨”은 그렇지 않다.
원칙 2: 비교해야 하는 것을 정규화하라
일부 모니터링은 실제로 페이지 전체에 관한 것이다. 이용약관, 공급업체의 “소개” 페이지, 지속가능성 선언문, 정책 페이지 등이다. 이런 경우, 비교 전에 중요하지 않은 것을 제거하라:
- 비콘텐츠 블록 제거: 스크립트, 스타일, 템플릿, 인라인 SVG.
- 눈에 보이는 텍스트만 유지, 공백을 축약하고 대소문자를 통일.
- 휘발성 조각 제거: 시계 시간, ISO 날짜, 상대 시간, 토큰, 긴 16진수 식별자.
- 영역 제한, 기사 본문이나 main 요소처럼 안정적인 주요 콘텐츠 영역이 있는 경우.
정규화는 노이즈의 상당 부분을 제거한다. 전부는 아니며, 그래서 다음 단계가 중요하다.
원칙 3: 여부가 아니라 얼마나인지 측정하라
정규화된 텍스트가 동일한지 묻는 대신, 얼마나 유사한지 물어라. Simhash는 이를 위한 좋은 도구다. 문서를 64비트 지문으로 축소하는데, 유용한 속성이 있다. 유사한 문서는 비트 위치가 몇 개만 다른 지문을 갖는다. Google은 “Detecting Near-Duplicates for Web Crawling”(WWW 2007)에서 이를 사용해 유사 중복을 감지한 것을 설명했으며, 저자들은 “80억 개 웹페이지의 저장소에 대해, 64비트 simhash 지문과 k = 3이 합리적이다”라고 검증했다. 이는 지문이 최대 3비트 차이가 나는 페이지들을 유사 중복으로 취급할 수 있다는 의미다.
이를 통해 변경 감지는 거리의 문제가 된다. 우리 테스트에서, 45초 간격으로 가져온 뉴스 홈페이지의 두 번의 가져오기는 2비트 차이가 났다. 임계값 이하이므로 노이즈다. 같은 홈페이지를 무관한 기사와 비교했을 때는 34비트 차이가 났다. 명백히 다른 콘텐츠다.
import hashlib
import re
VOLATILE = [
r"\b\d{1,2}:\d{2}(?::\d{2})?\s?(?:am|pm|AM|PM)?\b", # clock times
r"\b\d{4}-\d{2}-\d{2}(?:T[\d:.]+Z?)?\b", # ISO dates
r"\b\d+\s+(?:second|minute|hour|day)s?\s+ago\b", # relative times
r"\b(?:csrf|nonce|token|session|sid)[\w-]*[=:]\s*[\w-]+", # tokens
r"\b[0-9a-f]{24,}\b", # long hex ids
]
def normalise(html):
"""Visible text only, with volatile fragments removed."""
html = re.sub(r"(?is)<(script|style|noscript|svg|template)\b.*?</\1>", " ", html)
text = re.sub(r"(?s)<[^>]+>", " ", html)
for pattern in VOLATILE:
text = re.sub(pattern, " ", text, flags=re.I)
return re.sub(r"\s+", " ", text).strip().lower()
def simhash(text, bits=64):
"""Charikar-style simhash over word 3-grams."""
words = text.split()
grams = [" ".join(words[i:i + 3]) for i in range(max(1, len(words) - 2))]
weights = [0] * bits
for gram in grams:
h = int.from_bytes(hashlib.blake2b(gram.encode(), digest_size=8).digest(), "big")
for i in range(bits):
weights[i] += 1 if h >> i & 1 else -1
return sum(1 << i for i in range(bits) if weights[i] > 0)
def distance(a, b):
return bin(a ^ b).count("1")
def compare(old_html, new_html, old_fields=None, new_fields=None, threshold=3):
"""Classify a change as none, noise, content or field-level."""
old_fields, new_fields = old_fields or {}, new_fields or {}
field_changes = {k: (old_fields.get(k), new_fields.get(k))
for k in set(old_fields) | set(new_fields)
if old_fields.get(k) != new_fields.get(k)}
if field_changes:
return {"kind": "field", "changes": field_changes}
if old_html == new_html:
return {"kind": "none"}
d = distance(simhash(normalise(old_html)), simhash(normalise(new_html)))
return {"kind": "content" if d > threshold else "noise", "distance": d}
임계값은 상수가 아니라 출발점으로 취급하라. WWW 2007의 수치는 시간에 걸쳐 하나의 페이지를 모니터링하기 위해서가 아니라, 수십억 개 페이지 중에서 중복을 찾기 위해 선택된 것이다. 페이지 유형별로 보정하라. 아무것도 의미 있게 변하지 않아야 하는 짧은 시간 동안 모니터링 중인 각 페이지를 여러 번 가져와서, 관찰된 거리보다 살짝 위에 임계값을 설정하라. 짧은 페이지는 더 주의가 필요한데, 몇 개의 단어가 바뀌면 짧은 문서의 지문이 긴 문서보다 더 많이 이동하기 때문이다.
원칙 4: 분류한 다음 전달하라
“변경됨”만 반환하는 감지기는 실제 작업을 알림을 읽는 사람에게 떠넘긴다. 종류를 반환하고 그에 따라 전달하라:
| 종류 | 의미 | 전달 위치 |
|---|---|---|
field | 추적하는 값이 변경됨 | 데이터셋으로 바로, 중요하다면 알림도 |
content | 페이지의 실질 내용이 노이즈 이상으로 변경됨 | 텍스트 diff를 첨부한 사람 검토 대기열 |
noise | 페이지가 움직였지만 정상 범위 내 | 보정을 위해 기록, 절대 알림 없음 |
none | 바이트 단위로 동일함 | 아무것도 없음 |
두 가지 개선 사항은 그만한 가치가 있다. 모든 content 변경 옆에 이전 스냅샷과 읽기 쉬운 텍스트 diff를 보관하여 검토자가 몇 초 안에 판단할 수 있도록 하라. 그리고 사이트별로 각 종류의 발생률을 기록하라. 노이즈 거리가 몇 주에 걸쳐 서서히 증가하는 사이트는 템플릿을 바꾸고 있는 것이고, 갑자기 변경이 전혀 발생하지 않는 사이트는 오래된 페이지나 차단된 페이지를 제공하고 있을 수 있다. 이 실패 모드는 침묵하는 실패율에서 다룬다.
규모 확장하기
대규모 변경 감지는 대부분 저장과 스케줄링의 문제다.
- 대부분의 실행에서는 전체 페이지가 아니라 지문과 필드를 저장하라. 64비트 지문과 몇 개의 필드는 매우 작다. 전체 스냅샷은 변경이 감지되었을 때만, 또는 감사를 위한 더 느린 일정으로만 보관하라.
- 예상 변경에 따라 재방문하라. 좀처럼 변하지 않는 페이지는 매시간 확인할 필요가 없다. 비용을 고려한 크롤링 스케줄링은 변경이 일어날 가능성이 높은 곳에 가져오기 예산을 쓰는 방법을 설명하며, 모든
none이나noise결과는 그 모델을 위한 증거가 된다. - 저렴한 신호를 먼저 사용하라. 사이트가 신뢰할 수 있는
ETag나Last-Modified헤더를 보내는 경우,304 Not Modified를 반환하는 조건부 요청은 거의 대역폭을 쓰지 않고 질문에 답해준다. - 감시자를 감시하라. 알려진 변경 패턴을 가진 카나리아 페이지는 감지기 자체가 여전히 작동하는지 알려준다.
이것이 사용되는 곳
동일한 메커니즘이 매우 다른 작업들의 기반이 된다. 실시간 경쟁사 가격 피드 구축하기에서처럼 필드 변경이 핵심인 가격 및 재고 모니터링, 공급업체의 공개 활동 모니터링하기와 공개 웹 증거로 ESG 주장 검증하기에서처럼 페이지 전체의 콘텐츠 변경이 중요한 공급업체 및 컴플라이언스 모니터링, 그리고 변경이 법적으로 중요할 때 증거를 보존하는 작업이다.
결론
“이 페이지가 변경되었는가?”는 현대 웹에는 잘못된 질문이다. 답이 거의 항상 예이기 때문이다. 유용한 질문은 “관심 있는 필드가 변경되었는가?”와, 페이지 전체를 비교해야 하는 경우 “실질 내용이 이 페이지의 정상적인 변동 범위를 넘어 변경되었는가?”이다.
가능하면 필드를 비교하라. 불가피하게 비교해야 하는 것을 정규화하고, 동일성이 아니라 유사도를 측정하며, 페이지 유형별로 임계값을 보정하고, 각 종류의 변경을 대응할 수 있는 곳으로 전달하라. 그 결과는 사람들이 신뢰하는 변경 피드이며, 그것만이 실제로 읽히는 종류의 피드다.
출처 및 참고자료
- Manku, Jain, Das Sarma, Detecting Near-Duplicates for Web Crawling, WWW 2007.
- Moses Charikar, Similarity Estimation Techniques from Rounding Algorithms, STOC 2002. Simhash의 기원.
- Shifter, 레지덴셜 프록시 지오타겟팅 문서.