DMF_Crawler/docs/research/03-crawling-theory-and-papers.md
Yun Chan 56a6e2da93 chore: 저장소 구조 정리 및 문서화, 첫 커밋
- src/dist 산출물 분리 원칙 정리(.gitignore, .gitattributes)
- 루트 및 주요 폴더(config/scripts/prompts/tests/src, 런타임 폴더 5종)에
  안내용 README.md 추가
- CHANGELOG.md, LICENSE, docs/ops/05-release-and-versioning.md 추가
- docs/README.md 문서 지도 갱신
2026-09-04 09:25:44 +09:00

230 KiB
Raw Blame History

크롤링 방법론 이론과 논문 근거

이 문서의 역할: DMF_Crawler 가 "매일 06:00 에 식약처 DMF 공고/현황을 1회 크롤링해 신규·변경·취하를 탐지한다"는 요구를 만족시키기 위해, 어떤 이론·논문·표준을 근거로 주기·politeness·파서 구조·diff 엔진·LLM 역할 경계를 정했는지 확정하는 단일 정본(SSOT) 이다.


0. 한눈에 보기

이 문서가 최종적으로 내린 결론이다. 근거는 각 섹션에 있다.

  • 크롤러 아키텍처는 Mercator(Heydon & Najork, 1999)의 6요소로 축소 적용한다. URL Frontier → DNS → Protocol Module(HTTP) → RIS(재읽기 가능 스트림) → Content-Seen Test(fingerprint) → URL-Seen Test(DUE). 우리 규모(호스트 1개, URL 수십~수백)에서는 frontier 를 "우선순위 있는 리스트 + 호스트별 직렬 큐" 로, content-seen 을 "레코드 지문 테이블" 로 축소한다. 체크포인트(checkpointing)는 규모와 무관하게 반드시 구현한다 — Mercator 가 장기 실행 프로세스의 필수 요소로 지목한 항목이고, 우리는 재부팅 자동 복구를 요구사항으로 갖고 있기 때문이다.
  • 재방문 정책은 Cho & Garcia-Molina(TODS 2003)의 결론에 따라 "균등(uniform) 고정 주기"를 채택한다. 이들은 웹 페이지 변경이 Poisson 과정으로 잘 모델링되며(변경 간격이 λe^(λt) 지수분포), 균등 정책이 비례(proportional) 정책보다 평균 freshness 가 높다는 반직관적 결과를 증명했다. 우리처럼 "매일 06:00 1회"는 이론적으로 정당한 선택이며, 자주 바뀐다고 특정 페이지만 더 자주 긁는 최적화는 하지 않는다.
  • 하루 1회(I = 1일) 주기에서 게시판당 기대 freshness 는 F̄ = (1 e^(λI))/(λI), 기대 age 는 Ā = I/2 1/λ + (1 e^(λI))/(λ²I) 로 계산한다. DMF 공고처럼 λ ≈ 0.21 건/일 수준이면 F̄ ≈ 0.900.63, Ā ≈ 0.08~0.42일이다. 즉 "최악의 경우 하루 늦게 감지"가 설계상 허용 오차이며, 이 값을 SLA 로 문서화한다.
  • λ(변경률)는 매일 관측 결과로 온라인 추정한다. n회 방문 중 X회에서 변경이 관측되면, 구간 검열(interval-censored) Poisson 의 최우추정은 λ̂ = ln(1 X/n)/I 이다. λ̂ 가 급등하면(예: 평소의 3배) 그날은 "이상 급증" 플래그를 리포트에 띄운다. 주기 자체는 바꾸지 않는다(균등 정책 유지).
  • 본 추출은 100% 결정론적 파서가 담당하고, LLM 은 (a) 셀렉터 후보 생성, (b) 셀렉터 검증/복구 제안, (c) 사람이 읽을 요약문 작성 세 가지만 담당한다. 근거: AutoScraper(EMNLP 2024)·AXE(2026)·"Automatic XPath generation agents"(2025) 모두 LLM 이 매 페이지를 읽는 구조는 비용·재현성 면에서 실패하고 LLM 이 한 번 만든 XPath/wrapper 를 반복 실행하는 구조가 이긴다는 동일한 결론에 도달했다. Prompt2DAG(2025)는 결정론적 템플릿 방식 92.3% vs 하이브리드 78.5% 성공률로 결정론 우위를 정량화했다.
  • 셀렉터는 ROBULA+(Leotta et al., JSEP 2016) 원칙으로 작성한다. 절대 XPath 대비 취약성 90% 감소, Selenium IDE 로케이터 대비 63% 감소. 실무 규칙: id/data-*/itemprop/ARIA role/헤더 텍스트 앵커 우선, 시각적 class 와 깊은 경로 체인 금지, "앵커 노드 기준 상대 XPath"(Huang & Song 2025) 사용.
  • schema drift(셀렉터 조용한 파손)는 RAPTURE(Kushmerick, AAAI 1999) 방식으로 매 실행마다 자동 검증한다. 필드별 통계 특징(레코드 수, 문자열 길이 평균/분산, 숫자 비율, 날짜 파싱 성공률, null 비율)의 과거 분포 대비 이탈 확률을 계산해 임계 이하이면 크롤 결과를 채택하지 않고 알림을 띄운다. Lerman/Minton/Knoblock(JAIR 2003)은 이 접근으로 37건 파손 중 35건 탐지(precision 0.73 / recall 0.95)를 달성했다.
  • 중복·변경 판정은 2단 구조다. ① 레코드 식별키(DMF 등록번호 등) 기반 upsert 로 신규/변경/취하를 확정하고, ② 상세 본문 텍스트에는 64-bit SimHash + Hamming 거리 k=3 (Manku·Jain·Das Sarma, WWW 2007 검증값) 을 적용해 "의미 없는 표기 흔들림"과 "실질 변경"을 구분한다. 저장은 스냅샷 + 이벤트 로그 병행(Fowler, Event Sourcing 2005 / Kimball SCD Type 2): 현재 상태 테이블과 append-only 변경 이벤트 테이블을 둘 다 유지한다.
  • robots.txt 는 RFC 9309(2022-09, Koster·Illyes·Zeller·Sassman) 를 그대로 따른다. 캐시 24시간 이내, 5xx 면 완전 금지로 간주, 4xx 면 접근 허용, 파싱 한계 최소 500 KiB, product token 은 a-z A-Z _ - 만. crawl-delay 는 RFC 9309 에 정의되지 않았고 Google 도 지원하지 않는다 — 따라서 crawl-delay 가 있으면 "존중하되", 없어도 우리 자체 하한(2초)을 반드시 건다.
  • 한국 법적 리스크는 낮으나 0은 아니다. 대법원 2022. 5. 12. 선고 2021도1533 판결(숙박앱 크롤링 무죄)은 "보호조치 없고 이용약관상 제한이 비회원에 미치지 않는 공개 정보"의 크롤링에 대해 정보통신망법 침입·저작권법 DB제작자 권리 침해·업무방해를 모두 부정했다. 반대로 서울남부지법 2021. 9. 8. 선고 2021고단588 판결은 회원 계정 로그인 + 약관상 금지 명시 상황에서 유죄를 선고했다. → 로그인하지 않는다. 약관·robots 를 매 실행 확인한다. 서버 부하를 유발하지 않는다. 이 세 가지가 우리의 법적 안전선이다.

1. 목차


2. 논문·문헌 마스터 표

raw dump 에서 확인된 모든 논문·표준·기술문서를 한 표에 모았다. "확인" 열은 리서치 에이전트가 WebFetch 로 실제 본문을 열어 확인했는지 여부다( = 본문 확인, 🔶 = 검색 결과 메타데이터만, = 접근 실패).

2.1 크롤러 아키텍처 고전

# 제목 저자 연도 게재처 URL 확인 이 프로젝트에 주는 시사점
A1 Mercator: A scalable, extensible Web crawler Allan Heydon, Marc Najork (Compaq SRC, Palo Alto) 1999 (1999-06-26) World Wide Web journal, Vol. 2, No. 4, pp. 219229 https://link.springer.com/article/10.1023/A:1019213109274 🔶 확장 가능한 크롤러의 부품 목록을 정의한 원전이다. URL Frontier / DNS Resolver / Protocol Module / RIS / Content-Seen Test / URL-Seen Test(DUE) / Processing Module 이라는 분해는 규모와 무관하게 유효하므로, 우리 코드도 이 7개 모듈 이름을 그대로 파이썬 모듈명으로 쓴다. "동일 웹서버에 동시 다운로드 금지"라는 최소 politeness 정의도 여기서 온다.
A2 High-Performance Web Crawling (SRC Research Report 173) Marc Najork, Allan Heydon 2001-09-26 Compaq Systems Research Center, SRC Research Report 173 (26 pages) https://www.cs.cornell.edu/courses/cs685/2002fa/mercator.pdf Mercator 후속 보고서로 frontier 의 front-end/back-end 2단 구조를 명시한다. front-end 는 우선순위 k개 FIFO 큐, back-end 는 n개 FIFO 큐이며 각 back-end 큐는 "한 호스트의 URL만" 담아 강한 politeness 를 보장한다. URL-Seen 은 Rabin fingerprint 8바이트 체크섬 해시테이블(10억 URL ≈ 5GB), robots.txt 는 호스트→규칙 LRU 캐시 기본 2^18 엔트리. 백그라운드 스레드가 기본 10초마다 깨어나 통계 로깅·종료 조건 확인·체크포인트를 수행한다 → 우리의 스케줄러/헬스체크 루프 설계를 그대로 이 패턴으로 만든다.
A3 An Introduction to Heritrix: an open source archival quality web crawler Gordon Mohr, Michael Stack, Igor Ranitovic, Dan Avery, Michele Kimpton 2004-07 Proc. 4th International Web Archiving Workshop (IWAW'04), Bath, UK, pp. 109115 https://docs.huihoo.com/heritrix/An-Introduction-To-Heritrix.ppt 🔶 Internet Archive 의 아카이브 품질 크롤러. "정중함(politeness)을 설정으로 노출한다"는 운영 철학이 핵심이다. 우리는 Heritrix 를 쓰지 않지만 politeness 파라미터를 코드에 하드코딩하지 않고 설정 파일로 노출하는 원칙을 그대로 가져온다.
A4 Heritrix3 (소스 저장소) Internet Archive 현재 GitHub, Java, Apache License 2.0 https://github.com/internetarchive/heritrix3 robots.txt 및 META nofollow 준수, 크롤 잡(crawl job) 단위 설정, WARC 출력, Frontier 큐잉이 4대 개념. WARC 개념은 우리에게 **"원본 HTML 스냅샷을 날짜별로 보존한다"**는 요구로 번역된다(디버깅·소급 재파싱·법적 증빙 용도).
A5 Heritrix 설정 문서 (configuring-jobs) Internet Archive 현재 heritrix.readthedocs.io https://heritrix.readthedocs.io/en/latest/configuring-jobs.html 실제 politeness 프로퍼티 이름과 기본값을 확인했다: delayFactor(기본 5.0, "직전 URI 를 가져오는 데 걸린 시간의 배수"), minDelayMs(delayFactor 계산값보다 우선하는 최소 대기), maxDelayMs(기본 30000ms), maxPerHostBandwidthUsageKbSec, robotsPolicyName(obey / classic / robotsTxtOnly / ignore, 기본 obey, RFC 9309 경로 와일드카드 지원), metadata.operatorContactUrl, maxToeThreads, extract404s. 우리의 백오프 정책은 delayFactor=5.0, minDelayMs=2000, maxDelayMs=30000 을 그대로 채택한다.
A6 Web Crawling (서베이) Christopher Olston (Yahoo! Research), Marc Najork (Microsoft Research) 2010 Foundations and Trends in Information Retrieval, Vol. 4, No. 3, pp. 175246, DOI 10.1561/1500000017 https://doi.org/10.1561/1500000017 (초록) "웹 크롤링은 BFS 의 단순 응용처럼 보이지만 실제로는 초대형 자료구조 관리 같은 시스템 문제부터 '변화하는 콘텐츠를 얼마나 자주 재방문할 것인가' 같은 이론 문제까지 걸쳐 있다"는 문장이 이 프로젝트의 전체 난이도 지도를 요약한다. 우리 문제는 시스템 축은 거의 0(호스트 1개)이고 재방문·변경 탐지 축이 전부임을 이 서베이가 정당화한다.
A7 A Brief History of Web Crawlers Seyed M. Mirtaheri, Mustafa Emre Dinçktürk, Salman Hooshmand, Gregor V. Bochmann, Guy-Vincent Jourdan, Iosif Viorel Onut 2014 arXiv:1405.0749 https://arxiv.org/abs/1405.0749 (초록만) 크롤러 평가 기준을 정립하려는 서베이. 초록 수준에서만 확인되어 Mercator/Heritrix/Cho 관련 상세 서술은 확인하지 못했다(⚠️ 본문 미확인). 배경 읽기용으로만 참조한다.
A8 Web crawler (Wikipedia) — Re-visit policy / Politeness policy / Crawler identification 현재 Wikipedia https://en.wikipedia.org/wiki/Web_crawler 실무 crawl-delay 값의 실측 레퍼런스: Mercator 계열은 "직전 다운로드 소요 시간의 10배"를 기다리는 적응형, Cho 는 10초, WIRE 는 기본 15초, 실제 관측된 접근 간격은 20초~34분. freshness 는 0/1 이진값, age 는 마지막 수정 이후 경과 시간. "최적 정책은 비례보다 균등에 가깝다", "특정 페이지 접근은 가능한 한 균등 간격으로 배치해야 한다"(Coffman et al.), 변경은 지수분포로 잘 모델링된다. 우리 하한값 2초는 이 스펙트럼의 보수적 끝단이 아니므로, DMF 게시판이 응답이 느릴 경우 delayFactor 로 자동 확대되게 한다.

2.2 증분 크롤링 · 변경 감지 이론

# 제목 저자 연도 게재처 URL 확인 이 프로젝트에 주는 시사점
B1 The Evolution of the Web and Implications for an Incremental Crawler Junghoo Cho, Hector Garcia-Molina 2000 Proc. 26th VLDB, pp. 200209, Morgan Kaufmann https://dl.acm.org/doi/10.5555/645926.671679 (403) 웹 변경 이력을 실측해 증분(incremental) 크롤러 아키텍처를 제안한 원전. 핵심 대비는 "주기적 크롤(periodic: 전체를 새로 긁고 통째로 교체)" vs "증분 크롤(incremental: 변경분만 갱신, 로컬 컬렉션을 항상 유지)". 우리는 매일 전체 목록을 긁되 저장은 증분 upsert 로 하는 하이브리드를 택한다. ⚠️ ACM 403 및 Semantic Scholar 429 로 본문 미확인 — 인용 시 2차 출처 기준.
B2 Effective Page Refresh Policies for Web Crawlers Junghoo Cho, Hector Garcia-Molina 2003-12 ACM Transactions on Database Systems (TODS), Vol. 28, Issue 4, pp. 390426, DOI 10.1145/958942.958945 (Semantic Scholar citationCount 314) https://dl.acm.org/doi/10.1145/958942.958945 🔶 (메타데이터 API 로 확인) 이 프로젝트의 주기 정책 근거 1순위. ① Poisson 과정이 웹 페이지 변경을 잘 기술한다 — 변경률 λ 인 페이지의 변경 간격은 지수분포 λe^(λt). ② 제안된 refresh 정책이 freshness 를 유의하게 개선한다. ③ 균등(uniform) 정책이 비례(proportional) 정책을 이긴다 — 자주 바뀌는 페이지에 자원을 몰아주면 오히려 평균 freshness 가 떨어진다. 우리는 "매일 06:00 전 게시판 균등 1회"를 이 결과로 정당화한다. ⚠️ 원문 PDF(oak.cs.ucla.edu, ACM)는 접속 실패 — 수식은 표준 유도로 §4.2 에 재구성.
B3 Tractable near-optimal policies for crawling Yossi Azar (Tel-Aviv Univ.), Eric Horvitz (MSR), Eyal Lubetzky (NYU Courant), Yuval Peres (MSR), Dafna Shahaf (HUJI) 2018-07-23 PNAS Vol. 115, Issue 32, pp. 80998103, DOI 10.1073/pnas.1801519115 https://www.pnas.org/doi/10.1073/pnas.1801519115 (PMC 전문) 문제 정식화가 우리에게 그대로 쓸 수 있다: 페이지별 Poisson 변경률 Δᵢ, 요청률 μᵢ, 총 폴링 대역폭 제약 R. 최적 무작위 정책은 "utility/change-rate 비로 정렬 후 할당 0인 페이지를 골라내기"로 O(n log n)에 구해지며, EDF(earliest-deadline-first)로 비무작위화한 결정론 정책이 최적해의 99% 성능을 낸다. → 우리가 나중에 대상 게시판을 여러 개로 늘릴 때, "중요도(μ)/변경률(Δ) 비로 정렬 후 상위 몇 개만 하루 2회" 같은 확장을 이 알고리즘으로 정당화할 수 있다. 지금은 n 이 작아 균등으로 충분하다.
B4 Staying up to Date with Online Content Changes Using Reinforcement Learning for Scheduling Andrey Kolobov, Yuval Peres, Cheng Lu, Eric J. Horvitz 2019 Advances in Neural Information Processing Systems 32 (NeurIPS 2019) https://papers.nips.cc/paper/2019/hash/ad13a2a07ca4b7642959dc0c4c740ab6-Abstract.html 변경 관측이 불완전하고(mixed content change observability) 변경 모델 파라미터를 처음엔 모를 때도 최적성 보장이 있는 스케줄링. 18.5M URL 을 14주간 매일 크롤한 실험으로 검증. → 우리도 λ 를 사전에 모르는 상태에서 시작하므로, "관측하면서 λ 를 온라인 갱신한다"는 §4.6 의 설계가 이 논문 계열의 표준 관행임을 근거로 삼는다. ⚠️ harmonic policy 등 세부 알고리즘은 초록 수준까지만 확인.
B5 A Scalable Crawling Algorithm Utilizing Noisy Change-Indicating Signals Róbert Busa-Fekete, Julian Zimmert, András György, Linhai Qiu, Tzu-Wei Sung, Hao Shen, Hyomin Choi, Sharmila Subramaniam, Li Xiao 2025-02-04 (rev. 2025-03-20) arXiv:2502.02430 https://arxiv.org/abs/2502.02430 sitemap·CDN 시그널 같은 잡음 섞인 부가 정보(오탐도 있고 실제 변경을 놓치기도 함)를 최적으로 활용하는 크롤 스케줄링. Azar et al. 2018 의 "변경·요청이 독립 Poisson" 가정을 완화한다. → DMF 게시판의 "총 게시물 수", "최근 게시일", RSS/Atom, Last-Modified 헤더가 정확히 이 noisy change-indicating signal 이다. 우리는 이것들을 1차 필터로 쓰되 절대 신뢰하지 않고, 목록 페이지 1페이지는 항상 실제로 긁는다.
B6 웹 사이트 컨텐츠 변경 모니터링 시스템 (The Monitoring System for Informing the Change of Contents on the Web Sites) 김원중, 조이기, 손철수 2002 한국정보통신학회논문지 제6권 제4호, pp. 505512 https://www.kci.go.kr/kciportal/ci/sereArticleSearch/ciSereArtiView.kci?sereArticleSearchBean.artiId=ART000881131 국내 최초급 변경 모니터링 시스템 논문. 구성은 우리와 동일한 3요소다: ① HTML 태그를 이용해 웹 문서를 의미 있는 단위로 구조화·분류해 변경을 감지, ② 사용자 정의 모니터링 주기, ③ 변경 시 알람/E-mail 자동 통지. → "전체 페이지 diff" 가 아니라 구조화된 단위(레코드) diff 가 정답이라는 것이 20년 전에 이미 확립됐음을 보여준다. 우리 diff 엔진이 레코드 단위인 이유.
B7 실시간 웹 크롤링 분산 모니터링 시스템 설계 및 구현 (Design and Implementation of Real-time Web Crawling Distributed Monitoring System, R-WCMS) 김영아, 김계희, 김현주, 김창근 (경남과학기술대학교 컴퓨터공학과) 2019 융합정보논문지 Vol. 9, No. 1, pp. 4553 https://scienceon.kisti.re.kr/srch/selectPORSrchArticle.do?cn=JAKO201909258120005 Apache Kafka + Spark Streaming + Hadoop 으로 수집 시간 1517% 단축. 우리에게 주는 시사점은 반대 방향이다 — 단일 게시판 일 1회 감시에 이런 스택은 명백한 과잉이다. "수집 시간 예측을 통한 효율화" 아이디어만 취해, 우리는 실행 소요 시간을 기록해 이상 지연(예: 평소의 3배)을 헬스체크 알림 조건으로 쓴다.
B8 실시간 웹 게시판 모니터링 및 모바일웹을 이용한 알람 서비스 개발 김종근, 심근호, 이요셉, 임영환 2012 디지털콘텐츠학회논문지 제13권 제1호, pp. 111 https://www.kci.go.kr/kciportal/ci/sereArticleSearch/ciSereArtiView.kci?sereArticleSearchBean.artiId=ART001648031 기존 방식(DB 직접 접근 / 공개 API)의 한계 = 비공개 게시판 접근 불가, 실시간 알림 어려움. 이메일 대신 모바일 웹 알림으로 전환. → 우리 프로젝트에서 "알림 채널"을 xlsx 리포트 하나에만 걸지 말고, 서비스 사망 시 Windows 토스트 알림이라는 별도 out-of-band 채널을 갖는 설계가 선행 연구와 일치함을 보여준다.
B9 웹 크롤링 모니터링 시스템 및 방법 (특허) 등록 KR101757822B1 (Google Patents) https://patents.google.com/patent/KR101757822B1/ko 🔶 검색 결과로만 확인. ⚠️ 청구항 미확인 — 상용화 계획이 없는 사내 도구이므로 침해 리스크 평가는 보류하되, 존재는 기록해 둔다.
B10 결정 이론 웹 크롤링, 및 웹 페이지 변경의 예측 (특허) 등록 KR101213930B1 (Google Patents) https://patents.google.com/patent/KR101213930B1/ko 🔶 검색 결과로만 확인. 제목상 Azar/Kolobov 계열의 결정이론 스케줄링에 대응. ⚠️ 청구항 미확인.
B11 Clustering-based incremental web crawling 2010 ACM Transactions on Information Systems https://dl.acm.org/doi/10.1145/1852102.1852103 🔶 검색 결과로만 확인. 유사 변경 패턴 페이지를 군집화해 증분 크롤 효율을 올리는 계열. n 이 작은 우리에겐 적용 대상 아님.
B12 A Dynamic Page-Refresh Index Policy for Web Crawlers 2014 Springer LNCS (978-3-319-08219-6_4) https://link.springer.com/chapter/10.1007/978-3-319-08219-6_4 🔶 검색 결과로만 확인. refresh 정책 후속 연구로 기록만 남긴다.
B13 Towards a Quality-Oriented Real-Time Web Crawler 2010 Springer LNCS (978-3-642-16515-3_10) https://link.springer.com/chapter/10.1007/978-3-642-16515-3_10 🔶 검색 결과로만 확인.
B14 Management Of Volatile Information In Incremental Web Crawler 2009 arXiv:0910.1869 https://arxiv.org/pdf/0910.1869 🔶 검색 결과로만 확인.
B15 Learning to Crawl 2019 arXiv:1905.12781 https://arxiv.org/pdf/1905.12781 🔶 검색 결과로만 확인. Kolobov 계열 RL 크롤 스케줄링.
B16 Online Learning for Active Cache Synchronization 2020 arXiv:2002.12014 https://arxiv.org/pdf/2002.12014 🔶 검색 결과로만 확인.
B17 Look back, look around: a systematic analysis of effective predictors for new outlinks in focused Web crawling 2021 arXiv:2111.05062 https://arxiv.org/pdf/2111.05062 🔶 검색 결과로만 확인. "새 링크(=새 공고)를 예측하는 신호"라는 주제가 우리 문제와 유사하나 focused crawling 문맥.

2.3 구조적 데이터 추출 (wrapper induction 계열)

# 제목 저자 연도 게재처 URL 확인 이 프로젝트에 주는 시사점
C1 Wrapper Induction for Information Extraction Nicholas Kushmerick, Daniel S. Weld, Robert B. Doorenbos 1997-08 (IJCAI-97, Nagoya, Japan, Aug 2329) Proc. 15th International Joint Conference on Artificial Intelligence (IJCAI), pp. 729737 https://www.semanticscholar.org/paper/Wrapper-Induction-for-Information-Extraction-Kushmerick-Weld/f9e7402ad740b73cc0bb64178f86df3478c3aaf5 🔶 wrapper 자동 생성 개념의 원전. hlrt wrapper 클래스는 효율적으로 학습 가능하면서 당시 조사 대상 인터넷 리소스의 48% 를 처리할 만큼 표현력이 있었다. PAC 분석으로 표본 복잡도를 상한하고, 라벨링이 불완전해도 성능이 완만히 저하됨을 보였다. 가장 단순한 클래스는 LR wrapper(문서를 문자열로 보고 좌/우 구분자로 필드를 잘라냄). → "필드 앞뒤의 안정적인 구분자(레이블 텍스트)로 값을 자른다"는 우리 fallback 파서의 이론적 근거. Weld 개인 출판 목록에서도 IJCAI-97 항목이 확인되며, Artificial Intelligence 저널 버전은 확인되지 않았다(⚠️ 저널 버전 존재 미확인).
C2 The Wrapper Induction Environment (WIEN) Nicholas Kushmerick (Dublin City University) 1998 AAAI Workshop WS-98-10, pp. 022 https://cdn.aaai.org/Workshops/1998/WS-98-10/WS98-10-022.pdf 🔶 WIEN 은 문서를 문자 시퀀스로 보고 여러 wrapper 언어 클래스를 정의한다(가장 단순한 것이 LR).
C3 Regression testing for wrapper maintenance (RAPTURE) Nicholas Kushmerick (University College Dublin) 1999 Proc. 16th National Conference on Artificial Intelligence (AAAI-99), 논문번호 AAAI99-011, 6 pages https://cdn.aaai.org/AAAI/1999/AAAI99-011.pdf (PDF 텍스트 추출) schema drift 탐지의 원전이며 우리 검증 로직의 직접 설계도. 원문 인용: "The wrapper verification problem is to determine whether a wrapper is correct. Standard regression testing approaches are inappropriate, because both the formatting regularities and a site's underlying content may change." RAPTURE 는 도메인 독립·완전 구현된 휴리스틱 검증 알고리즘으로, wrapper 의 기대 출력과 관측 출력 사이의 유사도를 계산한다. 27개 실제 인터넷 사이트 실험에서 표준 회귀 테스트 대비 큰 성능 향상. 비교 대상인 STRAWMAN 은 "같은 질의에 대해 과거 정상 페이지와 현재 페이지 출력을 비교"하는 단순 방식이다. → 우리는 STRAWMAN(전날 결과와 오늘 결과 직접 비교)이 아니라 RAPTURE(통계적 특징 분포 비교)를 써야 한다. 게시판 내용 자체가 매일 바뀌는 게 정상이기 때문이다.
C4 Wrapper Maintenance: A Machine Learning Approach Kristina Lerman, Steven N. Minton, Craig A. Knoblock 2003 Journal of Artificial Intelligence Research (JAIR), Vol. 18, pp. 149181 https://arxiv.org/abs/1106.4872 양성 예시만으로 데이터의 구조 정보를 학습하는 효율적 알고리즘 → 두 응용: wrapper verification(파손 탐지)과 wrapper reinduction(자동 복구). 실측: 27개 wrapper 를 1년간 추적해 37건의 파손 중 35건 탐지, precision 0.73 / recall 0.95. reinduction 은 10개 소스에서 precision 0.90 / recall 0.80. → 파손 탐지는 recall 을 높이고(0.95) precision 은 희생해도 된다(오탐 = 사람이 한 번 확인하면 끝, 미탐 = 잘못된 리포트가 배포됨)는 임계값 정책을 이 수치가 정당화한다.
C5 RoadRunner: Towards Automatic Data Extraction from Large Web Sites Valter Crescenzi, Giansalvatore Mecca, Paolo Merialdo 2001 Proc. 27th VLDB, Rome, Italy http://www.vldb.org/conf/2001/P109.pdf (PDF 텍스트 추출) 완전 자동 wrapper 추론. 핵심: 데이터 집약 사이트의 페이지는 백엔드 DBMS 내용을 스크립트가 찍어낸 것이므로, 같은 클래스의 페이지 2장을 비교하면 템플릿(상수)과 데이터(변수)를 분리할 수 있다. 형식화는 union-free regular expression(UFRE): 알파벳 Σ {#PCDATA, ·, +, ?, (, )} 위의 문자열로, a·b, (a)+, (a)? 로 구성되며 (a)* = ((a)+)?. UFRE ↔ nested type 대응(#PCDATA→string, +→list, ?→nullable). 알고리즘 match(σ₁, σ₂) = 두 UFRE 의 least upper bound 계산, 매칭 기법 이름은 ACME (Align, Collapse under Mismatch, and Extract). 페이지를 XHTML 로 정규화 후 토큰 리스트(태그 or 문자열)로 만들고, page 1 을 초기 wrapper 로 삼아 page 2(sample)를 파싱하며 mismatch 를 풀어 일반화한다. mismatch 는 두 종류: string mismatch → 필드(#PCDATA) 발견(예: 'John Smith' vs 'Paul Jones' → #PCDATA 로 일반화하되 'Books of:' 같은 상수는 필드가 아님), tag mismatch → iterator 또는 optional 발견(먼저 반복 패턴을 찾고 실패하면 optional 로 처리; cross-search 로 optional 이 wrapper 쪽인지 sample 쪽인지 판별 후 (<IMG src=.../>)? 형태로 일반화). → 우리 크롤러의 "LLM 셀렉터 생성" 단계는 사실상 RoadRunner 를 LLM 으로 대체하는 것이다. 따라서 LLM 에게도 "목록 페이지 2장 이상을 주고 공통 템플릿을 찾게" 하고, 상수/변수 구분과 optional 필드 존재를 명시적으로 물어야 한다.
C6 Automatic Wrappers for Large Scale Web Extraction Nilesh Dalvi (Yahoo! Research), Ravi Kumar (Yahoo! Research), Mohamed Soliman (Univ. of Waterloo) 2011-03-12 Proceedings of the VLDB Endowment (PVLDB), Vol. 4, No. 4, pp. 219230 https://arxiv.org/abs/1103.2406 잡음 섞인 학습 데이터로도 동작하는 wrapper induction 프레임워크. 사전(dictionary)·정규식으로 자동 생성한 저비용·저품질 주석만으로 비지도 wrapper 학습이 가능해져 사이트별 사람 감독 없이 웹 스케일로 확장. Yahoo! 프로덕션 투입. → 우리도 "정답 라벨"을 사람이 만들 필요가 없다: DMF 등록번호 패턴(정규식), 날짜 형식, 회사명 사전 같은 약한 신호로 필드를 자동 라벨링해 셀렉터 후보를 검증할 수 있다. ⚠️ 시간적 구조 변화 대한 강건성 평가 방법은 초록 수준에서 확인되지 않음.
C7 Robust web extraction: an approach based on a probabilistic tree-edit model Nilesh Dalvi, Philip Bohannon, Fei Sha 2009 Proc. ACM SIGMOD 2009, Providence, Rhode Island, USA https://www.researchgate.net/publication/221214620_Robust_web_extraction_An_approach_based_on_a_probabilistic_tree-edit_model 🔶 스크립트 생성 사이트의 페이지들은 공통 HTML 트리 구조를 공유하므로 wrapper 가 잘 동작하지만, 스크립트와 트리 구조가 시간에 따라 진화하면 wrapper 가 깨져 유지보수 비용이 커진다는 문제를 확률적 tree-edit 모델로 다룬다. ⚠️ raw dump 의 검색 결과가 저자 조합(Dalvi/Kumar/Soliman vs Dalvi/Bohannon/Sha)을 혼동해 서술하므로, 저자는 Dalvi·Bohannon·Sha 로 표기하되 미검증으로 둔다.
C8 Robula+: An algorithm for generating robust XPath locators for web testing Maurizio Leotta, Andrea Stocco, Filippo Ricca, Paolo Tonella 2016 Journal of Software: Evolution and Process (JSEP), Vol. 28, Issue 3, pp. 177204, DOI 10.1002/smr.1771 https://onlinelibrary.wiley.com/doi/10.1002/smr.1771 셀렉터 안정성의 정량적 근거. "test code fragility problem" — 애플리케이션이 진화하면 로케이터가 깨지고 수동 수리 비용이 든다. Robula+ 가 생성한 XPath 는 절대 로케이터 대비 평균 90%, Selenium IDE 로케이터 대비 63% 취약성 감소. 현재 강건 XPath 자동 생성의 state of the art 로 평가된다. → 우리 셀렉터 작성 규칙(§5.6)의 근거이자, 셀렉터 자동 생성 도구를 붙일 때의 알고리즘 선택.
C9 robula-plus (TypeScript 구현) Cyluxx 현재 GitHub https://github.com/cyluxx/robula-plus API: getRobustXPath(element, document), getElementByXPath(xPath, document), uniquelyLocate(xPath, element, document). Node.js 필요, npm installnpm run build. 라이선스가 미정이라 공개 install 패키지가 없다("The License of this code needs some clarification, so until then there will be no public install package available") → 의존성으로 채택 불가. 알고리즘 아이디어만 우리 코드에 재구현한다.
C10 robula-plus (Python 포팅) ZeusFSX 현재 GitHub https://github.com/ZeusFSX/robula-plus 🔶 파이썬 버전 존재만 확인. ⚠️ 라이선스·유지보수 상태 미확인 — 채택 전 실사 필요.
C11 WebTables: Exploring the Power of Tables on the Web Michael J. Cafarella, Alon Halevy, Daisy Zhe Wang, Eugene Wu, Yang Zhang 2008 Proceedings of the VLDB Endowment, Vol. 1, No. 1, pp. 538549, DOI 10.14778/1453856.1453916 http://www.vldb.org/pvldb/vol1/1453916.pdf 🔶 웹에서 141억 개 테이블을 추출해 통계적 분류로 1억 5,400만 개만이 고품질 관계형 데이터임을 밝혔다(약 1.1%). 나머지는 레이아웃용. → <table> 을 봤다고 데이터 테이블이라 가정하지 마라. 우리 파서는 헤더 행 존재, 열 수 일관성, 셀 타입 동질성 같은 "관계형성 판정"을 먼저 통과시킨 뒤 파싱해야 한다.
C12 Schema Extraction for Tabular Data on the Web Marco D. Adelfio, Hanan Samet 2013 PVLDB Vol. 6 http://www.vldb.org/pvldb/vol6/p421-adelfio.pdf 🔶 웹 테이블에서 스키마(헤더 행 식별 포함)를 추출하는 기법. 우리 헤더 판정 로직 참고용.
C13 On Extracting Data from Tables that are Encoded using HTML 2019 arXiv:1903.08305 https://arxiv.org/pdf/1903.08305 🔶 rowspan/colspan 처리 등 HTML 테이블 파싱의 실무 함정 정리.
C14 Web Table Extraction, Retrieval and Augmentation: A Survey 2020 arXiv:2002.00207 https://arxiv.org/pdf/2002.00207 🔶 웹 테이블 처리 전반 서베이.
C15 Identifying Web Tables: Supporting a Neglected Type of Content on the Web 2015 arXiv:1503.06598 / Springer https://arxiv.org/pdf/1503.06598 🔶 레이아웃 테이블 vs 데이터 테이블 분류. C11 의 실무 보완.
C16 An Annotated Corpus of Webtables for Information Extraction Tasks 2020 arXiv:2008.07680 https://arxiv.org/pdf/2008.07680 🔶 웹 테이블 주석 코퍼스.
C17 Structured Data Extraction: Wrapper Generation (교재 챕터) 2011 Springer (978-3-642-19460-3_9) https://link.springer.com/chapter/10.1007/978-3-642-19460-3_9 🔶 wrapper 생성 전반의 교과서적 정리.
C18 Schema-guided wrapper maintenance for web-data extraction 2003 Proc. 5th ACM Intl. Workshop on Web Information and Data Management (WIDM) https://dl.acm.org/doi/abs/10.1145/956699.956701 🔶 스키마를 힌트로 wrapper 를 유지보수. 우리가 "필드 스키마를 JSON Schema 로 명시"할 때의 이론적 지지.
C19 Intelligent Self-repairable Web Wrappers 2011 Springer (978-3-642-23954-0_26) https://link.springer.com/chapter/10.1007/978-3-642-23954-0_26 🔶 자가 수리 wrapper. LLM 기반 자동 복구의 전신.
C20 Leveraging Flexible Tree Matching to Repair Broken Locators in Web Automation Scripts 2021 arXiv:2106.04916 https://arxiv.org/pdf/2106.04916 🔶 깨진 로케이터를 트리 매칭으로 복구. LLM 없이도 셀렉터 복구가 가능한 경로.
C21 Robust wrappers for web extraction (미국 특허) 등록 US 8,762,829 https://image-ppubs.uspto.gov/dirsearch-public/print/downloadPdf/8762829 🔶 검색 결과로만 확인.
C22 Web data extraction, applications and techniques (서베이) 2014 Knowledge-Based Systems https://dl.acm.org/doi/abs/10.1016/j.knosys.2014.07.007 🔶 웹 데이터 추출 전반 서베이.

2.4 LLM 기반 추출 · 웹 에이전트 (2023~2026)

# 제목 저자 연도 게재처 URL 확인 이 프로젝트에 주는 시사점
D1 AutoScraper: A Progressive Understanding Web Agent for Web Scraper Generation Wenhao Huang, Zhouhong Gu, Chenghao Peng, Zhixu Li, Jiaqing Liang, Yanghua Xiao, Liqian Wen, Zulong Chen 2024 (arXiv 2024-04-19 제출, 2024-09-26 개정) EMNLP 2024 Main, arXiv:2404.12753, ACL Anthology 2024.emnlp-main.141 https://arxiv.org/abs/2404.12753 "LLM 으로 스크레이퍼를 생성한다"는 패러다임을 정의한 논문. 2단계 프레임워크(progressive generation + synthesis)가 HTML 의 계층 구조와 페이지 간 유사성을 활용한다. 새 평가지표 executability 제안. 주장: wrapper 기반 방법은 적응성이 낮고, language agent 방법은 재사용성이 낮은데 AutoScraper 는 둘 다 이긴다. GPT-4-Turbo 로 SWDE 에서 zero-shot 임에도 지도학습 방법보다 높은 F1. → 우리 아키텍처의 정당화 그 자체: LLM 은 스크레이퍼를 "생성"하고, 실행은 생성된 결정론적 스크레이퍼가 한다.
D2 AutoScraper (구현체) EZ-hwh 현재 GitHub, Python, Apache License 2.0, 492 stars / 45 forks / 12 watchers https://github.com/EZ-hwh/AutoScraper crawler_generation.py 로 스크레이퍼를 만들고 crawler_extraction.py 로 추출 실행. LLM(ChatGPT/GPT-4)에 reflexion 패턴 적용. 평가는 SWDE 벤치마크 + DS1 + Klarna. README 에 "실제 웹사이트 적응"과 "공개 데모"가 TODO 로 남아 있어 연구 코드이지 프로덕션 코드가 아니다 → 의존성으로 채택하지 않고, 2단계 분리 아이디어와 reflexion 루프만 차용한다.
D3 Automatic XPath generation agents for vertical websites by LLMs Jing Huang, Jie Song 2025 Journal of King Saud University Computer and Information Sciences, DOI 10.1007/s44443-025-00071-w https://link.springer.com/article/10.1007/s44443-025-00071-w 우리 "셀렉터 생성" 파이프라인의 최적 설계도. 3단계 분해: ① 속성 추출 — HTML 을 먼저 Markdown 으로 변환해 LLM 이해도를 높인 뒤 seed page 2~3장에서 목표 정보를 뽑는다. ② 목표 노드 위치 특정 — 추출된 텍스트를 포함하는 후보 DOM 노드를 찾고, perturbation testing(각 노드의 텍스트를 변조해 LLM 출력이 바뀌는지 관찰; 바뀌면 그 노드가 정답)으로 확정. ③ XPath 생성 — 구조 변화에 취약한 절대 경로 대신 anchor node(주변의 구별되는 요소) 기준 상대 XPath 를 만든다. 결과: F1 86.8392.46, 비교 대상 대비 14.8423.86%p 우위. LLM 호출 수는 직접 추출의 n 회 대비 n_s + p·n_s + 1 회(n_s = seed 페이지 수) — 페이지가 10개만 넘어도 큰 절감. → 우리는 seed 페이지 3장, perturbation 검증, anchor 기반 상대 XPath 를 그대로 채택한다.
D4 AXE: Low-Cost Cross-Domain Web Structured Information Extraction Abdelrahman Mansour, Khaled W. Alshaer, Moataz Elsaban 2026-02-02 제출 (2026-03-30 개정) arXiv:2602.01838 https://arxiv.org/abs/2602.01838 "HTML DOM 을 읽어야 할 텍스트 덩어리가 아니라 가지치기해야 할 트리로 취급한다"가 논지. 3요소: ① DOM Pruning(보일러플레이트·무관 노드 제거로 고밀도 컨텍스트 생성), ② 0.6B 파라미터 소형 LLM 으로 구조화 출력 생성, ③ Grounded XPath Resolution (GXR) — 추출된 모든 값이 소스 노드로 물리적으로 추적 가능하도록 보장. 결과: SWDE F1 88.1% zero-shot 으로 더 크고 완전 학습된 모델을 상회. → GXR 은 우리 감사(audit) 요구와 정확히 일치한다: xlsx 리포트의 모든 셀은 "어느 URL 의 어느 노드에서 왔는가"를 역추적할 수 있어야 한다. 또한 DOM pruning 을 LLM 호출 전에 결정론적으로 수행해 토큰과 비용을 줄인다.
D5 Co-Scraper: query-aware DOM Pruning and Reusable Scraper Synthesis for Lightweight Web Data Extraction Shoupeng Wang, Jiantao Qiu, Wuyang Zhang, Conghui He 2026-06-12 제출 arXiv:2606.14821 https://arxiv.org/abs/2606.14821 유사 페이지에 재사용 가능한 스크레이퍼를 생성하는 2단계(질의 기반 DOM pruning + 추출 전략 귀납). 파인튜닝된 LM 이 HTML 을 실행 가능한 wrapper 로 변환. F1 94.78%, 재사용 성공률 90.39%. → "재사용 성공률"이라는 지표를 우리도 도입한다: 어제 만든 셀렉터가 오늘도 동작한 비율을 매일 기록하고, 이 값이 떨어지면 셀렉터 재생성을 트리거한다.
D6 Mind2Web: Towards a Generalist Agent for the Web Xiang Deng, Yu Gu, Boyuan Zheng, Shijie Chen, Sam Stevens, Boshi Wang, Huan Sun, Yu Su 2023 NeurIPS 2023, Datasets and Benchmarks Track (Spotlight) https://papers.nips.cc/paper_files/paper/2023/hash/5950bf290a1570ea401bf98882128160-Abstract-Datasets_and_Benchmarks.html 137개 웹사이트·31개 도메인에서 2,000개 이상의 open-ended 태스크와 크라우드소싱 액션 시퀀스. 핵심 기술적 교훈: "실제 웹사이트의 raw HTML 은 LLM 입력 한계를 초과하므로, 먼저 소형 LM 으로 필터링하면 LLM 의 효과성과 효율성이 크게 개선된다"(MindAct 의 2단계 구조). → 우리에게 그대로 적용: LLM 에 HTML 전체를 넣지 마라. 관련 서브트리만 잘라서 넣는다(D4 의 DOM pruning 과 동일한 결론에 독립적으로 도달). ⚠️ MindAct 의 후보 랭킹·객관식 액션 예측 세부와 cross-task/website/domain 분할 수치는 공식 페이지에서 확인 실패.
D7 WebVoyager: Building an End-to-End Web Agent with Large Multimodal Models Hongliang He, Wenlin Yao 외 6인 2024 (arXiv 2024-01-25 최초, 2024-06-06 최종) ACL 2024 (Main), arXiv:2401.13919 https://arxiv.org/abs/2401.13919 스크린샷(시각) + 텍스트를 함께 처리하는 멀티모달 웹 에이전트. 15개 실제 사이트 태스크에서 성공률 59.1% — GPT-4(All Tools) 및 텍스트 전용 WebVoyager 를 상회. GPT-4V 자동 평가는 사람 판단과 85.3% 일치. 태스크는 Mind2Web 태스크를 시드로 변형·생성. → 59.1% 는 프로덕션 자동화에 쓸 수 없는 수치다. "브라우저를 LLM 이 직접 조종해 매일 데이터를 긁는다"는 설계는 이 논문이 반증한다. 우리는 브라우저 조종을 최초 1회 셀렉터 발견/디버깅에만 쓰고, 일상 실행은 HTTP + 결정론적 파서로 한다.
D8 OpenWebVoyager: Building Multimodal Web Agents via Iterative Real-World Exploration, Feedback and Optimization 2024 arXiv:2410.19609 https://arxiv.org/pdf/2410.19609 🔶 WebVoyager 오픈 계열. 검색 결과로만 확인.
D9 The AI Committee: A Multi-Agent Framework for Automated Validation and Remediation of Web-Sourced Data Sunith Vallabhaneni, Thomas Berkane, Maimuna Majumder 2025-12-25 제출 arXiv:2512.21481 https://arxiv.org/abs/2512.21481 LLM 에이전트의 전형적 실패를 명시적으로 나열한다: "값을 환각하거나 누락, 페이지 의미 오해, 무효 정보 탐지 실패". 각 에이전트가 출처 검증·팩트체크 등 별개 품질보증 과업을 맡는 다중 에이전트로 3개 실세계 데이터셋에서 완전성 최대 78.7%, 정밀도 최대 100%(태스크별 학습 없이). 오픈소스 공개. → 완전성 78.7% 라는 숫자가 "LLM 에게 본 추출을 맡기면 안 되는" 결정적 근거다. 규제 데이터(DMF)에서 21%의 누락은 허용 불가. LLM 은 검증자(validator) 역할일 때만 가치가 있다.
D10 DELM: a Python toolkit for Data Extraction with Language Models 2025 arXiv:2509.20617 https://arxiv.org/pdf/2509.20617 🔶 동시 실행, 재시도 로직, 그리고 완전히 렌더된 프롬프트·스키마·모델·생성 파라미터를 키로 하는 결정론적 캐싱. → 우리도 LLM 호출에 동일한 캐시 키를 쓴다: sha256(prompt + schema + model_id + temperature + top_p). 같은 입력에 두 번 과금하지 않고, 리포트 재현성도 확보된다.
D11 A Reliability Evaluation of Hybrid Deterministic-LLM Based Approaches for Academic Course Registration PDF Information Extraction 2026 ResearchGate / arXiv:2604.00003 (Tabular PDF Information Extraction with Local LLMs and Layout-Aware Parsing) https://arxiv.org/pdf/2604.00003 🔶 세 전략 비교: LLM-only / Hybrid Deterministic-LLM(정규식 + LLM) / Camelot 파이프라인 + LLM fallback. 결론: 하이브리드가 LLM-only 대비 효율이 좋고, 결정론적 메타데이터에서 특히 그렇다. → DMF 등록번호·날짜·업체명처럼 형식이 고정된 필드는 절대 LLM 에 맡기지 않는다. 정규식/파서로 뽑고, LLM 은 자유서술 필드(변경 사유 요약 등)에만 쓴다.
D12 Prompt2DAG: A Modular Methodology for LLM-Based Data Enrichment Pipeline Generation 2025 arXiv:2509.13487 https://arxiv.org/html/2509.13487v1 🔶 결정론적 Template 기반 방법이 성공률 92.3% 로 최고, 생성형 중에서는 Hybrid 가 78.5% 로 최고 품질. → 파이프라인 자체를 LLM 이 매번 만들게 하지 말고, 템플릿(코드)을 고정하고 파라미터(셀렉터·필드 매핑)만 LLM 이 채우게 한다.
D13 A Hybrid LLM and Supervised Model Pipeline for Polymer Property Extraction from Tables in Scientific Literature 2025 ACL Anthology 2025.wasp-main.11 https://aclanthology.org/2025.wasp-main.11/ 🔶 테이블 추출에서 LLM + 지도학습 모델 하이브리드. 도메인은 다르나 "테이블에서 규제/과학 데이터 추출" 이라는 구조가 우리와 동일.
D14 RATE: An LLM-Powered Retrieval Augmented Generation Technology-Extraction Pipeline 2025 arXiv:2507.21125 https://arxiv.org/html/2507.21125v1 🔶 검색 결과로만 확인.
D15 ZeroShotCeres: Zero-Shot Relation Extraction from Semi-Structured Webpages 2020 ACL https://www.researchgate.net/publication/343297191_ZeroShotCeres_Zero-Shot_Relation_Extraction_from_Semi-Structured_Webpages 🔶 LLM 이전의 zero-shot 반구조 웹 추출. SWDE 계열 비교 기준선.
D16 From one tree to a forest: a unified solution for structured web data extraction 2011 SIGIR https://www.researchgate.net/publication/221299838_From_one_tree_to_a_forest_a_unified_solution_for_structured_web_data_extraction 🔶 검색 결과로만 확인.

2.5 중복·유사도 탐지, 저장 모델

# 제목 저자 연도 게재처 URL 확인 이 프로젝트에 주는 시사점
E1 Detecting Near-Duplicates for Web Crawling Gurmeet Singh Manku (Google), Arvind Jain (Google), Anish Das Sarma (Stanford) 2007 Proc. 16th International World Wide Web Conference (WWW 2007) https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/33026.pdf (PDF 텍스트 추출) 우리 near-duplicate 파라미터의 출처. 원문 기여 3가지: (A) Charikar 의 simhash 가 다중 십억 페이지 저장소의 근접 중복 식별에 실용적임을 입증하고, 80억(8B) 웹페이지 저장소에 대해 64-bit simhash 지문과 k=3 이 합리적임을 실험으로 검증. (B) Hamming Distance Problem 해법: f-bit 지문 집합에서 주어진 지문과 최대 k 비트 다른 것을 빠르게 찾기. (C) 중복 탐지 알고리즘 서베이. 크기 이점: Broder 의 shingle 기반 지문은 지문당 24바이트가 필요한 반면 simhash 는 8바이트(64bit) 로 충분. simhash 의 두 상충 성질: (A) 문서 지문은 그 특징들의 "해시"이고 (B) 유사 문서는 유사한 해시값을 갖는다(암호학적 해시엔 없는 성질). 소박한 해법의 비용: 64-bit·k=3 이면 정렬 테이블 탐색에 C(64,3) = 41,664 회 프로브 필요 → 실용 알고리즘은 순열 + 블록 분할(예: 64비트를 11,11,11,11,10,10 의 6블록으로 나눠 3개 선택 = C(6,3)=20개 테이블, pᵢ=31/32/33, 프로브당 평균 2^(3431)=8개 지문 회수; 또는 16bit×4 블록 → 16개 테이블, pᵢ=28, 프로브당 2^(3428)=64개; 또는 13,13,13,13,12 의 5블록에서 2개 선택 = 10개 테이블). 압축: 연속 지문이 상위 d 비트를 공유하므로 XOR 최상위 1비트 위치에 Huffman 코드(블록 크기 B 전형값 1024바이트)를 적용해 테이블 크기를 약 절반으로.
E2 Similarity Estimation Techniques from Rounding Algorithms (SimHash 원전) Moses Charikar 2002 Proc. 34th Annual ACM Symposium on Theory of Computing (STOC 2002) https://en.wikipedia.org/wiki/SimHash (Wikipedia 경유) 알고리즘: 입력을 특징들로 분해 → 각 특징을 해시 → 비트 위치별로 1의 개수와 0의 개수를 세어(가중치 적용) 1이 우세하면 최종 해시의 그 비트를 1, 아니면 0. 유사 입력 → Hamming 거리가 작은 해시. 전체 문서 비교(O(n²)) 없이 정렬만으로 유사 항목 발견 가능. ⚠️ STOC 2002 원문 PDF 는 Semantic Scholar 429 로 직접 확인 실패.
E3 On the Resemblance and Containment of Documents (MinHash 원전) Andrei Z. Broder 1997 Compression and Complexity of Sequences 1997, IEEE, pp. 2129 https://www.semanticscholar.org/paper/On-the-resemblance-and-containment-of-documents-Broder/8addb1718c2bc6bbb0d82cd1a57b41198bf65965 🔶 (Wikipedia 로 보완 확인) shingling(텍스트를 단어 n-gram 으로 자름) + resemblance = Jaccard 유사도, containment, min-wise independent permutations 스케치. 핵심 성질: P(h_min(A) = h_min(B)) = J(A,B). 표본 내 공통 shingle 수는 초기하분포. AltaVista 검색엔진의 중복 웹페이지 제거에 실제 사용. LSH 스킴이므로 근접 이웃 검색·군집화에 사용 가능(banding). → 우리는 SimHash 를 1순위로 쓴다(지문 8바이트, 짧은 게시판 레코드에 적합). MinHash 는 첨부파일/장문 상세 페이지에 대해 집합 유사도가 필요할 때의 대안으로 남긴다.
E4 python-simhash scrapinghub 아카이브됨 (2026-06-24) GitHub, Python + C 확장(GCC), BSD-3-Clause, 127 stars / 29 forks / 11 watchers https://github.com/scrapinghub/python-simhash 함수: fingerprint()(해시 시퀀스에서 지문 생성), hamming_distance(), simpair_indices()(임계 내 유사 해시 탐색), fnvhash()(FNV-1a). 사용 예: hash1 = fingerprint(map(hash, "some text we want to hash"))hamming_distance(hash1, hash2)2. 아카이브된 저장소이므로 의존성으로 채택하지 않고, 순수 파이썬 60줄로 직접 구현한다(§7.1 코드). BSD-3-Clause 라 참고·재구현에 제약 없음.
E5 Probabilistic near-duplicate detection using simhash Sood, Loguinov 2011 Proc. 20th ACM CIKM https://dl.acm.org/doi/10.1145/2063576.2063737 🔶 simhash 의 확률적 확장. 검색 결과로만 확인.
E6 Event Sourcing Martin Fowler 2005-12-12 martinfowler.com (Enterprise Application Architecture) https://martinfowler.com/eaaDev/EventSourcing.html 정의: "애플리케이션 상태의 모든 변경을 이벤트의 시퀀스로 포착한다." 두 지속 요소 = 이벤트 로그애플리케이션 상태. 상태는 이벤트에서 완전히 유도 가능하므로 스냅샷은 성능 최적화이지 대체물이 아니다. 기능: complete rebuild(상태를 버리고 전 이벤트 재실행), event replay(잘못된 이벤트를 되돌리고 수정 후 재처리), temporal query(특정 시점까지만 재생해 그 시점 상태 확인). 트레이드오프: 외부 시스템 상호작용·코드 변경·이벤트 되돌리기 로직에서 복잡도가 커지므로 기본 선택지가 아니며, 감사(audit)·디버깅·확장성에서 실질 이득이 있을 때만 채택해야 한다. → DMF 는 감사가 본질인 도메인이다. "언제 무엇이 어떻게 바뀌었는가"가 산출물 그 자체이므로 event sourcing 의 채택 조건을 명백히 충족한다.
E7 Type 2 Slowly Changing Dimension (Kimball 기법) Kimball Group kimballgroup.com Dimensional Modeling Techniques https://www.kimballgroup.com/data-warehouse-business-intelligence-resources/kimball-techniques/dimensional-modeling-techniques/type-2/ 변경 시 기존 행을 갱신하지 않고 새 행을 추가한다. 필수 요건: ① 자연키가 아닌 대리키(surrogate key) 를 PK 로, ② 3개 컬럼 — Effective Date(변경 발효), Expiration Date(행 만료), Current Row Indicator(활성 플래그). 완전한 이력 감사 추적을 유지하면서 참조 무결성 유지. → 우리 dmf_record 테이블의 정확한 스키마다. 자연키 = DMF 등록번호, 대리키 = 자동 증가 id, valid_from / valid_to / is_current.
E8 Event Sourcing Pattern (Azure Architecture Center) Microsoft 현재 Microsoft Learn https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing 🔶 검색 결과로만 확인. 구현 가이드로 참조.
E9 Change Data Capture: From Batch to Real-Time Capital One 현재 capitalone.com/tech https://www.capitalone.com/tech/software-engineering/batch-to-real-time-with-change-data-capture/ 🔶 CDC vs Event Sourcing 구분: CDC 는 CRUD 저장소에 사후적으로 이벤트 스트림을 덧씌운 것으로 도메인 의도 없는 행 델타이고, Event Sourcing 은 애플리케이션 수준의 도메인 이벤트를 포착한다. → 우리는 "신규 등록 / 성분 변경 / 취하"라는 도메인 이벤트를 쓴다. "row updated" 같은 CDC 수준 이벤트는 리포트에 쓸모가 없다.
E10 Event Sourcing (arc42 Quality Model) arc42 현재 quality.arc42.org https://quality.arc42.org/approaches/event-sourcing 🔶 스냅샷은 "일정 개수의 이벤트가 쌓이면 만드는 최적화"라는 실무 규칙 제시. → 우리는 이벤트 수 기준이 아니라 매 실행마다 스냅샷을 남긴다(하루 1회이므로 비용이 무의미하게 작다).

2.6 표준 · 도구 문서

# 제목 저자/발행 연도 게재처 URL 확인 이 프로젝트에 주는 시사점
F1 RFC 9309: Robots Exclusion Protocol M. Koster, G. Illyes, H. Zeller, L. Sassman (Google LLC) 2022-09 IETF, Internet Standard https://www.rfc-editor.org/rfc/rfc9309.html §8 에 전문 인용. 1994년 Martijn Koster 가 정의한 방식을 표준화·확장.
F2 Google robots.txt 사양 문서 Google 현재 developers.google.com https://developers.google.com/search/docs/crawling-indexing/robots/robots_txt "Google 은 다음 필드를 지원한다(crawl-delay 같은 다른 필드는 지원하지 않는다)", "allow / disallow / user-agent 외의 규칙은 파서가 무시한다", "가장 구체적인 user agent 그룹을 선택한다", "일반적으로 최대 24시간 캐시, 갱신 불가 상황에선 더 길게", "500 KiB 파일 크기 제한, 초과분 무시".
F3 urllib.robotparser (Python 표준 라이브러리) Python Software Foundation 현재 docs.python.org https://docs.python.org/3/library/urllib.robotparser.html RobotFileParser 메서드: set_url(url), read(), parse(lines), can_fetch(useragent, url), crawl_delay(useragent) (3.6 추가), request_rate(useragent)RequestRate(requests, seconds) (3.6 추가), site_maps() (3.8 추가), mtime(), modified(). → 표준 라이브러리만으로 RFC 준수 파싱이 가능하다. 외부 의존성 불필요. 단, 5xx→완전 금지 규칙은 직접 구현해야 한다(§8.3 코드).
F4 Scrapy AutoThrottle 문서 Scrapy 현재 docs.scrapy.org https://docs.scrapy.org/en/latest/topics/autothrottle.html 설정과 기본값: AUTOTHROTTLE_ENABLED(기본 False), AUTOTHROTTLE_START_DELAY(기본 5.0초), AUTOTHROTTLE_MAX_DELAY(기본 60.0초), AUTOTHROTTLE_TARGET_CONCURRENCY(기본 1.0, "값이 낮을수록 크롤러가 보수적이고 정중해진다"), DOWNLOAD_DELAY(최소 하한, AutoThrottle 이 이 아래로 내리지 않음). 알고리즘: ① 시작 지연으로 출발 → ② 응답 수신 시 목표 지연 = latency / N(N = target concurrency) → ③ 다음 요청 지연 = (이전 지연 + 목표 지연) / 2 → ④ 비 200 응답은 지연을 늘리기만 하고 줄이지 않는다 → ⑤ 최종 지연은 DOWNLOAD_DELAYAUTOTHROTTLE_MAX_DELAY 사이로 클램프. → Scrapy 를 안 쓰더라도 이 4단계 알고리즘을 그대로 구현한다(§10 표). ⚠️ 문서 내에 ROBOTSTXT_OBEY 언급은 없음.
F5 Heritrix User Manual Kristinn Sigurðsson, Michael Stack 외 crawler.archive.org http://crawler.archive.org/articles/user_manual.pdf 🔶 검색 결과로만 확인. A5 의 readthedocs 문서로 대체 확인함.
F6 Respecting Robots Exclusion Protocol or robots.txt at Scale Rashad Moarref (GumGum Tech) Medium https://medium.com/gumgum-tech/respecting-robots-exclusion-protocol-or-robots-txt-at-scale-60ee57dc1295 🔶 대규모 robots.txt 준수 실무기. 검색 결과로만 확인.
F7 RFC 9309 — status, mechanics & checks AgentGrade 현재 agentgrade.com https://agentgrade.com/standards/rfc-9309 🔶 검색 결과로만 확인.
F8 How Attackers Exploit robots.txt? Baeldung 현재 baeldung.com/cs https://www.baeldung.com/cs/robots-txt-risk-threat 🔶 robots.txt 의 보안적 함의. 우리는 robots.txt 를 "숨겨진 경로 목록"으로 쓰지 않는다(윤리·법적 리스크).

2.7 실무 블로그 (셀렉터 파손 / schema drift)

# 제목 발행 URL 확인 핵심 내용 및 시사점
G1 How to Fix Web Scraping Errors: 2026 Complete Troubleshooting Guide PromptCloud https://www.promptcloud.com/blog/how-to-fix-web-scraping-errors-2026/ 🔶 검색 요약으로 확인: schema drift = 페이지 구조가 조금 바뀌어 CSS/XPath 셀렉터가 깨지는 현상. 프로덕션 스크레이퍼는 DOM 스냅샷 시점 기준으로 셀렉터를 작성하므로, 사이트가 레이아웃을 개편하거나 class 명을 바꾸거나 테이블을 재구성하면 셀렉터가 조용히 아무것도 반환하지 않거나 잘못된 데이터를 반환한다.
G2 Managing Change in Web Scraping: 10 Critical Challenges PromptCloud https://www.promptcloud.com/blog/managing-change-in-web-scraping-10-challenges/ 🔶 대응책: 야간 검증 실행으로 출력 필드 개수를 과거 기준선과 비교, 셀렉터 버저닝(스크레이퍼에 스키마 날짜 태깅 후 이상 자동 플래그). 가장 위험한 실패는 "셀렉터가 여전히 무언가에 매치되지만 그게 올바른 노드가 아닌" 조용한 정확성 드리프트(silent correctness drift). → 우리 검증기는 "0건"뿐 아니라 "건수는 맞는데 값이 이상한" 경우까지 잡아야 한다(§5.7).
G3 Web Scraping With XPath and CSS Selectors Crawlbase https://crawlbase.com/blog/web-scraping-with-xpath-and-css-selectors/ 🔶 시각적 class 보다 안정 속성 우선: data-testid, id, itemprop, ARIA role 이 리스타일을 살아남을 확률이 훨씬 높다. 길고 깊은 셀렉터 체인은 레이아웃 전체를 인코딩하므로 경로 중간에 래퍼 하나만 추가돼도 깨진다.
G4 When the Scraper Breaks Itself: Building a Self-Healing CSS Selector Repair System Vinicius Puerto (DEV) https://dev.to/viniciuspuerto/when-the-scraper-breaks-itself-building-a-self-healing-css-selector-repair-system-312d 🔶 자가 치유 셀렉터 시스템 실무 사례. 우리 LLM 복구 루프의 참고 패턴.
G5 What are CSS selectors and XPath in web extraction? / What is an xpath selector in web scraping? Firecrawl Glossary https://www.firecrawl.dev/glossary/web-extraction-apis/what-are-css-selectors-xpath-web-extraction · https://www.firecrawl.dev/glossary/web-scraping-apis/what-is-xpath-selector-in-web-scraping 🔶 용어 정리.

3. 크롤러 아키텍처 고전 — 프론티어·politeness·중복 제거·재방문

3.1 Mercator 의 부품 목록 (Heydon & Najork 1999; Najork & Heydon 2001)

SRC Research Report 173 의 Figure 1 이 명시하는 Mercator 구성 요소는 다음과 같다(원문 표기 그대로):

Protocol Modules:  HTTP, FTP, Gopher
Processing Modules: Link Extractor, GIF Stats, Tag Counter
공통 부품:          DNS Resolver, RIS, Content Seen?, URL Filter, DUE, URL Frontier
저장소:             Queue Files, URL Set, Log, Doc FPs

원문이 정의하는 크롤러 기본 알고리즘:

"Remove a URL from the URL list, determine the IP address of its host name, download the corresponding document, and extract any links contained in it. For each of the extracted links, ensure that it is an absolute URL (derelativizing it if necessary), and add it to the list of URLs to download, provided it has not been encountered before. If desired, process the downloaded document in other ways (e.g., index its content)."

우리 프로젝트로의 매핑:

Mercator 부품 원래 역할 DMF_Crawler 에서의 축소 구현
URL Frontier 다운로드 대기 URL 우선순위 큐 frontier.py — 시드 = DMF 공고 목록 URL(페이지 파라미터 포함). 우선순위 = 목록 페이지 > 상세 페이지. 호스트가 1개이므로 back-end 큐는 1개.
DNS Resolver 호스트명 → IP, 캐시 필수 OS/requests 기본 해석기 + 세션 keep-alive. 별도 구현 불필요하나 DNS 실패는 별도 에러 코드로 분류(네트워크 장애 vs 사이트 개편 구분).
Protocol Module (HTTP) 프로토콜별 fetch fetcher.pyrequests.Session 1개, 재시도·백오프·조건부 요청 담당.
RIS (RewindInputStream) 임의 입력 스트림을 여러 번 다시 읽을 수 있게 하는 I/O 추상 원본 HTML 바이트를 raw/YYYY-MM-DD/<sha1>.html.gz먼저 저장한 뒤 파싱한다. 파싱 실패 시 재파싱·소급 재처리가 가능해진다. Mercator 가 RIS 를 둔 이유와 동일.
Content-Seen Test 다른 URL이지만 같은 내용인 문서를 걸러냄 (Doc FPs) dedup.py — 레코드 지문(§7). Mercator 는 문서 단위였지만 우리는 레코드 단위로 내린다.
URL Filter 스코프 밖 URL 제거 도메인 화이트리스트 + 경로 정규식. 외부 링크는 절대 따라가지 않는다.
DUE (URL-seen test) 이미 큐에 넣은 URL 재삽입 방지. Rabin fingerprint 8바이트 체크섬 해시테이블. 체크섬 상위 3바이트를 스파인 인덱스로, 하위 5바이트만 저장 → 10억 URL ≈ 5GB set[str] 로 충분(URL 수 수백). Rabin 지문은 불필요하나, 정규화된 URL 문자열(쿼리 파라미터 정렬, 세션 ID 제거)을 키로 쓰는 원칙은 유지.
Checkpointing "장기 실행 프로세스의 필수 요소". 실패 시 체크포인트를 읽어 정확히 그 시점 상태로 복구할 수 있을 만큼의 상태를 안정 저장소에 기록 state.json + SQLite WAL. 목록 페이지 n장 중 k장까지 처리했다는 커서를 기록해, 중단 후 재실행 시 이어서 진행. 재부팅 자동 복구 요구사항과 직결.
백그라운드 스레드 기본 10초마다 깨어나 통계 로깅 / 종료 조건 확인 / 체크포인트 시점 판단 헬스체크 루프. 우리는 실행이 짧으므로 10초 대신 각 페이지 처리 후 통계 기록 + 체크포인트.
robots 캐시 호스트명 → robots 규칙, 기본 2^18 엔트리 LRU 호스트 1개이므로 단일 엔트리. RFC 9309 의 24시간 캐시 규칙만 지킨다.

3.2 Frontier 의 front-end / back-end 2단 구조 = politeness 의 구조적 보장

SRC 173 원문:

"The frontier consists of a front-end (the top part of the figure) that is responsible for prioritizing URLs, and a back-end (the bottom part of the figure) that is responsible for ensuring strong politeness. When a URL u is added to the frontier, a pluggable prioritizer component computes a priority value p between 1 and k based on the URL and its download history (e.g. whether the document has changed since the last download), and inserts u into front-end FIFO queue p. The back-end maintains n FIFO queues, each of which is guaranteed to be non-empty and to contain URLs of one [host]."

핵심 통찰 3가지:

  1. politeness 를 "sleep 을 잘 넣자"는 규율 문제가 아니라 자료구조 문제로 바꿨다. back-end 큐가 호스트당 1개이고 큐당 워커가 1개면, 동일 호스트 동시 접속은 구조적으로 불가능하다. → 우리 코드도 HostQueue 클래스를 만들어, 요청 함수가 이 큐를 통해서만 호출되게 강제한다. requests.get 을 아무 데서나 부르지 못하게 한다.
  2. 우선순위 계산에 "지난번 다운로드 이후 문서가 변경되었는가"가 명시적으로 들어간다. 이것이 §4 의 증분 크롤 이론과 아키텍처가 만나는 지점이다.
  3. front-end/back-end 분리 덕에 우선순위 로직과 politeness 로직이 서로 오염되지 않는다.

3.3 Mercator 의 politeness 정의 (원문)

"Despite the need for speed, anyone running a web crawler that overloads web servers soon learns that such behavior is considered unacceptable. At the very least, a web crawler should not attempt to download multiple pages from the same web server simultaneously; better, it should impose a limit on the portion of a web server's resources it consumes."

두 단계가 명시돼 있다: 최소 요건 = 동시 접속 금지, 더 나은 요건 = 서버 자원 점유율 상한. 우리는 둘 다 채택한다(동시성 1 + delayFactor 5.0).

성능 참고치(원문): Compaq DS20E 666MHz Alpha 서버 4대, 160 Mbit/sec 회선 포화 상태에서 17일간 하루 약 5,000만 문서 다운로드. 우리는 하루 수십수백 요청이다 — **56 자릿수의 여유**가 있으므로 속도 최적화는 전면 금지하고 전부 안전성에 쓴다.

3.4 robots.txt 캐싱 (Mercator 원문)

"Courteous web crawlers implement the Robots Exclusion Protocol, which allows web masters to declare parts of their sites off limits to crawlers. The Robots Exclusion Protocol requires a web crawler to fetch a resource named '/robots.txt' containing these declarations from a web site before downloading any real content from it. To avoid downloading this resource on every request, Mercator's HTTP protocol module maintains a fixed-sized cache mapping host names to their robots exclusion rules. By default, the cache is limited to 2^18 entries, and uses an LRU replacement strategy."

매 요청마다 robots.txt 를 받지 마라. 실행 시작 시 1회 받아 프로세스 수명 동안 캐시하고, RFC 9309 의 24시간 상한을 지키면 하루 1회 실행에서는 실행당 정확히 1회 fetch 가 된다.

3.5 Content-Seen Test (Mercator 원문)

"Once the document has been written to the RIS, the worker thread invokes the content-seen test to determine whether this document with the same content, but a different URL, has been seen before. If so, the document is not processed any further, and the worker thread goes back to step 1."

→ 우리의 대응물: 같은 DMF 레코드가 목록 페이지와 상세 페이지, 혹은 페이징 경계에서 중복 등장할 수 있다. 레코드 지문 테이블로 한 번 본 레코드를 두 번 처리하지 않는다.

3.6 Heritrix — politeness 를 설정으로 노출한다

heritrix.readthedocs.io/en/latest/configuring-jobs.html 에서 확인된 프로퍼티(정확한 이름):

프로퍼티 의미 기본값 DMF_Crawler 채택값
delayFactor "직전 URI 를 가져오는 데 걸린 시간의 배수"만큼 대기 5.0 5.0 (그대로)
minDelayMs 요청 간 최소 대기. delayFactor 계산값보다 우선 2000
maxDelayMs politeness 지연 상한 30000 30000 (그대로)
maxPerHostBandwidthUsageKbSec 호스트당 최대 대역폭 미사용(요청 수가 적음)
robotsPolicyName obey / classic / robotsTxtOnly / ignore. RFC 9309 경로 와일드카드 지원 obey obey (그대로)
metadata.operatorContactUrl "문제 발생 시 크롤 대상 호스트 관리자가 참조할 URI 제공" User-Agent 에 연락처 포함
maxToeThreads 워커 스레드 수(도메인 크롤 시 호스트 수의 약 2배 권장) 1
extract404s 404 응답에서 링크 추출 여부 false

Heritrix3 README 에서 확인된 개념: robots.txt 및 META nofollow 준수, 크롤 잡 단위 설정, WARC 출력, Frontier 큐잉. Java, Apache License 2.0.

→ WARC 는 우리에게 "원본 보존" 원칙으로 번역된다. 파싱 결과만 저장하고 원본을 버리면, 셀렉터가 깨진 날의 데이터를 되살릴 방법이 없다.

3.7 Olston & Najork 서베이가 정의하는 문제 공간

"Though the idea of web crawling may seem straightforward — a simple application of breadth-first-search — the field actually presents numerous challenges, ranging from systems concerns such as managing very large data structures to theoretical questions such as how often to revisit evolving content sources." — Web Crawling, Foundations and Trends in IR 4(3):175246, 2010, DOI 10.1561/1500000017

이 프로젝트에서 두 축의 무게는 극단적으로 비대칭이다:

웹 스케일 크롤러 DMF_Crawler
초대형 자료구조 관리 지배적 난제 (URL frontier 수십억, 지문 테이블 수 TB) 사실상 0 — SQLite 한 파일
분산·병렬화 필수 불필요 — 단일 프로세스
politeness 필수 필수 (동일)
재방문 주기 / 변경 감지 이론적 난제 본질적 난제 — 이 프로젝트의 전부
구조적 추출 정확도 부차적(검색 인덱싱은 잡음에 관대) 치명적 — 규제 데이터는 1건 누락도 실패

→ 그러므로 이 프로젝트의 엔지니어링 예산은 재방문 정책(§4) + 추출 정확도/검증(§5) + diff 정확도(§7) 에 몰아야 한다. 성능·확장성 작업은 전부 금지 항목이다.

3.8 실측 crawl-delay 값 레퍼런스 (Wikipedia "Web crawler" 정리)

크롤러/연구 접근 간격 정책
MercatorWeb 적응형 — 직전 다운로드 소요 시간의 10배 대기
Cho 10초 고정
WIRE 기본 15초
관측된 실제 접근 간격 20초 ~ 34분
Heritrix delayFactor 5.0 × 직전 소요 시간, minDelayMs/maxDelayMs(기본 30000) 로 클램프

→ 우리의 min_delay = 2.0s 는 이 스펙트럼에서 가장 공격적인 쪽이다. 정당화: 하루 요청 총량이 수십 건에 불과하므로 절대 부하가 무의미하게 작다. 그러나 delayFactor 5.0 이 살아 있으므로, 서버 응답이 3초로 느려지면 자동으로 15초 간격이 된다. 안전은 하한이 아니라 적응 규칙이 보장한다.


4. 증분 크롤링과 변경 감지 이론

4.1 주기적(periodic) vs 증분(incremental) 크롤러

Cho & Garcia-Molina (VLDB 2000) 가 대비시킨 두 설계:

주기적 크롤러 증분 크롤러
동작 컬렉션 전체를 새로 긁고 통째로 교체 로컬 컬렉션을 유지하며 변경분만 갱신
신선도 크롤 주기 = 최대 지연 지속적으로 최신에 가까움
비용 매번 전체 변경 감지 비용 + 변경분만
이력 없음(교체됨) 자연스럽게 축적

DMF_Crawler 의 선택 = 하이브리드:

  • 수집은 주기적으로 — 매일 06:00 에 목록 페이지 전체를 다시 긁는다. DMF 게시판은 총 레코드가 수천 건 규모이고 페이지 수가 적어, "전체를 보고 비교"하는 것이 "변경만 골라내는" 것보다 단순하고 정확하다.
  • 저장은 증분으로 — 긁은 결과를 통째로 덮어쓰지 않고, 키 기반 upsert 로 신규/변경/취하 이벤트를 생성한다. "취하(사라짐)" 탐지는 전체 스냅샷 비교로만 가능하다 — 이것이 목록 전체를 매일 긁어야 하는 결정적 이유다.

⚠️ VLDB 2000 원문은 ACM 403, Semantic Scholar 429 로 본문 확인 실패. 위 대비는 논문 초록·2차 출처 요약에 기반하며, 페이지 변경률 실측치·half-life 수치는 확인하지 못했다(부록 B 항목).

4.2 Poisson 변경 모델과 freshness / age

Cho & Garcia-Molina (TODS 2003) 의 핵심 명제:

"A Poisson process is a good model to describe the changes of Web pages." "the time between changes follow an exponential distribution λe^(λt) if the change frequency of the page is λ"

정의 (Wikipedia "Web crawler" 의 Re-visit policy 절에서 확인된 표준 정의):

  • Freshness F(p; t) — 이진값. 로컬 사본이 시각 t 에 최신이면 1, 아니면 0.
  • Age A(p; t) — 로컬 사본이 낡은 채로 얼마나 오래 있었는가. 원본이 수정된 시점부터 경과한 시간.

고정 주기 I 로 재방문할 때의 기대값 (Poisson 변경률 λ 가정, 표준 유도):

한 번 동기화한 직후를 t=0 이라 하면, 시각 t 에 사본이 여전히 최신일 확률은 "그 사이 변경이 0회 발생할 확률" 이므로

P[F(t) = 1] = e^(λt)

주기 I 에 대한 시간 평균 freshness:

        1  ⌠I           1  e^(λI)
F̄(I) = ─  │ e^(λt) dt = ───────────
        I  ⌡0                λI

시각 t 에서의 기대 age 는 E[A(t)] = t (1 e^(λt))/λ 이므로, 시간 평균 age:

        1  ⌠I ⎡     1  e^(λt)⎤        I    1    1  e^(λI)
Ā(I) = ─  │  ⎢t  ───────────⎥ dt  =  ─   ─  + ───────────
        I  ⌡0 ⎣          λ     ⎦        2    λ       λ²I

⚠️ 위 수식은 표준 Poisson 유도로 재구성한 것이다. Cho & Garcia-Molina 원문 PDF(oak.cs.ucla.edu/~cho/papers/cho-tods03.pdf, dl.acm.org/doi/pdf/10.1145/958942.958945)는 각각 ECONNREFUSED / 403 으로 본문 확인 실패. Poisson 모델과 지수분포 λe^(λt) 라는 명제 자체는 검색 결과에서 확인됨. 수식 표기는 원문과 다를 수 있다(부록 B 항목).

DMF_Crawler 실측 대입 (I = 1일):

게시판 성격 λ (건/일) λI Ā (일) Ā (시간)
매우 한산 (주 1회 변경) 0.14 0.14 0.932 0.047 1.1h
한산 (5일에 1회) 0.20 0.20 0.906 0.065 1.6h
보통 (2일에 1회) 0.50 0.50 0.787 0.148 3.6h
활발 (매일 1회) 1.00 1.00 0.632 0.264 6.3h
매우 활발 (하루 3회) 3.00 3.00 0.317 0.394 9.5h

해석 — 그리고 이것이 왜 우리 문제에서 오해를 부르는가: 위 F̄ 는 "임의의 순간에 사본이 최신일 확률"이다. 그러나 DMF_Crawler 의 실제 SLA 는 "게시된 공고를 며칠 안에 리포트에 싣는가" 이고, 하루 1회 균등 크롤에서 이 값은 λ 와 무관하게 항상 최대 24시간, 평균 12시간이다(공고가 하루 중 균등하게 올라온다고 가정). freshness/age 는 "실시간 캐시 품질" 지표이지 "탐지 지연" 지표가 아니다.

문서화할 SLA: 최대 탐지 지연 = 24시간 + 실행 소요 시간, 평균 탐지 지연 ≈ 12시간. 이것이 요구사항("매일 06:00 1회")에서 수학적으로 도출되는 값이며, 더 좋게 만들려면 주기를 줄이는 방법밖에 없다. freshness 를 올리려고 스케줄링을 정교하게 만드는 것은 효과가 없다.

4.3 균등(uniform) vs 비례(proportional) — 반직관적 핵심 결과

Wikipedia "Web crawler" Re-visit policy 절에서 확인된 서술:

"Cho and Garcia-Molina [demonstrated] that the uniform policy outperforms the proportional policy in terms of average freshness" "The optimal [policy] is closer to the uniform policy than to the proportional policy" Coffman et al.: "accesses to any particular page should be kept as evenly spaced as possible"

두 정책:

  • Proportional: 변경률 λᵢ 에 비례해 재방문 빈도를 배분. 직관적으로 옳아 보인다.
  • Uniform: 모든 페이지를 같은 빈도로 재방문.

왜 균등이 이기는가: 매우 자주 바뀌는 페이지는 아무리 자주 긁어도 금방 낡는다 — 그 페이지에 자원을 쓰면 한계 효용이 급감한다. 반면 그 자원을 덜 바뀌는 페이지에 쓰면 그 페이지는 오래 최신 상태를 유지한다. 총 freshness 를 최대화하려면 낭비되는 곳(초고빈도 변경 페이지)에서 자원을 빼야 한다.

DMF_Crawler 의 결론 (확정):

  • 대상 게시판이 여러 개여도 모두 하루 1회 균등으로 긁는다.
  • "이 게시판은 자주 바뀌니까 하루 3번 긁자"는 최적화는 하지 않는다.
  • Coffman 의 "가능한 한 균등 간격" 원칙에 따라, 매일 정확히 06:00(±지터 최소)에 실행한다. 실행 시각이 들쭉날쭉하면 간격 분산이 커져 이론적 이점이 사라진다.

4.4 λ 의 온라인 추정 — 구간 검열(interval-censored) 최우추정

우리는 방문 사이에 몇 번 바뀌었는지 볼 수 없다. "바뀌었다 / 안 바뀌었다"만 관측한다(interval censoring). 주기 I 로 n 회 방문해 그중 X 회에서 변경이 관측되었다면:

P[한 주기 안에 1회 이상 변경] = 1  e^(λI)
X / n  ≈  1  e^(λI)
       ⇒  λ̂ =  ln(1  X/n) / I

경계 처리: X = n(매번 변경)이면 λ̂ = ∞ 이므로, Laplace 보정을 적용한다.

λ̂ =  ln(1  (X + 0.5) / (n + 1)) / I

n < 14(2주 미만 관측)이면 추정을 신뢰하지 않고 사전값 λ₀ = 0.5 건/일 을 쓴다.

# src/dmf_crawler/change_rate.py
"""Poisson 변경률 λ 의 구간 검열 최우추정 (Cho & Garcia-Molina 2003 의 Poisson 모델 기반).

관측 모델:
  - 주기 I(일) 로 n 회 방문했고, 그중 X 회에서 '이전 방문 대비 변경'이 관측되었다.
  - 한 주기 내 1회 이상 변경 확률 = 1 - exp(-lambda * I)
"""
from __future__ import annotations

import math
from dataclasses import dataclass

PRIOR_LAMBDA_PER_DAY = 0.5   # 관측이 부족할 때 쓰는 사전값
MIN_OBSERVATIONS = 14        # 이 미만이면 사전값 사용
ANOMALY_RATIO = 3.0          # 최근 λ 가 장기 λ 의 이 배를 넘으면 이상 급증


@dataclass(frozen=True)
class ChangeRateEstimate:
    lambda_per_day: float
    n_observations: int
    n_changes: int
    is_prior: bool

    def expected_freshness(self, interval_days: float = 1.0) -> float:
        """F̄(I) = (1 - e^(-λI)) / (λI)"""
        x = self.lambda_per_day * interval_days
        if x <= 1e-12:
            return 1.0
        return (1.0 - math.exp(-x)) / x

    def expected_age_days(self, interval_days: float = 1.0) -> float:
        """Ā(I) = I/2 - 1/λ + (1 - e^(-λI)) / (λ²I)"""
        lam = self.lambda_per_day
        if lam <= 1e-12:
            return interval_days / 2.0
        x = lam * interval_days
        return interval_days / 2.0 - 1.0 / lam + (1.0 - math.exp(-x)) / (lam * lam * interval_days)


def estimate_lambda(n_observations: int, n_changes: int, interval_days: float = 1.0) -> ChangeRateEstimate:
    if interval_days <= 0:
        raise ValueError("interval_days must be positive")
    if n_observations < 0 or n_changes < 0 or n_changes > n_observations:
        raise ValueError("invalid observation counts")

    if n_observations < MIN_OBSERVATIONS:
        return ChangeRateEstimate(PRIOR_LAMBDA_PER_DAY, n_observations, n_changes, is_prior=True)

    # Laplace 보정: X = n 일 때 발산을 막는다.
    p = (n_changes + 0.5) / (n_observations + 1.0)
    p = min(max(p, 1e-9), 1.0 - 1e-9)
    lam = -math.log(1.0 - p) / interval_days
    return ChangeRateEstimate(lam, n_observations, n_changes, is_prior=False)


def is_anomalous_burst(recent: ChangeRateEstimate, longterm: ChangeRateEstimate) -> bool:
    """최근 변경률이 장기 변경률 대비 급등했는지 판정. 주기는 바꾸지 않고 리포트에 플래그만 단다."""
    if recent.is_prior or longterm.is_prior:
        return False
    if longterm.lambda_per_day <= 1e-9:
        return recent.lambda_per_day > 0.0
    return recent.lambda_per_day / longterm.lambda_per_day >= ANOMALY_RATIO


if __name__ == "__main__":
    for n, x in [(30, 6), (30, 15), (30, 30), (5, 2)]:
        est = estimate_lambda(n, x)
        print(
            f"n={n:3d} X={x:3d}  lambda={est.lambda_per_day:6.3f}/day  "
            f"F̄={est.expected_freshness():.3f}  Ā={est.expected_age_days()*24:5.2f}h  prior={est.is_prior}"
        )

이 추정값의 용도 (그리고 용도가 아닌 것):

  • 리포트의 "이 게시판은 평소 며칠에 한 번 바뀝니다" 컨텍스트 제공.
  • 이상 급증 탐지 — λ̂_recent(최근 14일) ≥ 3 × λ̂_longterm(전체) 이면 리포트 상단에 경고 배지.
  • 이상 정지 탐지 — 평소 λ 가 0.5 인 게시판에서 30일간 변경 0건이면 "셀렉터가 조용히 깨졌을 가능성"을 의심한다(§5.7 의 검증과 교차 확인).
  • 크롤 주기 변경에는 쓰지 않는다 (§4.3 균등 정책 유지).

4.5 조건부 요청 — 서버가 알려주는 "변경 없음"

λ 추정과 별개로, HTTP 레벨에서 변경을 저비용으로 판별하는 표준 수단이 있다.

헤더 방향 의미
Last-Modified 응답 리소스 최종 수정 시각
ETag 응답 리소스 버전 식별자
If-Modified-Since 요청 이 시각 이후 변경됐을 때만 본문 전송
If-None-Match 요청 이 ETag 와 다를 때만 본문 전송
304 Not Modified 응답 변경 없음 — 본문 없음

정책 (확정):

  • 저장해 둔 ETag/Last-Modified 가 있으면 항상 조건부 요청을 보낸다.
  • 304 를 받아도 "변경 없음"으로 즉시 확정하지 않는다. 이는 §4.6 의 noisy signal 원칙에 따른다. 304 는 "본문 다운로드를 생략해도 좋다"는 신호일 뿐이며, 정부 게시판의 CMS 가 Last-Modified 를 정확히 관리한다는 보장이 없다. 목록 페이지 1페이지만은 조건부 요청 없이 무조건 다시 받아 파싱한다.
  • 상세 페이지에 대해서는 304 를 신뢰해 다운로드를 생략하되, 주 1회(일요일)는 조건부 요청을 끄고 전체를 다시 받아 검증한다.

4.6 잡음 섞인 변경 신호를 어떻게 다룰 것인가 (Busa-Fekete et al. 2025)

arXiv:2502.02430 의 문제의식:

sitemap·CDN 같은 side information 은 유용하지만 오탐(false positive)이 있고 실제 갱신을 놓치기도 한다. 기존 연구(Azar et al. 2018)는 변경·요청이 각각 독립 Poisson 과정이라는 이상화된 가정 위에 최적 스케줄을 유도했으나, 실제 신호는 잡음이 있다.

DMF 게시판에서의 noisy change-indicating signal 목록과 신뢰도 등급:

신호 취득 비용 신뢰도 정책
목록 페이지의 "총 게시물 수" 표시 매우 낮음 중 — 증가는 신뢰, 감소·동일은 불신(수정은 총수를 안 바꾼다) 증가 시 즉시 상세 크롤 트리거
목록 1페이지 최상단 게시일 매우 낮음 중 — 상단 고정 공고에 의해 왜곡됨 보조 신호로만
Last-Modified / ETag 낮음(HEAD 또는 조건부 GET) 낮음 — 정부 CMS 는 동적 생성으로 매번 바뀌거나 아예 없는 경우가 많음 상세 페이지에만, 주 1회 전체 재검증
RSS/Atom 피드(존재 시) 낮음 중상 존재 여부 실측 필요 (부록 B)
공공데이터포털/식의약 데이터 포털 OpenAPI 낮음 높음 — 정형 데이터 존재 시 1순위 소스로 전환, 크롤링은 대조·보완용 (부록 B)
목록 페이지 전체 파싱 결과 높음 최종 근거. 매일 무조건 수행.

확정 규칙: 위 신호들은 상세 페이지 크롤을 생략할지 결정하는 데만 쓴다. "오늘은 신호가 없으니 크롤을 건너뛴다"는 절대 하지 않는다. 신호는 비용을 줄이고, 진실은 항상 목록 페이지 파싱이 정한다.

4.7 자원 배분 이론이 준비해 둔 확장 경로 (Azar et al. 2018 / Kolobov et al. 2019)

지금은 필요 없지만, 대상이 수십 개 게시판으로 늘어날 때를 위해 기록해 둔다.

Azar et al., PNAS 2018 의 정식화:

  • 페이지 i 의 Poisson 변경률 Δᵢ, 사용자 요청률 μᵢ, 총 폴링 대역폭 R.
  • 목적: freshness 가중 효용 최대화(요청 시 최신 페이지를 제공).
  • Algorithm 1(이산시간) / Algorithm 2(연속시간): utility-to-change-rate 비로 정렬 후 할당 0인 페이지를 식별해 유일한 최적 무작위 정책을 O(n log n) 에 계산.
  • Algorithm 3: EDF(earliest-deadline-first) 로 비무작위화 → 실험에서 수치 최적해의 99% 달성. 확률적 할당을 실용적 순환 refresh 스케줄로 변환.

Kolobov et al., NeurIPS 2019: 변경 관측이 부분적이고 파라미터를 모르는 상황에서도 최적성 보장이 있는 알고리즘. 18.5M URL 을 14주간 매일 크롤한 실험으로 검증.

확장 트리거: 대상 게시판 수가 10개를 넘고, 실행 시간이 politeness 제약 때문에 30분을 넘기 시작하면, 그때 μᵢ(사용자가 실제로 어느 탭을 보는가) × Δᵢ(추정 λ) 로 정렬해 상위 그룹만 하루 2회로 올린다. 그 전에는 하지 않는다.


5. 구조적 데이터 추출 — wrapper induction 부터 셀렉터 안정성까지

5.1 Wrapper induction 의 계보

1997  Kushmerick, Weld, Doorenbos — Wrapper Induction for Information Extraction (IJCAI-97)
      · LR wrapper: 문서를 문자 시퀀스로 보고 좌/우 구분자로 필드를 자름
      · hlrt 클래스: 효율적 학습 가능 + 조사 대상 인터넷 리소스의 48% 커버
      · PAC 분석으로 표본 복잡도 상한, 불완전 라벨링에 완만한 성능 저하
        ↓
1998  Kushmerick — WIEN (AAAI Workshop WS-98-10)
        ↓
1999  Kushmerick — RAPTURE (AAAI-99)  ★ wrapper verification 문제 정의
        ↓
2001  Crescenzi, Mecca, Merialdo — RoadRunner (VLDB 2001)  ★ 완전 자동, 2페이지 비교
        ↓
2003  Lerman, Minton, Knoblock — Wrapper Maintenance (JAIR 18:149-181)  ★ verification + reinduction
        ↓
2009  Dalvi, Bohannon, Sha — Robust web extraction (SIGMOD 2009)  확률적 tree-edit
2011  Dalvi, Kumar, Soliman — Automatic Wrappers for Large Scale Web Extraction (PVLDB 4(4):219-230)
        ↓
2016  Leotta, Stocco, Ricca, Tonella — ROBULA+ (JSEP 28(3):177-204)  ★ 강건 XPath 생성
        ↓
2024~ AutoScraper / AXE / Co-Scraper / LLM XPath agents  — LLM 이 wrapper 생성자 자리를 차지

이 계보가 우리에게 주는 한 문장: 30년간 이 분야의 모든 진보는 "wrapper 를 누가 만드는가" 를 바꿨을 뿐, "실행은 결정론적 wrapper 가 한다" 는 전제는 한 번도 바뀌지 않았다. LLM 시대의 최신 논문들(§6)도 여전히 wrapper 를 만들어 실행한다.

5.2 RoadRunner — 페이지 2장 비교로 템플릿과 데이터 분리

VLDB 2001 원문(PDF 텍스트 추출로 확인)에서:

전제: "Pages in data-intensive sites are usually automatically generated: data are stored in a back-end DBMS, and HTML pages are produced using scripts i.e., programs from the content of the database."

형식화 — union-free regular expression (UFRE):

"Given a special symbol #PCDATA, and an alphabet of symbols Σ not containing #PCDATA, a union-free regular expression (UFRE) over Σ is a string over alphabet Σ {#PCDATA, ·, +, ?, (, )} defined as follows. First, the empty string, ϵ and all elements of Σ {#PCDATA} are union-free regular expressions. If a and b are UFRE, then a·b, (a)+, and (a)? are UFRE."

(a)* = ((a)+)? 는 축약. UFRE ↔ nested type 대응: #PCDATA → string 필드, + → 리스트(중첩 가능), ? → nullable 필드.

문제 정식화: HTML 문자열 s₁…s_k 가 nested type τ 의 인스턴스 i₁…i_k 의 인코딩이라면, L(σ) ⊇ {s₁…s_k} 인 최소 UFRE σ 를 찾으면 τ = type(σ) 이고, σ 를 wrapper 로 써서 원본 데이터를 복원할 수 있다. 따라서 문제는 두 UFRE 의 least upper bound 계산 으로 환원된다 → 알고리즘 match(σ₁, σ₂).

ACME 매칭 기법 (Align, Collapse under Mismatch, and Extract):

  1. HTML 을 XHTML 로 정규화(태그가 제대로 닫히고 중첩되도록). "several tools are available to turn an HTML page into an XHTML one."
  2. 어휘 분석기로 토큰 리스트(각 토큰은 HTML 태그 또는 문자열 값)로 변환. 논문 Figure 3 예시에서 두 HTML 샘플이 각각 20개, 27개 토큰으로 변환된다.
  3. page 1 을 초기 wrapper 로 삼고 page 2 를 sample 로 파싱한다.
  4. sample 의 토큰이 wrapper 문법에 맞지 않으면 mismatch 발생 → wrapper 를 일반화해 해소.
  5. 모든 mismatch 를 해소하면 공통 wrapper 완성.

mismatch 두 종류와 처리:

종류 발생 조건 의미 처리
String mismatch wrapper 와 sample 의 대응 위치에 다른 문자열 같은 클래스 페이지라면 DB 필드 값 차이일 수밖에 없다 그 위치를 #PCDATA 로 일반화 = 필드 발견
Tag mismatch 다른 태그끼리, 또는 태그 vs 문자열 iterator 또는 optional ① 먼저 반복 패턴(iterator) 탐색 → ② 실패하면 optional 로 처리

String mismatch 예시(원문): 토큰 4에서 'John Smith' vs 'Paul Jones' → wrapper(초기값 = page 1)의 'John Smith'#PCDATA 로 치환. 몇 단계 뒤 'Database Primer' vs 'XML at Work' 도 동일. 중요: 토큰 2의 'Books of:' 처럼 두 페이지에서 동일한 상수 문자열은 필드가 되지 않는다 — 생성 스크립트가 HTML 레이아웃의 일부로 넣은 것이다.

Tag mismatch → optional 처리 (원문): 토큰 6에서 wrapper 쪽 <UL> vs sample 쪽 <IMG.../>. 반복 패턴 탐색이 실패하면 optional 로 간주하고 cross-search 로 위치를 판별한다:

  • (a) optional 이 wrapper 쪽에 있다고 가정 → 이를 건너뛴 뒤 sample 의 이미지를 wrapper 의 후속 <IMG.../> 와 매칭할 수 있어야 한다.
  • (b) optional 이 sample 쪽에 있다고 가정 → wrapper 의 토큰 6을 sample 의 <UL> 등장 위치와 매칭할 수 있어야 한다.

예시에서는 wrapper 에 이미지가 없으므로 (b) 가 성립 → optional 은 sample 쪽. wrapper 를 (<IMG src=.../>)? 형태로 일반화하고 파싱 재개.

DMF_Crawler 로의 번역 — LLM 프롬프트 설계 규칙:

RoadRunner 를 그대로 구현할 필요는 없지만, LLM 에게 셀렉터를 만들게 할 때 반드시 RoadRunner 가 하는 일을 시켜야 한다:

  1. 목록 페이지를 최소 2장(가능하면 3장) 준다 — 1페이지, 2페이지, 그리고 검색 결과가 적은 페이지. 1장만 주면 상수와 변수를 구분할 수 없다.
  2. "두 페이지에서 동일하게 나타나는 텍스트는 레이블(상수)이고, 다른 텍스트는 데이터(필드)" 라고 명시적으로 지시한다.
  3. optional 필드를 반드시 물어본다 — "어떤 레코드에는 있고 어떤 레코드에는 없는 필드가 있는가?" (RoadRunner 의 tag mismatch → optional 발견에 대응). DMF 게시판에서는 "첨부파일", "비고", "변경 사유"가 전형적인 optional 이다.
  4. 반복 단위(iterator)를 명시적으로 물어본다 — "한 레코드에 해당하는 최소 반복 요소는 무엇인가?" 이것이 우리 파서의 row_selector 가 된다.

5.3 셀렉터 안정성 — ROBULA+ 와 anchor 기반 상대 XPath

ROBULA+ (Leotta et al., JSEP 2016) 의 정량 결과:

  • 절대 XPath 대비 취약성 평균 90% 감소
  • Selenium IDE 로케이터 대비 63% 감소
  • 현재 강건 XPath 자동 생성의 state of the art

"Automatic XPath generation agents" (Huang & Song, 2025) 의 anchor node 기법:

"Rather than absolute paths vulnerable to structural changes, the framework uses 'anchor nodes' (distinctive nearby elements) to generate relative XPaths with superior robustness."

두 기법의 공통 원리: 문서 루트에서 목표까지의 경로를 인코딩하지 말고, 절대 변하지 않는 가까운 무언가에서 목표까지의 짧은 경로를 인코딩하라.

5.4 셀렉터 작성 규칙 (확정)

우선순위가 높은 순서다. 위 규칙을 만족하는 셀렉터를 만들 수 있으면 아래 규칙으로 내려가지 않는다.

순위 규칙 예시 근거
1 id 속성 앵커 #boardList tr G3, ROBULA+
2 data-* / itemprop / ARIA role 앵커 [data-board="dmf"] tbody tr G3 — "리스타일을 살아남을 확률이 훨씬 높다"
3 테이블 헤더 텍스트 앵커 (열 위치를 텍스트로 찾기) th 중 텍스트가 등록번호 인 것의 인덱스를 구해 그 인덱스의 td 를 읽음 열 순서 변경에 면역. 정부 게시판에서 가장 흔한 변경이 열 추가/재배치다
4 의미 있는 구조 앵커 + 상대 경로 table.board-list >> tbody >> tr (깊이 3 이하) Huang & Song 2025 anchor node
5 레이블 텍스트 기준 형제 탐색 (상세 페이지) dt 텍스트가 주성분명 인 요소의 다음 dd Kushmerick LR wrapper 의 직계 후손
6 클래스 기반 (최후 수단, 반드시 fallback 과 함께) .tbl_list tr
절대 XPath /html/body/div[3]/div[2]/table/tbody/tr[2]/td[3] 금지. ROBULA+ 기준 취약성 최악
nth-child 위치 의존 tr:nth-child(4) td:nth-child(2) 금지 (헤더 텍스트 앵커로 대체)
깊이 5 초과 체인 div > div > div > div > div > table 금지. G3 — "래퍼 하나만 추가돼도 깨진다"

추가 규칙:

  • 모든 필드는 주 셀렉터 + 최소 1개의 fallback 셀렉터 를 갖는다. 주 셀렉터가 0건을 반환하면 fallback 을 시도하고, fallback 이 성공하면 "주 셀렉터 파손" 이벤트를 로그에 남긴다(§5.6 의 자동 복구 트리거).
  • 셀렉터는 코드에 하드코딩하지 않고 selectors/<board_id>.yaml버전과 검증 날짜를 붙여 저장한다(G2 의 "셀렉터 버저닝, 스키마 날짜 태깅").
# selectors/dmf_notice.yaml
board_id: dmf_notice
schema_version: 3
verified_at: "2026-09-02"
source_url_template: "https://nedrug.mfds.go.kr/.../list?page={page}"   # ⚠️ 실제 URL 은 실측 필요(부록 B)

row:
  primary: "#boardList tbody tr"
  fallbacks:
    - "table.tbl_list tbody tr"
    - "table tbody tr"
  min_expected: 10        # 목록 페이지당 최소 행 수. 이보다 적으면 파손 의심
  max_expected: 100

fields:
  reg_no:
    label_ko: "등록번호"
    strategy: header_text          # th 텍스트로 열 인덱스를 찾는다
    header_aliases: ["등록번호", "DMF번호", "원료의약품등록번호"]
    fallbacks:
      - {strategy: css, selector: "td.reg-no"}
    validate:
      pattern: '^[0-9]{4}-[0-9]+$'  # ⚠️ 실제 형식 실측 필요(부록 B)
      required: true
  ingredient_name:
    label_ko: "주성분명"
    strategy: header_text
    header_aliases: ["주성분명", "성분명", "원료명"]
    validate:
      min_length: 2
      required: true
  company:
    label_ko: "업체명"
    strategy: header_text
    header_aliases: ["업체명", "제조업체", "등록업체"]
    validate:
      required: true
  registered_on:
    label_ko: "등록일"
    strategy: header_text
    header_aliases: ["등록일", "등록일자", "공고일"]
    validate:
      date_formats: ["%Y-%m-%d", "%Y.%m.%d", "%Y/%m/%d"]
      required: true
  status:
    label_ko: "상태"
    strategy: header_text
    header_aliases: ["상태", "처리상태", "구분"]
    validate:
      enum: ["등록", "변경", "취하", "취소"]   # ⚠️ 실제 값 집합 실측 필요(부록 B)
      required: false
  detail_url:
    strategy: attr
    selector: "a"
    attr: "href"
    validate:
      required: true

5.5 웹 테이블을 신뢰하지 마라 (Cafarella et al. 2008)

WebTables 는 141억 개 HTML 테이블 중 1억 5,400만 개(약 1.1%) 만이 고품질 관계형 데이터임을 보였다. 나머지는 레이아웃용이다.

파싱 전 관계형성 판정 (table_guard.py 의 책임):

# src/dmf_crawler/table_guard.py
"""HTML 테이블이 '데이터 테이블'인지 '레이아웃 테이블'인지 판정한다.

근거: Cafarella et al., WebTables (PVLDB 2008) — 웹 테이블의 약 1.1%만이 관계형 데이터다.
"""
from __future__ import annotations

from dataclasses import dataclass
from statistics import pstdev

from bs4 import BeautifulSoup, Tag


@dataclass(frozen=True)
class TableVerdict:
    is_relational: bool
    reason: str
    n_rows: int
    n_cols: int


def _cells(row: Tag) -> list[Tag]:
    return row.find_all(["td", "th"], recursive=False)


def judge_table(table: Tag, *, min_rows: int = 3, min_cols: int = 2) -> TableVerdict:
    rows = table.find_all("tr")
    if len(rows) < min_rows:
        return TableVerdict(False, f"행이 {len(rows)}개로 부족(최소 {min_rows})", len(rows), 0)

    widths = [len(_cells(r)) for r in rows if _cells(r)]
    if not widths:
        return TableVerdict(False, "셀이 없음", len(rows), 0)

    modal_width = max(set(widths), key=widths.count)
    if modal_width < min_cols:
        return TableVerdict(False, f"열이 {modal_width}개로 부족(최소 {min_cols})", len(rows), modal_width)

    # 열 수 일관성: 최빈 폭을 갖는 행의 비율이 80% 이상이어야 한다.
    consistency = widths.count(modal_width) / len(widths)
    if consistency < 0.8:
        return TableVerdict(False, f"열 수 불일치(일관성 {consistency:.2f})", len(rows), modal_width)

    # 중첩 테이블은 레이아웃 테이블의 강한 신호다.
    if table.find("table") is not None:
        return TableVerdict(False, "중첩 테이블 포함(레이아웃 의심)", len(rows), modal_width)

    # 헤더 행 존재
    has_header = table.find("th") is not None or table.find("thead") is not None
    if not has_header:
        return TableVerdict(False, "헤더(th/thead) 없음", len(rows), modal_width)

    # 셀 길이 분산이 지나치게 크면 데이터 테이블이 아닐 가능성
    lengths = [len(c.get_text(strip=True)) for r in rows for c in _cells(r)]
    if lengths and pstdev(lengths) > 200:
        return TableVerdict(False, "셀 길이 분산 과다(본문 레이아웃 의심)", len(rows), modal_width)

    return TableVerdict(True, "관계형 테이블로 판정", len(rows), modal_width)


def pick_data_table(html: str) -> tuple[Tag | None, list[TableVerdict]]:
    """페이지 내 모든 테이블을 판정하고, 관계형으로 판정된 것 중 행이 가장 많은 것을 고른다."""
    soup = BeautifulSoup(html, "lxml")
    verdicts: list[TableVerdict] = []
    best: tuple[Tag, TableVerdict] | None = None
    for tbl in soup.find_all("table"):
        v = judge_table(tbl)
        verdicts.append(v)
        if v.is_relational and (best is None or v.n_rows > best[1].n_rows):
            best = (tbl, v)
    return (best[0] if best else None), verdicts

5.6 schema drift 자동 탐지 — RAPTURE 를 우리 문제에 이식

왜 STRAWMAN 이 아니라 RAPTURE 인가. Kushmerick(AAAI-99) 원문:

"The wrapper verification problem is to determine whether a wrapper is correct. Standard regression testing approaches are inappropriate, because both the formatting regularities and a site's underlying content may change."

STRAWMAN(단순 회귀 테스트)은 "같은 질의에 대해 과거 정상 페이지의 출력과 현재 출력을 비교"한다. 게시판은 내용이 매일 바뀌는 것이 정상이므로 STRAWMAN 은 매일 오탐을 낸다. RAPTURE 는 대신 출력의 통계적 특징 분포를 비교한다.

RAPTURE 의 방법 (검색 결과에서 확인된 서술): 각 데이터 필드를 전역 특징들의 집합(단어 수, 평균 단어 길이, 타입 밀도 등)으로 기술한다. 학습 예시들에 대해 각 특징 분포의 평균과 분산을 계산한다. 출력이 알려진 질의 집합에 대해 새 결과를 생성하고, 각 특징에 대해 관측값이 나올 확률을 계산한 뒤, 개별 특징 확률을 결합해 wrapper 가 올바르게 추출했을 전체 확률을 만든다.

Lerman/Minton/Knoblock (JAIR 2003) 의 실측 성능: 27개 wrapper 를 1년 추적, 37건의 파손 중 35건 탐지 — precision 0.73 / recall 0.95.

임계값 정책: recall 을 우선한다. 오탐(정상인데 경고)의 비용은 "사람이 5분 확인"이고, 미탐(파손인데 통과)의 비용은 "틀린 규제 데이터가 배포됨"이다. 비대칭이 극단적이므로 precision 0.73 수준을 감수하고 recall 0.95 를 목표로 임계값을 잡는다.

# src/dmf_crawler/drift.py
"""RAPTURE (Kushmerick, AAAI-99) 방식의 wrapper verification.

필드별 통계 특징의 과거 분포(평균/표준편차) 대비 오늘 관측값의 이탈도를 z-score 로 재고,
필드별 확률을 결합해 '추출이 올바를 전체 확률'을 만든다.
임계값 이하이면 결과를 채택하지 않고 사람에게 알린다.

설계 원칙(Lerman/Minton/Knoblock JAIR 2003 의 precision 0.73 / recall 0.95 결과에 근거):
  recall 우선. 오탐은 사람이 5분 확인하면 되지만, 미탐은 틀린 규제 데이터 배포로 이어진다.
"""
from __future__ import annotations

import json
import math
import re
from dataclasses import dataclass, asdict
from pathlib import Path
from statistics import mean, pstdev
from typing import Any, Iterable, Sequence

ACCEPT_THRESHOLD = 0.05   # 결합 확률이 이 값 미만이면 파손 의심
MIN_HISTORY = 7           # 기준선 계산에 필요한 최소 과거 실행 수
Z_CAP = 8.0               # z-score 상한 (수치 안정)

_NUM_RE = re.compile(r"\d")


@dataclass(frozen=True)
class FieldFeatures:
    """한 필드(열)에 대해 계산하는 RAPTURE 스타일 전역 특징."""
    n_values: int
    null_ratio: float
    mean_len: float
    stdev_len: float
    digit_density: float
    distinct_ratio: float
    pattern_match_ratio: float


def compute_features(values: Sequence[str | None], pattern: str | None = None) -> FieldFeatures:
    total = len(values)
    if total == 0:
        return FieldFeatures(0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0)

    non_null = [v for v in values if v is not None and v.strip() != ""]
    lens = [len(v) for v in non_null] or [0]
    all_chars = sum(len(v) for v in non_null)
    digits = sum(len(_NUM_RE.findall(v)) for v in non_null)

    if pattern:
        rx = re.compile(pattern)
        matched = sum(1 for v in non_null if rx.match(v))
        pattern_ratio = matched / len(non_null) if non_null else 0.0
    else:
        pattern_ratio = 1.0

    return FieldFeatures(
        n_values=total,
        null_ratio=1.0 - len(non_null) / total,
        mean_len=mean(lens),
        stdev_len=pstdev(lens) if len(lens) > 1 else 0.0,
        digit_density=(digits / all_chars) if all_chars else 0.0,
        distinct_ratio=len(set(non_null)) / len(non_null) if non_null else 0.0,
        pattern_match_ratio=pattern_ratio,
    )


def _gaussian_prob(x: float, mu: float, sigma: float) -> float:
    """관측값이 기준 분포에서 나올 '그럴듯함'. z-score 를 확률로 사상한다."""
    if sigma < 1e-6:
        sigma = max(abs(mu) * 0.05, 0.01)   # 과거 분산이 0이면 5% 허용폭을 준다
    z = min(abs(x - mu) / sigma, Z_CAP)
    return math.exp(-0.5 * z * z)


@dataclass
class DriftReport:
    field: str
    probability: float
    z_details: dict[str, float]
    suspect: bool


class DriftDetector:
    """과거 실행들의 특징을 baseline.json 에 누적하고, 오늘 결과를 검증한다."""

    FEATURE_KEYS = (
        "null_ratio", "mean_len", "stdev_len",
        "digit_density", "distinct_ratio", "pattern_match_ratio",
    )

    def __init__(self, baseline_path: Path):
        self.path = baseline_path
        self.history: dict[str, list[dict[str, Any]]] = {}
        if baseline_path.exists():
            self.history = json.loads(baseline_path.read_text(encoding="utf-8"))

    def verify(self, field: str, feats: FieldFeatures) -> DriftReport:
        past = self.history.get(field, [])
        if len(past) < MIN_HISTORY:
            return DriftReport(field, 1.0, {}, suspect=False)   # 기준선 부족 → 판정 보류

        probs: list[float] = []
        z_details: dict[str, float] = {}
        for key in self.FEATURE_KEYS:
            series = [p[key] for p in past]
            mu, sigma = mean(series), (pstdev(series) if len(series) > 1 else 0.0)
            x = getattr(feats, key)
            probs.append(_gaussian_prob(x, mu, sigma))
            eff_sigma = sigma if sigma >= 1e-6 else max(abs(mu) * 0.05, 0.01)
            z_details[key] = (x - mu) / eff_sigma

        # 개별 특징 확률의 기하평균으로 결합 (RAPTURE 의 '개별 확률 결합')
        combined = math.exp(sum(math.log(max(p, 1e-12)) for p in probs) / len(probs))
        return DriftReport(field, combined, z_details, suspect=combined < ACCEPT_THRESHOLD)

    def record(self, field: str, feats: FieldFeatures, *, keep: int = 90) -> None:
        self.history.setdefault(field, []).append(asdict(feats))
        self.history[field] = self.history[field][-keep:]

    def save(self) -> None:
        self.path.parent.mkdir(parents=True, exist_ok=True)
        self.path.write_text(json.dumps(self.history, ensure_ascii=False, indent=2), encoding="utf-8")


def verify_extraction(
    rows: list[dict[str, str | None]],
    field_patterns: dict[str, str | None],
    baseline_path: Path,
) -> tuple[bool, list[DriftReport]]:
    """오늘 추출 결과 전체를 검증한다. 반환: (채택 가능 여부, 필드별 리포트)"""
    det = DriftDetector(baseline_path)
    reports: list[DriftReport] = []
    for field, pattern in field_patterns.items():
        values = [r.get(field) for r in rows]
        feats = compute_features(values, pattern)
        rep = det.verify(field, feats)
        reports.append(rep)
        if not rep.suspect:            # 정상으로 판정된 실행만 기준선에 반영한다
            det.record(field, feats)
    det.save()
    accept = not any(r.suspect for r in reports)
    return accept, reports

추가 게이트 (통계 검증만으로는 부족한 부분):

게이트 조건 조치
G-1 행 수 하한 목록 페이지 행 수 < min_expected 즉시 실패. 리포트 미생성, 알림
G-2 행 수 급변 총 레코드 수가 전일 대비 ±30% 초과 변동 보류. 사람 확인 후 승인
G-3 필수 필드 null required: true 필드의 null 비율 > 5% 즉시 실패
G-4 패턴 불일치 pattern 매치율 < 95% 즉시 실패
G-5 전량 취하 "취하" 판정 레코드가 전체의 > 10% 보류 — 셀렉터 파손 시 전 레코드가 사라진 것처럼 보인다
G-6 fallback 발동 주 셀렉터가 0건이고 fallback 이 성공 경고 + 셀렉터 재생성 트리거. 결과는 채택
G-7 변경률 이상 정지 평소 λ 대비 30일간 변경 0건 경고 — 조용한 파손 의심(§4.4)
G-8 실행 시간 이상 평소 대비 3배 이상 소요 경고 — 사이트 개편 또는 네트워크 문제

G-5 는 이 프로젝트에서 가장 중요한 게이트다. 셀렉터가 깨지면 "0건 추출" → diff 엔진이 "전 레코드 취하"로 해석 → 리포트에 수천 건의 취하가 실린다. 이 실패 모드를 반드시 구조적으로 막아야 한다.


6. LLM 기반 추출 최신 연구(2023~2026)와 역할 분리 원칙

6.1 각 논문이 무엇을 주장하는가

논문 연도 한 문장 주장 정량 근거
Mind2Web (Deng et al.) 2023 실제 웹사이트의 raw HTML 은 LLM 입력 한계를 초과한다. 소형 LM 으로 먼저 필터링하면 LLM 의 효과성과 효율성이 크게 개선된다 137개 사이트 / 31개 도메인 / 2,000+ 태스크
WebVoyager (He et al.) 2024 멀티모달(스크린샷+텍스트) 에이전트가 실제 사이트를 end-to-end 로 조작할 수 있다 성공률 59.1%, GPT-4V 자동평가가 사람과 85.3% 일치
AutoScraper (Huang et al.) 2024 LLM 으로 스크레이퍼를 생성하라. wrapper 기반은 적응성이, language agent 는 재사용성이 부족하다 SWDE zero-shot 이 지도학습 상회, executability 지표 제안
AXE (Mansour et al.) 2026 DOM 은 읽을 텍스트가 아니라 가지치기할 트리다. pruning + 0.6B 소형 LLM + Grounded XPath Resolution 이면 충분하다 SWDE F1 88.1% zero-shot
Co-Scraper (Wang et al.) 2026 질의 인식 DOM pruning + 재사용 가능한 스크레이퍼 합성 F1 94.78%, 재사용 성공률 90.39%
Automatic XPath generation agents (Huang & Song) 2025 seed 2~3장에서 속성 추출 → perturbation testing 으로 노드 확정 → anchor 기반 상대 XPath 생성 F1 86.8392.46 (+14.84~23.86%p), LLM 호출 n_s + p·n_s + 1
The AI Committee (Vallabhaneni et al.) 2025 LLM 에이전트는 값을 환각·누락하고 페이지 의미를 오해하며 무효 정보를 탐지하지 못한다. 다중 에이전트 검증으로 보완하라 완전성 최대 78.7%, 정밀도 최대 100%
Prompt2DAG 2025 결정론적 템플릿이 생성형보다 낫다 Template 92.3% vs Hybrid 78.5% vs (생성형 기타 이하)
DELM 2025 LLM 추출 파이프라인에는 동시 실행·재시도·결정론적 캐싱이 필요하다 캐시 키 = 렌더된 프롬프트 + 스키마 + 모델 + 생성 파라미터
Hybrid Deterministic-LLM (PDF) 2026 정규식+LLM 하이브리드가 LLM-only 보다 효율적이며, 결정론적 메타데이터에서 특히 그렇다 LLM-only vs Hybrid vs Camelot+LLM fallback 비교

6.2 이 논문들이 한목소리로 말하는 것

각 논문이 서로 다른 각도에서 독립적으로 같은 결론에 도달한다:

  1. LLM 에 전체 HTML 을 넣으면 안 된다. — Mind2Web(소형 LM 필터), AXE(DOM pruning), Co-Scraper(query-aware pruning). 세 논문이 각각 다른 방법으로 같은 문제를 해결했다.
  2. LLM 의 산출물은 데이터가 아니라 "추출기(extractor)" 여야 한다. — AutoScraper(스크레이퍼 생성), AXE(GXR 로 XPath 접지), Co-Scraper(재사용 가능 스크레이퍼), Huang & Song(XPath 생성). 네 논문 전부.
  3. LLM 이 매 페이지를 읽는 방식은 비용이 감당 안 된다. — Huang & Song 의 호출 수 비교(n_s + p·n_s + 1 vs n)가 이를 정량화했다. 우리 게시판이 하루 100건이면 LLM 직접 추출은 하루 100회 호출, 셀렉터 생성 방식은 셀렉터가 깨질 때만 호출.
  4. LLM 출력은 신뢰할 수 없다. — AI Committee 의 "완전성 최대 78.7%", WebVoyager 의 "성공률 59.1%". 규제 데이터에 21%~41% 실패율은 절대 허용 불가.
  5. 결정론이 이긴다. — Prompt2DAG 의 92.3% vs 78.5%.

6.3 결정론적 파서와 LLM 의 역할 분리 원칙 (확정)

┌───────────────────────────────────────────────────────────────────────────┐
│                        DMF_Crawler 실행 파이프라인                          │
└───────────────────────────────────────────────────────────────────────────┘

 [매일 06:00 — 결정론 경로. LLM 호출 0회가 정상]

   robots.txt 확인 (RFC 9309)
        ↓
   HTTP fetch (politeness / 재시도 / 조건부 요청)
        ↓
   원본 HTML 보존  raw/YYYY-MM-DD/*.html.gz          ← Mercator 의 RIS
        ↓
   table_guard  (관계형 테이블 판정)                   ← WebTables 2008
        ↓
   결정론적 파서 (selectors/*.yaml 의 셀렉터 실행)      ← RoadRunner / ROBULA+ 계보
        ↓
   drift 검증 (RAPTURE) + 게이트 G-1~G-8              ← Kushmerick 1999 / Lerman 2003
        ↓                                   ↘ 실패 시
   정규화 → diff 엔진 (키 upsert + SimHash)     [셀렉터 복구 경로]로 분기
        ↓                                        ↙
   이벤트 로그 + 스냅샷 저장                   ← Fowler / Kimball SCD2
        ↓
   xlsx 리포트 생성
        ↓
   (선택) LLM 요약  ← 여기서만 LLM 이 등장. 실패해도 리포트는 나간다

 [셀렉터 복구 경로 — 파손 시에만 실행. LLM 호출 3~10회]

   원본 HTML 로드 → DOM pruning (결정론)              ← AXE / Mind2Web
        ↓
   seed 페이지 2~3장 + 목표 스키마를 LLM 에 제시       ← Huang & Song 2025 / RoadRunner
        ↓
   LLM 이 셀렉터 후보 N개 생성  (agy -p, JSON 강제 출력)
        ↓
   ★ 결정론적 검증 — 후보를 실제로 실행해 게이트 G-1~G-8 통과 여부 확인
        ↓
   perturbation test (노드 텍스트 변조 → 출력 변화 확인)  ← Huang & Song 2025
        ↓
   통과한 후보만 selectors/*.yaml 에 새 schema_version 으로 기록
        ↓
   Windows 알림: "셀렉터가 자동 복구되었습니다. 확인 바랍니다."  ← 사람 승인 필요

금지 목록 (하드 룰):

# 금지 사항 이유
1 LLM 이 최종 데이터 값을 생성하는 것 AI Committee: 환각·누락, 완전성 78.7%
2 LLM 출력을 검증 없이 채택하는 것 위 + Prompt2DAG 92.3% vs 78.5%
3 브라우저 자동화 에이전트로 일상 수집 WebVoyager 성공률 59.1%
4 HTML 전체를 프롬프트에 넣는 것 Mind2Web / AXE / Co-Scraper 공통 결론 + 비용
5 형식이 고정된 필드(번호·날짜)를 LLM 에 맡기는 것 Hybrid Deterministic-LLM: 결정론적 메타데이터에서 정규식 우위
6 LLM 이 만든 셀렉터를 사람 확인 없이 영구 채택 자동 복구는 "제안"이고 승인은 사람이 한다
7 매 실행마다 LLM 을 호출하는 것 정상 실행의 LLM 호출 수는 0회 여야 한다(요약 기능 제외)

허용 목록 (LLM 이 실제로 잘하는 것):

# 허용 작업 근거 실패 시
1 셀렉터 후보 생성 (seed 2~3장, 스키마 제시) AutoScraper / Huang & Song 이전 셀렉터 유지 + 알림
2 헤더 별칭 확장 제안 (등록번호DMF번호, 원료의약품등록번호) 도메인 언어 이해 기존 별칭 유지
3 파손 원인 진단 문장 생성 ("class 명이 tbl_listboard-table 로 변경된 것으로 보임") 사람의 디버깅 시간 절약 로그만 남김
4 변경 사유 자유서술 필드의 한국어 요약 값 자체가 아니라 요약 원문 그대로 표기
5 일일 리포트의 "오늘의 핵심" 3줄 요약 사람이 읽을 산문 요약 생략, 표만 배포

4, 5 의 안전장치: LLM 요약은 원문과 병기한다. 요약만 싣고 원문을 버리면 환각이 검증 불가능해진다(AXE 의 Grounded XPath Resolution 정신 — 모든 산출물은 소스로 역추적 가능해야 한다).

6.4 LLM 호출의 결정론적 캐싱 (DELM 2025)

# src/dmf_crawler/llm_cache.py
"""LLM 호출 캐시. 캐시 키 = 렌더된 프롬프트 + 스키마 + 모델 ID + 생성 파라미터.

근거: DELM (arXiv:2509.20617) — "deterministic caching keyed to the fully rendered
prompt, schema, model, and generation parameters".
같은 입력에 두 번 과금하지 않고, 리포트 재현성도 확보한다.
"""
from __future__ import annotations

import hashlib
import json
import subprocess
from pathlib import Path
from typing import Any

CACHE_DIR = Path("var/llm_cache")


def cache_key(prompt: str, schema: dict[str, Any], model: str, params: dict[str, Any]) -> str:
    payload = json.dumps(
        {"prompt": prompt, "schema": schema, "model": model, "params": params},
        ensure_ascii=False, sort_keys=True,
    )
    return hashlib.sha256(payload.encode("utf-8")).hexdigest()


def call_agy(
    prompt: str,
    schema: dict[str, Any],
    *,
    model: str = "default",
    params: dict[str, Any] | None = None,
    timeout_s: int = 180,
) -> dict[str, Any]:
    """Google Antigravity CLI (agy) 를 headless 로 호출하고 JSON 을 강제 파싱한다.

    ⚠️ agy 의 정확한 플래그는 별도 문서(에이전트 CLI 축)에서 확정한다.
       여기서는 `agy -p <prompt>` 가 stdout 으로 응답을 낸다고 가정한다.
    """
    params = params or {}
    key = cache_key(prompt, schema, model, params)
    CACHE_DIR.mkdir(parents=True, exist_ok=True)
    cached = CACHE_DIR / f"{key}.json"
    if cached.exists():
        return json.loads(cached.read_text(encoding="utf-8"))

    full_prompt = (
        f"{prompt}\n\n"
        f"반드시 아래 JSON Schema 를 만족하는 JSON 객체 하나만 출력하라. "
        f"코드 펜스나 설명 문장을 붙이지 마라.\n"
        f"{json.dumps(schema, ensure_ascii=False)}"
    )
    proc = subprocess.run(
        ["agy", "-p", full_prompt],
        capture_output=True, text=True, encoding="utf-8", timeout=timeout_s,
    )
    if proc.returncode != 0:
        raise RuntimeError(f"agy exited {proc.returncode}: {proc.stderr[:500]}")

    text = proc.stdout.strip()
    start, end = text.find("{"), text.rfind("}")
    if start < 0 or end <= start:
        raise ValueError(f"LLM 응답에서 JSON 객체를 찾지 못함: {text[:300]}")
    result = json.loads(text[start:end + 1])

    cached.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
    return result

6.5 perturbation testing — LLM 이 지목한 노드가 진짜인지 결정론적으로 확인

Huang & Song(2025)의 2단계를 그대로 이식한다.

# src/dmf_crawler/perturb.py
"""LLM 이 제안한 셀렉터가 '진짜 그 필드'를 가리키는지 결정론적으로 검증한다.

근거: Huang & Song (2025), "Automatic XPath generation agents for vertical websites by LLMs".
  - 후보 DOM 노드의 텍스트를 변조(perturb)했을 때 추출 결과가 바뀌면, 그 노드가 정답이다.
LLM 없이 순수 결정론으로 수행하므로 비용이 0이고 재현 가능하다.
"""
from __future__ import annotations

import copy
from dataclasses import dataclass

from bs4 import BeautifulSoup

SENTINEL = "☃PERTURBED☃"   # 원문에 절대 없을 문자열


@dataclass(frozen=True)
class PerturbResult:
    selector: str
    responded: bool
    n_before: int
    n_after: int
    note: str


def verify_selector_by_perturbation(html: str, selector: str, expected_value: str) -> PerturbResult:
    """selector 가 expected_value 를 담은 노드를 가리키는지 변조 실험으로 확인한다."""
    soup = BeautifulSoup(html, "lxml")
    before = [n.get_text(strip=True) for n in soup.select(selector)]
    if expected_value not in before:
        return PerturbResult(selector, False, len(before), len(before),
                             "셀렉터 결과에 기대값이 없음")

    # 기대값을 담은 노드만 변조한다.
    soup2 = BeautifulSoup(html, "lxml")
    perturbed = 0
    for node in soup2.select(selector):
        if node.get_text(strip=True) == expected_value:
            node.string = SENTINEL
            perturbed += 1
    if perturbed == 0:
        return PerturbResult(selector, False, len(before), len(before), "변조 대상 노드 없음")

    after = [n.get_text(strip=True) for n in soup2.select(selector)]
    responded = SENTINEL in after and expected_value not in after
    return PerturbResult(
        selector, responded, len(before), len(after),
        "변조가 결과에 반영됨" if responded else "변조가 결과에 반영되지 않음(다른 노드를 보고 있음)",
    )


def screen_candidates(html: str, candidates: list[str], expected_value: str) -> list[PerturbResult]:
    """LLM 이 낸 셀렉터 후보들을 전부 검증해, 응답한 것만 남긴다."""
    results = [verify_selector_by_perturbation(html, c, expected_value) for c in candidates]
    return [r for r in results if r.responded]

7. 중복·변경 탐지 알고리즘 — SimHash/MinHash/키 기반 upsert/스냅샷 vs 이벤트 로그

7.1 SimHash (Charikar 2002 / Manku·Jain·Das Sarma 2007)

알고리즘 (Charikar 2002, STOC):

  1. 입력을 특징(feature)들로 분해한다.
  2. 각 특징을 해시해 f-bit 값을 얻는다.
  3. f 개의 카운터를 두고, 각 특징 해시의 비트가 1이면 해당 카운터에 +가중치, 0이면 −가중치.
  4. 최종 지문의 각 비트 = 해당 카운터가 양수면 1, 아니면 0.
  5. 유사한 입력 → Hamming 거리가 작은 지문.

Manku·Jain·Das Sarma (WWW 2007) 의 파라미터 검증:

"We experimentally validate that for a repository of 8B webpages, 64-bit simhash fingerprints and k=3 are reasonable."

크기 이점 (원문): Broder 의 shingle 기반 지문은 지문당 24바이트(6개 Rabin 지문 중 2개 이상 일치 여부를 확인하는 방식). simhash 는 80억 페이지에 대해 64비트 = 8바이트로 충분.

simhash 의 상충하는 두 성질 (원문):

(A) The fingerprint of a document is a "hash" of its features, and (B) Similar documents have similar hash values. The latter property is atypical of hash-functions.

Hamming Distance Problem 과 해법 규모 (원문):

  • 정의: f-bit 지문 컬렉션에서 질의 지문 F 와 최대 k 비트 다른 지문을 찾기.
  • 구체적 규모: 80억 개 64-bit 지문 = 64GB. 온라인 질의는 수 밀리초 내, 배치는 100만 질의를 약 100초(하루 10억 질의 처리량).
  • 소박한 해법 1(정렬 테이블에 모든 F' 프로브): 64-bit·k=3 이면 C(64,3) = 41,664 회 프로브 필요 → 비현실적.
  • 소박한 해법 2(모든 F' 사전 계산): 지문 수의 최대 41,664배 → 비현실적.
  • 실용 해법: 순열 + 블록 분할. 예시(f=64, k=3, 80억 = 2³⁴ 지문 → d=34):
설계 블록 분할 테이블 수 pᵢ 프로브당 평균 회수 지문 수
20 tables 11,11,11,11,10,10 (6블록 중 3개 선택) C(6,3)=20 31/32/33 2^(3431) = 8
16 tables 16,16,16,16 중 1개 선택 + 나머지 48비트를 12×4 중 1개 선택 4×4=16 28 2^(3428) = 64
10 tables 13,13,13,13,12 (5블록 중 2개 선택) C(5,2)=10
  • 압축(원문 3.2): 정렬된 연속 지문은 상위 d 비트를 기대적으로 공유한다. 두 연속 지문의 XOR 에서 최상위 1비트 위치 h(0…f1)의 분포로 Huffman 코드를 만들고, 블록 크기 B(전형값 1024 바이트) 단위로: 블록 첫 지문은 통째로 저장(8f 비트), 이후는 XOR 최상위 1비트 위치의 Huffman 코드 + 그 오른쪽 비트들만 저장. 블록 키는 그 블록의 마지막 지문이며 interpolation search 로 찾는다. 결과: 테이블 크기 약 절반.

DMF_Crawler 에서의 규모: 우리 지문 수는 수천 개다. 위의 순열/블록/압축 기법은 전부 불필요하다. 전수 XOR 비교가 마이크로초 안에 끝난다. 우리가 가져오는 것은 f=64, k=3 이라는 검증된 파라미터뿐이다.

# src/dmf_crawler/simhash.py
"""64-bit SimHash + Hamming 거리 근접 중복 판정.

근거:
  - Charikar (STOC 2002): 특징 해시의 비트별 가중 합 부호로 지문 생성.
  - Manku, Jain, Das Sarma (WWW 2007): 80억 웹페이지에 대해 64-bit 지문과 k=3 이 합리적임을 실험 검증.
  - scrapinghub/python-simhash (BSD-3-Clause, 아카이브됨) 의 API 를 참고해 순수 파이썬으로 재구현.

규모 주의: 우리 지문 수는 수천 개이므로 WWW 2007 의 순열/블록 분할/Huffman 압축은 불필요하다.
전수 비교로 충분하다.
"""
from __future__ import annotations

import hashlib
import re
import unicodedata
from typing import Iterable, Iterator

F_BITS = 64
HAMMING_THRESHOLD = 3          # Manku et al. 2007 검증값
SHINGLE_SIZE = 3               # 한국어 텍스트에는 문자 3-gram 이 잘 맞는다

_WS_RE = re.compile(r"\s+")


def normalize_text(text: str) -> str:
    """비교 전 정규화. 표기 흔들림(공백/전각/유니코드 합자)을 제거한다."""
    t = unicodedata.normalize("NFKC", text)
    t = t.replace(" ", " ").replace("", "")
    t = _WS_RE.sub(" ", t)
    return t.strip()


def shingles(text: str, n: int = SHINGLE_SIZE) -> Iterator[str]:
    """문자 n-gram. Broder(1997)의 shingling 을 문자 단위로 적용."""
    t = normalize_text(text)
    if len(t) <= n:
        if t:
            yield t
        return
    for i in range(len(t) - n + 1):
        yield t[i:i + n]


def _feature_hash(feature: str) -> int:
    return int.from_bytes(hashlib.blake2b(feature.encode("utf-8"), digest_size=8).digest(), "big")


def simhash(features: Iterable[str], *, bits: int = F_BITS) -> int:
    counters = [0] * bits
    empty = True
    for feat in features:
        empty = False
        h = _feature_hash(feat)
        for i in range(bits):
            counters[i] += 1 if (h >> i) & 1 else -1
    if empty:
        return 0
    out = 0
    for i in range(bits):
        if counters[i] > 0:
            out |= (1 << i)
    return out


def simhash_text(text: str) -> int:
    return simhash(shingles(text))


def hamming_distance(a: int, b: int) -> int:
    return (a ^ b).bit_count()          # Python 3.10+


def is_near_duplicate(a: int, b: int, *, k: int = HAMMING_THRESHOLD) -> bool:
    return hamming_distance(a, b) <= k


def simpair_indices(fingerprints: list[int], *, k: int = HAMMING_THRESHOLD) -> list[tuple[int, int]]:
    """임계 내 유사 쌍의 인덱스를 반환. O(n²) — n 이 수천 규모일 때만 사용."""
    pairs: list[tuple[int, int]] = []
    for i in range(len(fingerprints)):
        for j in range(i + 1, len(fingerprints)):
            if hamming_distance(fingerprints[i], fingerprints[j]) <= k:
                pairs.append((i, j))
    return pairs


if __name__ == "__main__":
    a = simhash_text("아세트아미노펜 원료의약품 등록 변경 공고 2026년 9월")
    b = simhash_text("아세트아미노펜  원료의약품 등록 변경 공고 2026년 9월 ")   # 공백만 다름
    c = simhash_text("이부프로펜 원료의약품 신규 등록 공고 2026년 9월")
    print("a~b:", hamming_distance(a, b), is_near_duplicate(a, b))   # 0, True
    print("a~c:", hamming_distance(a, c), is_near_duplicate(a, c))   # 큰 값, False

7.2 MinHash (Broder 1997) — 언제 SimHash 대신 쓰는가

핵심 성질: P(h_min(A) = h_min(B)) = J(A,B) (Jaccard 유사도). 교집합·합집합을 명시적으로 계산하지 않고 해시 기반 표본추출로 유사도를 근사한다. min-wise independent permutation 이 필요하나 진짜 무작위 순열은 저장 비용이 과해, restricted / approximate min-wise independence 로 근사한다. AltaVista 에서 중복 웹페이지 제거에 실사용. LSH 스킴이므로 banding 으로 근접 이웃 검색·군집화가 가능하다.

선택 기준 (확정):

상황 선택 이유
게시판 레코드 한 줄(수십~수백 자) SimHash 지문 8바이트, 짧은 텍스트에 충분, 계산 빠름
상세 페이지 본문(수천 자) SimHash 동일
첨부 파일(PDF/HWP) 본문 대량 비교 MinHash + LSH 집합 유사도가 필요하고, banding 으로 후보를 좁힐 수 있음
첨부 파일이 정확히 같은지만 SHA-256 근사가 필요 없다

현 단계에서는 SimHash 와 SHA-256 만 구현하고, MinHash 는 첨부파일 대량 비교 요구가 실제로 생기면 추가한다.

7.3 키 기반 upsert — diff 엔진의 뼈대

중복 판정 키(식별키) 결정 규칙 — 우선순위 순:

순위 조건
1 DMF 등록번호(또는 게시판 고유 게시물 ID) 존재하고 안정적이면 이것만 쓴다
2 상세 페이지 URL 의 안정 파라미터 (seq, id 등) 세션 ID·페이지 번호는 제외하고 정규화
3 sha1(주성분명 ‖ 업체명 ‖ 등록일) 정규화 후 합성키 1·2가 없을 때만. 취약 — 업체명 표기가 바뀌면 신규+취하로 오판

⚠️ DMF 등록번호의 실제 존재 여부와 형식은 실측 필요(부록 B). 1순위 키가 없으면 3순위 합성키의 취약성을 문서화하고 리포트에 경고를 표시해야 한다.

변경 판정 규칙 (필드별 정책):

필드 종류 비교 방법 변경으로 볼 것인가
식별키 완전 일치 다르면 다른 레코드
코드/번호/날짜 정규화 후 완전 일치 다르면 변경
업체명·성분명 NFKC 정규화 + 공백 축약 + 괄호 내 주석 제거 후 완전 일치 다르면 변경
상태 문자열 enum 매핑 후 비교 다르면 변경(취하 판정의 근거)
자유서술(변경 사유·비고) SimHash Hamming ≤ 3 이면 동일, 초과면 변경 표기 흔들림을 변경으로 오판하지 않기 위함
첨부파일 목록 파일명 집합 + SHA-256 추가/삭제/내용변경을 구분

이벤트 분류:

이벤트 조건
NEW 오늘 스냅샷에 있고 어제 스냅샷에 없음
MODIFIED 양쪽에 있고, 비교 대상 필드 중 하나 이상이 위 규칙상 다름
WITHDRAWN 어제 스냅샷에 있고 오늘 없음 또는 상태 필드가 취하/취소로 변경
REAPPEARED 과거에 WITHDRAWN 이었던 키가 다시 등장 (데이터 오류 또는 재등록 — 반드시 별도 표시)
UNCHANGED 위 어디에도 해당 없음
# src/dmf_crawler/diff.py
"""레코드 단위 diff 엔진 — 키 기반 upsert + 필드별 변경 판정.

설계 근거:
  - 김원중·조이기·손철수(2002): 전체 페이지 diff 가 아니라 '의미 있는 단위'로 구조화해 비교.
  - Manku et al. (WWW 2007): 자유서술 필드는 64-bit SimHash, Hamming k=3 으로 근접 동일 판정.
  - Kimball SCD Type 2 / Fowler Event Sourcing: 스냅샷과 이벤트 로그를 함께 유지.

★ 안전장치: WITHDRAWN 이 전체의 WITHDRAWN_ALARM_RATIO 를 넘으면 셀렉터 파손으로 간주하고
  diff 결과를 채택하지 않는다(§5.6 게이트 G-5). 이 프로젝트에서 가장 위험한 실패 모드다.
"""
from __future__ import annotations

import re
import unicodedata
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Mapping, Sequence

from .simhash import hamming_distance, simhash_text

WITHDRAWN_ALARM_RATIO = 0.10     # 게이트 G-5
FREETEXT_HAMMING_K = 3           # 게이트: 자유서술 필드 근접 동일 임계
_PAREN_RE = re.compile(r"[\(][^\)]*[\)]")
_WS_RE = re.compile(r"\s+")


class EventType(str, Enum):
    NEW = "NEW"
    MODIFIED = "MODIFIED"
    WITHDRAWN = "WITHDRAWN"
    REAPPEARED = "REAPPEARED"
    UNCHANGED = "UNCHANGED"


class FieldKind(str, Enum):
    KEY = "key"
    EXACT = "exact"          # 코드/번호/날짜
    NAME = "name"            # 업체명/성분명
    ENUM = "enum"            # 상태
    FREETEXT = "freetext"    # 변경 사유/비고


@dataclass(frozen=True)
class FieldChange:
    field: str
    before: str | None
    after: str | None
    kind: FieldKind


@dataclass
class DiffEvent:
    event: EventType
    key: str
    changes: list[FieldChange] = field(default_factory=list)
    record: Mapping[str, Any] = field(default_factory=dict)


@dataclass
class DiffResult:
    events: list[DiffEvent]
    accepted: bool
    reason: str

    def counts(self) -> dict[str, int]:
        out = {e.value: 0 for e in EventType}
        for ev in self.events:
            out[ev.event.value] += 1
        return out


def norm_exact(v: str | None) -> str:
    if v is None:
        return ""
    return _WS_RE.sub("", unicodedata.normalize("NFKC", v)).strip()


def norm_name(v: str | None) -> str:
    if v is None:
        return ""
    t = unicodedata.normalize("NFKC", v)
    t = _PAREN_RE.sub("", t)                       # 괄호 주석 제거: "가나제약(주)" -> "가나제약"
    t = t.replace("주식회사", "").replace("(주)", "")
    return _WS_RE.sub("", t).strip()


def values_differ(kind: FieldKind, before: str | None, after: str | None) -> bool:
    if kind in (FieldKind.KEY, FieldKind.EXACT, FieldKind.ENUM):
        return norm_exact(before) != norm_exact(after)
    if kind is FieldKind.NAME:
        return norm_name(before) != norm_name(after)
    if kind is FieldKind.FREETEXT:
        b, a = (before or "").strip(), (after or "").strip()
        if b == a:
            return False
        if not b or not a:
            return True
        return hamming_distance(simhash_text(b), simhash_text(a)) > FREETEXT_HAMMING_K
    raise ValueError(f"unknown field kind: {kind}")


def diff_snapshots(
    previous: Sequence[Mapping[str, Any]],
    current: Sequence[Mapping[str, Any]],
    *,
    key_field: str,
    field_kinds: Mapping[str, FieldKind],
    withdrawn_status_values: frozenset[str] = frozenset({"취하", "취소", "말소"}),
    previously_withdrawn_keys: frozenset[str] = frozenset(),
    status_field: str = "status",
) -> DiffResult:
    prev_by_key = {str(r[key_field]): r for r in previous}
    curr_by_key = {str(r[key_field]): r for r in current}

    events: list[DiffEvent] = []

    for key, rec in curr_by_key.items():
        if key not in prev_by_key:
            ev = EventType.REAPPEARED if key in previously_withdrawn_keys else EventType.NEW
            events.append(DiffEvent(ev, key, [], rec))
            continue

        old = prev_by_key[key]
        # 상태 필드가 취하로 바뀌었으면 MODIFIED 가 아니라 WITHDRAWN 으로 승격한다.
        if norm_exact(rec.get(status_field)) in withdrawn_status_values \
                and norm_exact(old.get(status_field)) not in withdrawn_status_values:
            events.append(DiffEvent(EventType.WITHDRAWN, key, [
                FieldChange(status_field, old.get(status_field), rec.get(status_field), FieldKind.ENUM)
            ], rec))
            continue

        changes = [
            FieldChange(f, old.get(f), rec.get(f), kind)
            for f, kind in field_kinds.items()
            if kind is not FieldKind.KEY and values_differ(kind, old.get(f), rec.get(f))
        ]
        events.append(DiffEvent(EventType.MODIFIED if changes else EventType.UNCHANGED, key, changes, rec))

    for key, rec in prev_by_key.items():
        if key not in curr_by_key:
            events.append(DiffEvent(EventType.WITHDRAWN, key, [], rec))

    # ★ 게이트 G-5
    n_prev = len(prev_by_key)
    n_withdrawn = sum(1 for e in events if e.event is EventType.WITHDRAWN)
    if n_prev > 0 and n_withdrawn / n_prev > WITHDRAWN_ALARM_RATIO:
        return DiffResult(
            events, accepted=False,
            reason=(f"취하 판정 {n_withdrawn}건 / 이전 {n_prev}건 = "
                    f"{n_withdrawn / n_prev:.1%} > {WITHDRAWN_ALARM_RATIO:.0%} — "
                    f"셀렉터 파손 의심. 결과를 채택하지 않는다."),
        )
    return DiffResult(events, accepted=True, reason="정상")

7.4 스냅샷 vs 이벤트 로그 — 둘 다 쓴다

Fowler (2005) 의 정의와 트레이드오프:

  • Event Sourcing = "애플리케이션 상태의 모든 변경을 이벤트의 시퀀스로 포착"
  • 지속되는 것 두 가지: 이벤트 로그애플리케이션 상태. 상태는 이벤트에서 유도 가능하므로 스냅샷은 성능 최적화다.
  • 기능: complete rebuild, event replay(잘못된 이벤트를 되돌리고 수정 후 재처리), temporal query(특정 시점까지 재생해 그 시점 상태 확인)
  • 트레이드오프: 외부 시스템 상호작용·코드 변경·이벤트 되돌리기에서 복잡도 증가. "자연스러운 기본 선택지가 아니며, 감사·디버깅·확장성에서 의미 있는 이득을 기대할 때만 채택하라."

Kimball SCD Type 2: 변경 시 기존 행 갱신이 아니라 새 행 추가. 필수 요소 = 대리키(surrogate key) PK + Effective Date / Expiration Date / Current Row Indicator.

CDC vs Event Sourcing (Capital One): CDC 는 "도메인 의도 없는 행 델타", Event Sourcing 은 "도메인 이벤트". → 우리는 도메인 이벤트(신규 등록 / 변경 / 취하)를 쓴다.

DMF_Crawler 저장 스키마 (확정):

-- var/dmf.sqlite3
PRAGMA journal_mode = WAL;

-- ① 원본 스냅샷 인덱스 (Mercator RIS / Heritrix WARC 에 대응)
CREATE TABLE IF NOT EXISTS crawl_run (
    run_id          INTEGER PRIMARY KEY AUTOINCREMENT,
    board_id        TEXT    NOT NULL,
    started_at      TEXT    NOT NULL,          -- ISO8601, KST
    finished_at     TEXT,
    status          TEXT    NOT NULL,          -- RUNNING | OK | REJECTED | FAILED
    reject_reason   TEXT,
    n_rows          INTEGER,
    duration_ms     INTEGER,
    selector_version INTEGER NOT NULL,
    raw_dir         TEXT    NOT NULL           -- raw/YYYY-MM-DD/<board_id>/
);

-- ② 현재 상태 + 이력 (Kimball SCD Type 2)
CREATE TABLE IF NOT EXISTS dmf_record (
    sk              INTEGER PRIMARY KEY AUTOINCREMENT,   -- 대리키
    board_id        TEXT    NOT NULL,
    natural_key     TEXT    NOT NULL,                    -- DMF 등록번호 등
    reg_no          TEXT,
    ingredient_name TEXT,
    company         TEXT,
    registered_on   TEXT,
    status          TEXT,
    detail_url      TEXT,
    payload_json    TEXT    NOT NULL,                    -- 전 필드 원문 보존
    body_simhash    INTEGER,                             -- 상세 본문 64-bit SimHash
    valid_from      TEXT    NOT NULL,                    -- Effective Date
    valid_to        TEXT,                                -- Expiration Date (NULL = 현재)
    is_current      INTEGER NOT NULL DEFAULT 1,          -- Current Row Indicator
    first_seen_run  INTEGER NOT NULL REFERENCES crawl_run(run_id),
    last_seen_run   INTEGER NOT NULL REFERENCES crawl_run(run_id)
);
CREATE INDEX IF NOT EXISTS ix_record_current ON dmf_record(board_id, natural_key, is_current);
CREATE INDEX IF NOT EXISTS ix_record_key     ON dmf_record(board_id, natural_key);

-- ③ 도메인 이벤트 로그 (Fowler Event Sourcing) — append only, 절대 UPDATE/DELETE 금지
CREATE TABLE IF NOT EXISTS dmf_event (
    event_id        INTEGER PRIMARY KEY AUTOINCREMENT,
    run_id          INTEGER NOT NULL REFERENCES crawl_run(run_id),
    occurred_at     TEXT    NOT NULL,
    board_id        TEXT    NOT NULL,
    natural_key     TEXT    NOT NULL,
    event_type      TEXT    NOT NULL,   -- NEW | MODIFIED | WITHDRAWN | REAPPEARED
    changes_json    TEXT,               -- [{field, before, after, kind}, ...]
    record_json     TEXT    NOT NULL
);
CREATE INDEX IF NOT EXISTS ix_event_run  ON dmf_event(run_id);
CREATE INDEX IF NOT EXISTS ix_event_key  ON dmf_event(board_id, natural_key, occurred_at);
CREATE INDEX IF NOT EXISTS ix_event_type ON dmf_event(event_type, occurred_at);

-- ④ 변경률 관측 (§4.4 λ 추정용)
CREATE TABLE IF NOT EXISTS change_observation (
    board_id        TEXT NOT NULL,
    observed_on     TEXT NOT NULL,      -- YYYY-MM-DD
    changed         INTEGER NOT NULL,   -- 0/1 : 이 실행에서 변경이 있었는가
    n_new           INTEGER NOT NULL,
    n_modified      INTEGER NOT NULL,
    n_withdrawn     INTEGER NOT NULL,
    PRIMARY KEY (board_id, observed_on)
);

-- ⑤ 셀렉터 검증 기준선 (§5.6 drift.py 의 baseline 을 DB 로 옮길 경우)
CREATE TABLE IF NOT EXISTS drift_baseline (
    board_id        TEXT NOT NULL,
    field           TEXT NOT NULL,
    observed_on     TEXT NOT NULL,
    features_json   TEXT NOT NULL,
    PRIMARY KEY (board_id, field, observed_on)
);

temporal query 예시 — Fowler 가 말한 "특정 시점 상태 조회"가 SQL 한 줄이 된다:

-- 2026-06-01 시점의 전체 레코드 상태
SELECT natural_key, ingredient_name, company, status
FROM   dmf_record
WHERE  board_id = 'dmf_notice'
  AND  valid_from <= '2026-06-01'
  AND  (valid_to IS NULL OR valid_to > '2026-06-01');

-- 특정 원료의 전체 변경 이력
SELECT occurred_at, event_type, changes_json
FROM   dmf_event
WHERE  board_id = 'dmf_notice' AND natural_key = ?
ORDER  BY occurred_at;

xlsx 리포트의 탭 구성이 이 스키마에서 직접 나온다:

소스 쿼리
요약 crawl_run 최근 1건 + dmf_event 오늘자 집계 + λ 추정치 + 게이트 통과 여부
신규 dmf_event WHERE event_type='NEW' AND run_id=?
변경 dmf_event WHERE event_type='MODIFIED' AND run_id=? (changes_json 을 before/after 열로 펼침)
취하 dmf_event WHERE event_type IN ('WITHDRAWN')
전체현황 dmf_record WHERE is_current=1
이력 dmf_event 최근 90일
실행로그 crawl_run 최근 30건 (소요시간·행수·거부사유)

"탭별로 연동된" 요구사항은 natural_key 를 공통 축으로 삼아 xlsx 하이퍼링크(=HYPERLINK("#'전체현황'!A15", "상세보기"))로 연결하면 된다.


8. robots.txt RFC 9309 규범과 politeness 실무

8.1 RFC 9309 원문 규범 (2022년 9월, IETF)

서지: RFC 9309 "Robots Exclusion Protocol (REP)", M. Koster, G. Illyes, H. Zeller, L. Sassman (Google LLC), September 2022. 1994년 Martijn Koster 가 정의한 방식을 표준화·확장한 것.

항목 규범 (원문 인용) DMF_Crawler 구현
product token 문자 집합 "MUST contain only uppercase and lowercase letters ('a-z' and 'A-Z'), underscores ('_'), and hyphens ('-')" product token = DMF-Crawler. 숫자·점·슬래시 없음
product token 위치 "The product token SHOULD be a substring of the identification string that the crawler sends to the service. For example, in the case of HTTP, the product token SHOULD be a substring in the User-Agent header." User-Agent: DMF-Crawler/1.0 (+mailto:yunchanpaca@gmail.com)DMF-Crawler 가 부분문자열로 포함됨
매칭 대소문자 user-agent 매칭은 대소문자 무시. 여러 그룹이 매치되면 규칙을 합친다 urllib.robotparser 기본 동작
allow/disallow 우선순위 "The most specific match found MUST be used. The most specific match is the match that has the most octets." 경로 매칭은 대소문자 구분, 첫 문자부터 표준 라이브러리 위임
캐싱 "Crawlers SHOULD NOT use the cached version for more than 24 hours, unless the robots.txt file is unreachable." 실행 시작 시 1회 fetch, fetched_at 기록. 24시간 초과 시 재fetch
4xx "If a server status code indicates that the robots.txt file is unavailable to the crawler, then the crawler MAY access any resources." 접근 허용. 단 로그에 명시적으로 기록
5xx "If the robots.txt file is unreachable due to server or network errors, this means the robots.txt file is undefined and the crawler MUST assume complete disallow." 크롤 중단 + 알림. 표준 라이브러리는 이걸 자동으로 하지 않으므로 직접 구현
파싱 크기 한계 "The parsing limit MUST be at least 500 kibibytes [KiB]." 500 KiB 까지 읽고 초과분 무시
crawl-delay RFC 9309 은 crawl-delay 를 정의하지 않는다. 실무에서 쓰이지만 "표준화할 만한 일관된 실제 동작이 없었다"는 이유로 의도적으로 제외 존재하면 존중하되, 없어도 자체 하한 2초를 반드시 건다

8.2 Google 의 실무 사양 (developers.google.com)

항목 내용
crawl-delay 미지원 — "Google supports the following fields (other fields such as crawl-delay aren't supported)"
미지원 규칙 "Rules other than allow, disallow, and user-agent are ignored by the robots.txt parser"
user-agent 매칭 "Google's crawlers determine the correct group of rules by finding in the robots.txt file the group with the most specific user agent that matches the crawler's user agent"
캐싱 "Google generally caches the contents of robots.txt file for up to 24 hours, but may cache it longer in situations where refreshing the cached version isn't possible"
크기 제한 "Google enforces a robots.txt file size limit of 500 kibibytes (KiB). Content which is after the maximum file size is ignored."

crawl-delay 가 표준에도 없고 Google 도 안 지킨다는 사실이, 우리가 crawl-delay 를 무시해도 된다는 뜻은 아니다. 우리는 검색엔진이 아니라 한 사이트만 긁는 소규모 크롤러이므로, crawl-delay 가 명시돼 있으면 그것이 사이트 운영자의 명시적 의사 표시다. 명시값과 우리 하한(2초) 중 큰 값을 쓴다.

8.3 RFC 9309 준수 robots 게이트 구현

# src/dmf_crawler/robots.py
"""RFC 9309 (Robots Exclusion Protocol, 2022-09) 준수 게이트.

표준 라이브러리 urllib.robotparser 는 RFC 9309 의 다음 규범을 자동으로 처리하지 않으므로
직접 구현한다:
  - 5xx → "MUST assume complete disallow"
  - 24시간 캐시 상한
  - 500 KiB 파싱 한계
"""
from __future__ import annotations

import time
import urllib.parse
import urllib.robotparser
from dataclasses import dataclass

import requests

USER_AGENT = "DMF-Crawler/1.0 (+mailto:yunchanpaca@gmail.com)"
PRODUCT_TOKEN = "DMF-Crawler"          # RFC 9309: a-z A-Z _ - 만 허용
MAX_ROBOTS_BYTES = 500 * 1024          # RFC 9309: "parsing limit MUST be at least 500 KiB"
CACHE_TTL_SECONDS = 24 * 60 * 60       # RFC 9309: "SHOULD NOT use the cached version for more than 24 hours"
MIN_DELAY_SECONDS = 2.0                # 우리 자체 하한 (crawl-delay 부재 시)


class RobotsDisallowed(RuntimeError):
    """robots.txt 가 접근을 금지했거나, 5xx 로 '완전 금지'로 간주해야 하는 경우."""


@dataclass
class RobotsGate:
    base_url: str
    fetched_at: float = 0.0
    _parser: urllib.robotparser.RobotFileParser | None = None
    _crawl_delay: float | None = None
    _unreachable_4xx: bool = False

    @property
    def robots_url(self) -> str:
        p = urllib.parse.urlsplit(self.base_url)
        return urllib.parse.urlunsplit((p.scheme, p.netloc, "/robots.txt", "", ""))

    def _expired(self) -> bool:
        return (time.time() - self.fetched_at) > CACHE_TTL_SECONDS

    def refresh(self, session: requests.Session | None = None) -> None:
        sess = session or requests.Session()
        parser = urllib.robotparser.RobotFileParser()
        parser.set_url(self.robots_url)

        try:
            resp = sess.get(self.robots_url, headers={"User-Agent": USER_AGENT}, timeout=20)
        except requests.RequestException as exc:
            # 네트워크 오류 = "unreachable due to server or network errors" → 완전 금지
            raise RobotsDisallowed(f"robots.txt 취득 실패(네트워크): {exc}") from exc

        if 500 <= resp.status_code < 600:
            raise RobotsDisallowed(
                f"robots.txt 가 {resp.status_code} 응답 — RFC 9309 에 따라 완전 금지로 간주한다."
            )

        if 400 <= resp.status_code < 500:
            # RFC 9309: "the crawler MAY access any resources"
            self._unreachable_4xx = True
            parser.parse([])
            self._parser = parser
            self._crawl_delay = None
            self.fetched_at = time.time()
            return

        body = resp.content[:MAX_ROBOTS_BYTES].decode("utf-8", errors="replace")
        parser.parse(body.splitlines())
        self._parser = parser
        self._unreachable_4xx = False
        # crawl_delay / request_rate 는 Python 3.6+ 에서 제공된다.
        cd = parser.crawl_delay(PRODUCT_TOKEN)
        rr = parser.request_rate(PRODUCT_TOKEN)
        delay = float(cd) if cd is not None else None
        if rr is not None and rr.requests > 0:
            delay = max(delay or 0.0, rr.seconds / rr.requests)
        self._crawl_delay = delay
        self.fetched_at = time.time()

    def can_fetch(self, url: str, session: requests.Session | None = None) -> bool:
        if self._parser is None or self._expired():
            self.refresh(session)
        assert self._parser is not None
        if self._unreachable_4xx:
            return True
        return self._parser.can_fetch(PRODUCT_TOKEN, url)

    def require(self, url: str, session: requests.Session | None = None) -> None:
        if not self.can_fetch(url, session):
            raise RobotsDisallowed(f"robots.txt 가 금지한 경로: {url}")

    def effective_delay(self) -> float:
        """robots.txt 의 crawl-delay 와 우리 하한 중 큰 값."""
        return max(self._crawl_delay or 0.0, MIN_DELAY_SECONDS)

    def sitemaps(self) -> list[str]:
        """Sitemap 은 noisy change-indicating signal (§4.6) 로 활용한다. Python 3.8+."""
        if self._parser is None:
            return []
        return list(self._parser.site_maps() or [])

8.4 politeness 스로틀 — Scrapy AutoThrottle 알고리즘 + Heritrix delayFactor

# src/dmf_crawler/throttle.py
"""요청 간격 제어.

두 계보를 합친다:
  - Heritrix: delayFactor(기본 5.0) × 직전 요청 소요 시간, minDelayMs / maxDelayMs(기본 30000) 로 클램프
  - Scrapy AutoThrottle: 목표 지연 = latency / N, 다음 지연 = (이전 지연 + 목표 지연) / 2,
                         비 200 응답은 지연을 '늘리기만' 한다.
호스트당 동시 요청 1개는 구조적으로 보장한다(Mercator back-end 큐 = 호스트당 1큐).
"""
from __future__ import annotations

import random
import threading
import time
from dataclasses import dataclass

DELAY_FACTOR = 5.0            # Heritrix delayFactor 기본값
MIN_DELAY_S = 2.0             # Heritrix minDelayMs 대응
MAX_DELAY_S = 30.0            # Heritrix maxDelayMs 기본값 30000ms
TARGET_CONCURRENCY = 1.0      # Scrapy AUTOTHROTTLE_TARGET_CONCURRENCY (낮을수록 정중)
JITTER_RATIO = 0.1            # 서버 입장에서 규칙적 타격을 피하기 위한 소량 지터


@dataclass
class PolitenessThrottle:
    """단일 호스트용. 동시 접근 1개를 락으로 강제한다."""

    min_delay_s: float = MIN_DELAY_S
    max_delay_s: float = MAX_DELAY_S
    delay_factor: float = DELAY_FACTOR
    target_concurrency: float = TARGET_CONCURRENCY

    _lock: threading.Lock = threading.Lock()
    _current_delay: float = MIN_DELAY_S
    _last_request_end: float = 0.0

    def set_min_delay(self, seconds: float) -> None:
        """robots.txt 의 crawl-delay 를 반영한다."""
        self.min_delay_s = max(self.min_delay_s, seconds)
        self._current_delay = max(self._current_delay, self.min_delay_s)

    def _clamp(self, delay: float) -> float:
        return min(max(delay, self.min_delay_s), self.max_delay_s)

    def wait(self) -> None:
        """다음 요청 전 대기. 락 획득으로 동시 요청도 함께 막는다."""
        self._lock.acquire()
        elapsed = time.monotonic() - self._last_request_end
        jitter = self._current_delay * JITTER_RATIO * random.uniform(-1.0, 1.0)
        sleep_for = self._current_delay + jitter - elapsed
        if sleep_for > 0:
            time.sleep(sleep_for)

    def observe(self, latency_s: float, status_code: int | None) -> None:
        """응답 관찰 후 다음 지연을 갱신하고 락을 놓는다."""
        try:
            heritrix_delay = self.delay_factor * latency_s              # Heritrix delayFactor
            autothrottle_target = latency_s / max(self.target_concurrency, 1e-6)  # Scrapy 목표 지연
            target = max(heritrix_delay, autothrottle_target)
            new_delay = (self._current_delay + target) / 2.0            # Scrapy: 이전과 목표의 평균

            if status_code is None or status_code != 200:
                # Scrapy: 비 200 응답은 지연을 늘리기만 한다.
                new_delay = max(new_delay, self._current_delay)

            self._current_delay = self._clamp(new_delay)
            self._last_request_end = time.monotonic()
        finally:
            self._lock.release()

8.5 재시도와 백오프

# src/dmf_crawler/fetcher.py
"""HTTP 취득 — robots 게이트 + politeness + 재시도/백오프 + 조건부 요청 + 원본 보존.

Mercator 의 Protocol Module + RIS 에 대응한다.
"""
from __future__ import annotations

import gzip
import hashlib
import random
import time
from dataclasses import dataclass
from pathlib import Path

import requests

from .robots import USER_AGENT, RobotsGate, RobotsDisallowed
from .throttle import PolitenessThrottle

RETRY_STATUS = frozenset({429, 500, 502, 503, 504})
MAX_ATTEMPTS = 3
BACKOFF_BASE_S = 2.0
BACKOFF_FACTOR = 2.0
BACKOFF_CAP_S = 60.0
BACKOFF_JITTER = 0.25
CONNECT_TIMEOUT_S = 10.0
READ_TIMEOUT_S = 30.0


@dataclass
class FetchResult:
    url: str
    status_code: int
    body: bytes | None            # 304 면 None
    etag: str | None
    last_modified: str | None
    from_cache: bool
    latency_s: float
    raw_path: Path | None


def _backoff_seconds(attempt: int, retry_after: str | None) -> float:
    if retry_after:
        try:
            return min(float(retry_after), BACKOFF_CAP_S)
        except ValueError:
            pass
    base = min(BACKOFF_BASE_S * (BACKOFF_FACTOR ** (attempt - 1)), BACKOFF_CAP_S)
    return base * (1.0 + random.uniform(-BACKOFF_JITTER, BACKOFF_JITTER))


class Fetcher:
    def __init__(self, base_url: str, raw_dir: Path):
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": USER_AGENT,
            "Accept-Language": "ko-KR,ko;q=0.9",
            "Accept": "text/html,application/xhtml+xml",
        })
        self.robots = RobotsGate(base_url=base_url)
        self.throttle = PolitenessThrottle()
        self.raw_dir = raw_dir

    def prepare(self) -> None:
        """실행 시작 시 1회. robots.txt 를 받고 crawl-delay 를 스로틀에 반영한다."""
        self.robots.refresh(self.session)
        self.throttle.set_min_delay(self.robots.effective_delay())

    def _save_raw(self, url: str, body: bytes) -> Path:
        self.raw_dir.mkdir(parents=True, exist_ok=True)
        digest = hashlib.sha1(url.encode("utf-8")).hexdigest()[:16]
        path = self.raw_dir / f"{digest}.html.gz"
        path.write_bytes(gzip.compress(body))
        (self.raw_dir / f"{digest}.url.txt").write_text(url, encoding="utf-8")
        return path

    def get(self, url: str, *, etag: str | None = None, last_modified: str | None = None) -> FetchResult:
        self.robots.require(url, self.session)      # RFC 9309 게이트. 실패 시 RobotsDisallowed

        headers: dict[str, str] = {}
        if etag:
            headers["If-None-Match"] = etag
        if last_modified:
            headers["If-Modified-Since"] = last_modified

        last_exc: Exception | None = None
        for attempt in range(1, MAX_ATTEMPTS + 1):
            self.throttle.wait()
            started = time.monotonic()
            status: int | None = None
            try:
                resp = self.session.get(
                    url, headers=headers, timeout=(CONNECT_TIMEOUT_S, READ_TIMEOUT_S), allow_redirects=True
                )
                status = resp.status_code
            except requests.RequestException as exc:
                last_exc = exc
                self.throttle.observe(time.monotonic() - started, None)
                if attempt == MAX_ATTEMPTS:
                    raise
                time.sleep(_backoff_seconds(attempt, None))
                continue
            finally:
                if status is not None:
                    self.throttle.observe(time.monotonic() - started, status)

            latency = time.monotonic() - started

            if status == 304:
                return FetchResult(url, 304, None, etag, last_modified, True, latency, None)

            if status in RETRY_STATUS:
                if attempt == MAX_ATTEMPTS:
                    resp.raise_for_status()
                time.sleep(_backoff_seconds(attempt, resp.headers.get("Retry-After")))
                continue

            resp.raise_for_status()
            raw_path = self._save_raw(url, resp.content)     # ★ 파싱 전에 원본 보존 (Mercator RIS)
            return FetchResult(
                url, status, resp.content,
                resp.headers.get("ETag"), resp.headers.get("Last-Modified"),
                False, latency, raw_path,
            )

        raise RuntimeError(f"fetch 실패: {url}") from last_exc

9. 한국 문헌·선행 시스템·법적 맥락

9.1 국내 선행 연구 요약

논문 연도 우리와 같은 점 우리와 다른 점 / 취할 것
웹 사이트 컨텐츠 변경 모니터링 시스템 (김원중·조이기·손철수, 한국정보통신학회논문지 6(4):505512) 2002 대상 URL·모니터링 조건·주기를 사용자가 정의, 변경 시 알람/E-mail 통지. HTML 태그로 웹 문서를 의미 있는 단위로 구조화·분류해 변경 감지 전 페이지 텍스트 diff 가 아니라 구조 단위 diff 라는 원칙만 취한다. 알림 채널은 Windows 토스트로 대체
실시간 웹 크롤링 분산 모니터링 시스템 설계 및 구현 (R-WCMS) (김영아·김계희·김현주·김창근, 융합정보논문지 9(1):4553) 2019 제한된 웹사이트의 실시간 정보 수집, 수집 시간 예측을 통한 효율화 Kafka + Spark Streaming + Hadoop 스택은 우리 규모에 명백한 과잉. "수집 시간 1517% 단축" 결과보다 "수집 시간을 측정하고 예측한다"는 계측 습관만 가져와 §5.6 게이트 G-8 로 구현
실시간 웹 게시판 모니터링 및 모바일웹을 이용한 알람 서비스 개발 (김종근·심근호·이요셉·임영환, 디지털콘텐츠학회논문지 13(1):111) 2012 게시판 감시 + 알림. 기존 방식의 한계로 DB 직접 접근 불가·개방 API 부재·비공개 게시판 접근 불가·실시간 알림 어려움을 지목 우리도 DB 직접 접근이 불가하므로 크롤링이 유일한 수단이라는 상황이 동일. 알림 채널을 out-of-band(리포트와 독립) 로 두는 설계를 지지
나라장터 입찰공고 인공지능 검색모델 개발에 관한 연구 (이선표·현철호·장수현·지학수·박승범, 정보화연구 19(3)) 2022 정부 공고 게시판 대상, "검색 기능이 매우 부정확하여 많은 사용자들이 불편" 크롤링 방법은 명시되어 있지 않다(문서를 구절로 분할해 키워드로 의사-질의 생성하는 전처리만 서술). 모델 구조: ① Elastic Search BM25 초기 검색 → ② BERT 재순위화 → ③ 의사-질의/의사-레이블로 약지도학습. → 우리 리포트에 검색 기능을 붙일 때의 참고이며, 현 단계 범위 밖

국내 문헌에서 확인하지 못한 것 (부록 B 로 이관): nedrug.mfds.go.kr 또는 식약처 DMF 데이터를 대상으로 한 직접적인 크롤링 연구 논문은 raw dump 의 검색 범위에서 발견되지 않았다. DBpia/RISS 직접 검색이 필요하다(WebSearch 예산 소진으로 미수행).

9.2 관련 국내 데이터 출처 (실측 대상)

이름 URL 성격 우선순위
의약품안전나라 (NeDrug) https://nedrug.mfds.go.kr/ 식약처 의약품 정보 통합 포털. 의약품·제품·제조업체·광고·기준규격 정보 1순위 크롤 대상
의약품등 검색 https://nedrug.mfds.go.kr/searchDrug 검색 인터페이스 실측 대상
NeDrug 통합자료실 게시판 (예: eCTD 민원서식작성기) https://nedrug.mfds.go.kr/bbs/43 게시판 URL 패턴 실례/bbs/{번호} 형태 DMF 공고 게시판 번호 실측 필요
식의약 데이터 포털 https://data.mfds.go.kr/ XML/JSON 형식의 허가 의약품·의료기기 정보 제공 ★ OpenAPI 가 있으면 크롤링보다 우선한다 (부록 B)
식품의약품안전평가원 https://www.nifds.go.kr/ 평가원 보조
국가건강정보포털 약품/식품정보 https://health.kdca.go.kr/healthinfo/biz/health/gnrlzHealthInfo/healthInfo/medcinFoodInfoMain.do 질병관리청 참고
약학정보원 https://health.kr/ 민간 의약품 정보 참고, 크롤링 대상 아님(민간 DB — §9.3 법적 리스크)

⚠️ raw dump 의 리서치 에이전트는 이 URL들을 검색 결과로만 확인했고, nedrug.mfds.go.kr 의 실제 게시판 구조·robots.txt·DMF 공고 페이지 URL 은 WebFetch 로 열어보지 못했다. 전부 부록 B 의 실측 항목이다.

9.3 한국 법적 맥락 — 크롤링 관련 대법원 판결

출처 주의: 아래 내용은 raw dump 의 [CMD] 블록에서 발견된 PDF(법무법인 세종 IP그룹 뉴스레터로 추정, 2022-06-20)에서 추출한 것이다. 원본 URL 이 raw dump 에 기록되어 있지 않아 출처를 특정하지 못했다(⚠️ 미검증 출처). 판례 번호와 인용문은 PDF 원문 그대로이며, 법률 자문이 아니다.

대상 판결: 대법원 2022. 5. 12. 선고 2021도1533 판결 (숙박업소 정보제공 서비스 크롤링 형사사건, 무죄 확정)

사실관계: 피고인 회사가 '패킷캡쳐' 분석으로 만든 크롤링 프로그램으로 경쟁사 API 서버에 주기적으로 접근해 숙박업소 정보를 복제. 피해자 회사 앱은 이용자 위치 기준 7~30km 범위 검색이었으나, 피고인 프로그램은 특정 위도/경도 중심 반경 1,000km 정보를 불러오는 방식으로 수집.

소송 경과: 제1심(서울중앙지법 2020. 2. 11. 선고 2019고단1777) 전부 유죄 → 항소심(서울고법 2021. 1. 13. 선고 2020노611) 전부 무죄 → 상고심 무죄 확정.

3개 죄목별 판시:

죄목 결론 판시 요지
정보통신망법 위반(정보통신망침입, 제48조 제1항) 무죄 접근권한의 유무·범위는 서비스제공자가 부여한 접근권한을 기준으로 판단하고, 제한 여부는 보호조치나 이용약관 등 객관적으로 드러난 사정을 종합 고려해야 한다. 본건에서 (i) API 서버 URL·명령구문은 통상적인 패킷캡쳐 프로그램으로 누구나 쉽게 알아낼 수 있는 정보였고 접근을 막는 별도 보호조치가 없었으며, (ii) 이용약관상 제한이 비회원인 피고인들에게 적용된다고 보기 어렵고 규정 내용도 접근권한 자체를 제한하는 것으로 볼 수 없다 → '침입' 불인정
저작권법 위반(DB제작자 권리 침해, 제93조) 무죄 '상당한 부분의 복제등' 판단은 양적·질적 측면을 함께 고려하고, 질적 상당성은 개별 소재의 가치나 그 생산 투자가 아니라 제작·갱신·검증·보충에 대한 상당한 투자 여부로 판단한다. 반복적·체계적 복제등(제93조 제2항 단서)은 "결국 상당한 부분의 복제등을 한 것과 같은 결과를 발생하게 한 경우에 한하여" 인정함이 타당. 본건에서 피해자 DB 의 50여개 항목 중 피고인이 크롤링한 것은 적게는 3개, 많게는 8개였고 '업체명'·'주소'·'지역' 등 이용자들에게 공개한 정보였다
컴퓨터등장애업무방해(형법) 무죄 1,000km 거리 정보 입력은 "주어진 명령구문에 대응하는 정보를 반환하는" API 서버의 본래 목적에 따른 정보 호출이므로 '허위의 정보 또는 부정한 명령의 입력'으로 볼 수 없다. 크롤링 기간 중 일부 접속 장애가 있었으나 공휴일 등 자연 이용자 증가 가능성을 배제할 수 없어 현실적인 정보처리 장애 발생을 부정

반대 방향 판결 — 유죄 사례: 서울남부지방법원 2021. 9. 8. 선고 2021고단588 판결. 회원 계정을 이용해 두 피해자 회사 구인정보 웹사이트에 각각 3만여 회 접속해 이력서 정보를 상당량 수집했고, 이용약관에 검색 이력서 정보의 사용목적 제한 및 무단 복제·재배포 금지가 명시되어 있던 사안 → 정보통신망법 위반(침입) 및 저작권법 위반(DB제작자 권리 침해) 모두 유죄.

선행 대표 사건:

  • 엔하위키미러 사건: 서울중앙지법 2015. 11. 27. 선고 2014가합44470 → 서울고법 2016. 12. 15. 선고 2015나2074198 → 대법원 2017. 4. 13. 선고 2017다204315
  • 사람인 사건: 서울중앙지법 2016. 2. 17. 선고 2015가합517982 → 서울고법 2017. 4. 6. 선고 2016나2019365 → 대법원 2017. 8. 24. 선고 2017다224395

적용 법조 3종 세트 (크롤링 분쟁에서 통상 함께 주장됨):

  1. 저작권법 제93조 — 데이터베이스제작자의 권리 (형사처벌 대상)
  2. 부정경쟁방지 및 영업비밀보호에 관한 법률 제2조 제1호 (파)목 — "타인의 상당한 노력이나 투자로 만들어진 성과 등을 무단으로 사용하는 행위". 2021. 12. 7. 법률 제18548호 개정으로 2022. 4. 20. 시행, 개정 전 구법상으로는 동조 동호 (카)목. 형사처벌 대상에서 제외되어 민사소송 청구원인으로만 활용
  3. 정보통신망 이용촉진 및 정보보호 등에 관한 법률 제48조 제1항 — 정보통신망 침입 (형사처벌 대상)

뉴스레터가 제시한 "서비스 제공자가 크롤링을 막으려면" 목록 — 우리는 이것을 뒤집어 읽어 우리 안전 체크리스트로 쓴다:

서비스 제공자의 방어 수단 DMF_Crawler 의 대응 (안전선)
서버 접근 제한 보안조치(접속 허가 계정 관리, 암호화) 로그인하지 않는다. 인증이 필요한 페이지는 크롤하지 않는다.
웹페이지에 크롤링 금지 경고문구 기재, 이용약관에 크롤링 금지 명시 매 실행 전 robots.txt 확인. 이용약관 변경 여부를 분기 1회 사람이 확인한다. 금지 문구가 발견되면 즉시 중단하고 사람에게 알린다.
'워터마크' 정보(자연적/인위적 오류, 비일반적 표현방식) 삽입해 크롤링 증거화 우리는 원본을 그대로 보존하고 재배포하지 않으므로 무관. 수집 데이터를 외부에 재배포하지 않는다.
DB 구축 과정의 인적 투자 증빙 확보 무관
회원 ID 로 접근하는 경우 약관상 크롤링 금지 여부, 정보의 공개 범위 확인 회원 ID 를 사용하지 않는다.

DMF_Crawler 의 법적 안전선 (확정 4항):

  1. 로그인하지 않는다. 공개된 페이지만 취득한다.
  2. robots.txt 를 준수한다. 금지 경로는 절대 접근하지 않는다. 5xx 면 크롤을 중단한다.
  3. 서버에 부하를 주지 않는다. 동시성 1, 최소 지연 2초, delayFactor 5.0, 하루 총 요청 수십수백 건. 위 판결의 "3만여 회 접속"과는 34 자릿수 차이다.
  4. 수집 데이터를 외부에 재배포하지 않는다. 사내 리포트 용도로만 쓴다.

추가로, 대상이 정부기관(식약처) 공공 정보라는 점이 위 판례들의 민간 DB 사안과 결정적으로 다르다. 그러나 "공공 정보이므로 무제한 허용"이라는 결론은 도출되지 않으므로, 위 4항을 그대로 지킨다.

참고 문헌(뉴스레터 각주): 김현숙, "크롤링을 이용한 공개데이터의 수집·활용의 법적 쟁점에 대한 비판적 검토", 「강원법학」 제61권(2020), 237면.


10. 이 프로젝트의 크롤링 정책 확정안

10.1 주기 · 스케줄

항목 근거
크롤 주기 1일 1회 요구사항 + Cho & Garcia-Molina 균등 정책
실행 시각 매일 06:00 KST (정시, 지터 없음) Coffman et al. "접근은 가능한 한 균등 간격으로"
재방문 정책 균등(uniform) — 게시판별 차등 없음 Cho & Garcia-Molina TODS 2003: uniform > proportional
스케줄 실패 시 다음 정시까지 대기하지 않고 최대 3회, 30분 간격으로 재시도 후 포기 + 알림 하루를 통째로 놓치지 않기 위함
재부팅 후 Windows 작업 스케줄러의 "놓친 실행 즉시 시작" 옵션 사용 요구사항
최대 탐지 지연 (SLA) 24시간 + 실행 소요 시간 §4.2 유도
평균 탐지 지연 (SLA) 약 12시간 §4.2 유도
주 1회 전체 재검증 일요일 06:00 — 조건부 요청을 끄고 상세 페이지까지 전량 재취득 §4.5 — Last-Modified/ETag 신뢰 불가 대비

10.2 politeness

항목 근거
호스트당 동시 요청 1 (구조적으로 강제 — PolitenessThrottle 락) Mercator: "동일 웹서버에 동시 다운로드 금지"가 최소 요건. back-end 큐 = 호스트당 1개
최소 요청 간격 (min_delay_s) 2.0초 Heritrix minDelayMs 대응. Cho 10초·WIRE 15초보다 짧으나 총 요청량이 수십 건
적응형 지연 (delay_factor) 5.0 × 직전 요청 소요 시간 Heritrix delayFactor 기본값 5.0 그대로
최대 지연 (max_delay_s) 30.0초 Heritrix maxDelayMs 기본값 30000ms 그대로
목표 동시성 (AutoThrottle) 1.0 Scrapy AUTOTHROTTLE_TARGET_CONCURRENCY 기본값. "낮을수록 정중"
지연 갱신식 next = (current + max(5.0×latency, latency/1.0)) / 2, [2.0, 30.0] 로 클램프 Scrapy AutoThrottle 알고리즘 + Heritrix delayFactor 결합
비 200 응답 시 지연을 늘리기만 하고 줄이지 않는다 Scrapy AutoThrottle 규칙 4
지터 현재 지연의 ±10% 서버 입장의 규칙적 타격 회피
crawl-delay 존재 시 max(robots crawl-delay, 2.0초) RFC 9309 에 없는 필드이나 운영자의 명시적 의사
Request-rate 존재 시 max(위 값, seconds/requests) urllib.robotparser.request_rate (Python 3.6+)
하루 총 요청 상한 500건 (초과 시 중단 + 알림) 폭주 방지 안전판
대역폭 상한 미설정 요청 수가 적어 불필요

10.3 robots.txt (RFC 9309)

항목 근거
준수 여부 완전 준수 (obey) RFC 9309, Heritrix robotsPolicyName=obey
product token DMF-Crawler (a-z A-Z _ - 만 사용) RFC 9309 "MUST contain only..."
User-Agent 헤더 DMF-Crawler/1.0 (+mailto:yunchanpaca@gmail.com) RFC 9309 "SHOULD be a substring in the User-Agent header" + Heritrix metadata.operatorContactUrl 정신
robots.txt 취득 시점 실행 시작 시 1회 Mercator 의 호스트→규칙 캐시
캐시 유효기간 24시간 RFC 9309 "SHOULD NOT use the cached version for more than 24 hours"
파싱 크기 한계 500 KiB (초과분 무시) RFC 9309 "MUST be at least 500 KiB" / Google 동일
4xx 응답 시 접근 허용 + 로그 기록 RFC 9309 "the crawler MAY access any resources"
5xx 또는 네트워크 오류 시 크롤 전면 중단 + Windows 알림 RFC 9309 "the crawler MUST assume complete disallow"
Disallow 경로 절대 접근 금지 RFC 9309
Sitemap noisy change signal 로만 활용, 신뢰하지 않음 §4.6, Busa-Fekete et al. 2025

10.4 재시도 · 백오프 · 타임아웃

항목 비고
최대 시도 횟수 3 (최초 1 + 재시도 2)
재시도 대상 상태 코드 429, 500, 502, 503, 504
재시도 대상 예외 ConnectionError, Timeout, DNS 실패
재시도하지 않는 것 4xx (429 제외), RobotsDisallowed, 파싱 오류 재시도해도 결과가 같다
백오프 방식 지수 백오프 + 지터
백오프 기본값 base = 2.0초, factor = 2.0, cap = 60초 시도별: 2초 → 4초
지터 ±25% thundering herd 방지
Retry-After 헤더 존재 시 우선 사용 (cap 60초 적용) HTTP 표준 존중
연결 타임아웃 10초
읽기 타임아웃 30초
전체 실행 타임아웃 30분 (초과 시 중단 + 알림) 게이트 G-8 과 연동

10.5 조건부 요청 · 캐시

항목 근거
If-None-Match (ETag) 저장된 ETag 가 있으면 항상 전송 HTTP 표준
If-Modified-Since 저장된 Last-Modified 가 있으면 항상 전송 HTTP 표준
304 처리 (목록 1페이지) 무시하고 무조건 재취득·재파싱 §4.6 — noisy signal 을 진실로 삼지 않는다
304 처리 (상세 페이지) 다운로드 생략, 이전 파싱 결과 재사용 비용 절감
주 1회 전체 재검증 일요일에는 조건부 헤더를 보내지 않는다 Last-Modified 관리 부실 대비
원본 HTML 보존 파싱 전에 raw/YYYY-MM-DD/<sha1>.html.gz 로 저장 Mercator RIS / Heritrix WARC
원본 보존 기간 180일 (이후 월 1건만 남기고 삭제) 소급 재파싱 + 디스크 관리

10.6 중복 판정 키

우선순위 정규화
1 DMF 등록번호 / 게시물 고유 ID NFKC + 공백 제거 + 대문자화
2 상세 URL 의 안정 파라미터(seq/id 등) 쿼리 파라미터 정렬, 세션·페이지 파라미터 제거
3 sha1(norm_name(주성분명) ‖ norm_name(업체명) ‖ norm_exact(등록일)) 취약 — 사용 시 리포트에 경고 표시

부가 지문:

  • body_simhash = 상세 본문 텍스트의 64-bit SimHash (Manku et al. 2007 검증 파라미터)
  • 첨부파일 = SHA-256 (정확 일치)

10.7 변경 판정 규칙

필드 종류 비교 방법 임계
식별키 norm_exact 완전 일치
코드/번호/날짜 norm_exact 완전 일치
업체명/성분명 norm_name(NFKC + 괄호 주석 제거 + "주식회사"/"(주)" 제거 + 공백 제거) 완전 일치
상태 enum 매핑 후 완전 일치
자유서술(변경 사유·비고) SimHash Hamming 거리 ≤ 3 이면 동일, 초과면 변경
첨부파일 목록 파일명 집합 + SHA-256

이벤트 판정:

이벤트 조건
NEW 오늘 O, 어제 X, 과거 취하 이력 없음
REAPPEARED 오늘 O, 어제 X, 과거 취하 이력 있음 → 별도 표시
MODIFIED 양쪽 O, 비교 필드 중 1개 이상 상이
WITHDRAWN 어제 O 오늘 X 또는 상태가 취하/취소/말소 로 전이
UNCHANGED 그 외

10.8 결과 채택 게이트 (전부 통과해야 리포트 생성)

게이트 조건 실패 시 조치
G-0 robots robots.txt 5xx / 네트워크 오류 / Disallow 중단
G-1 행 수 하한 목록 페이지 행 수 < min_expected 중단
G-2 행 수 급변 총 레코드 수 전일 대비 ±30% 초과 보류 (사람 승인 필요)
G-3 필수 필드 null required 필드 null 비율 > 5% 중단
G-4 패턴 불일치 pattern 매치율 < 95% 중단
G-5 대량 취하 취하 판정 > 전체의 10% 중단 — 가장 중요한 게이트
G-6 fallback 발동 주 셀렉터 0건 → fallback 성공 결과 채택 + 경고 + 셀렉터 재생성 트리거
G-7 이상 정지 평소 λ 대비 30일간 변경 0건 경고
G-8 실행 시간 이상 평소 대비 3배 초과 경고
G-9 RAPTURE drift 필드별 결합 확률 < 0.05 보류 (사람 승인 필요)

"중단"의 의미: 리포트를 생성하지 않고, DB 에 이벤트를 기록하지 않으며(crawl_run.status = REJECTED 만 기록), Windows 토스트 알림으로 사람에게 알린다. 원본 HTML 은 이미 저장되어 있으므로 사람이 확인 후 재파싱할 수 있다.

10.9 LLM (agy) 사용 정책

항목
정상 실행 시 LLM 호출 수 0회 (요약 기능 사용 시 1회)
LLM 호출 조건 ① 셀렉터 파손 감지(G-6/G-9) ② 일일 요약 생성(선택)
실행 방식 agy -p <prompt> headless, JSON 강제, 타임아웃 180초
캐싱 sha256(prompt + schema + model + params) 키로 디스크 캐시 (DELM 2025)
LLM 출력 검증 필수 — perturbation test + 게이트 G-1~G-9 전량 재실행
셀렉터 자동 채택 금지 — 검증 통과해도 pending 상태로 저장하고 사람이 승인
LLM 실패 시 이전 셀렉터 유지, 리포트는 정상 생성(요약만 생략)
프롬프트 입력 DOM pruning 된 부분 트리만. 전체 HTML 금지 (Mind2Web/AXE/Co-Scraper)
seed 페이지 수 2~3장 (Huang & Song 2025)

10.10 관측·알림

항목
매 실행 기록 crawl_run — 시작/종료 시각, 상태, 행 수, 소요시간, 셀렉터 버전, 거부 사유
변경률 관측 기록 change_observation — 일자별 변경 여부 + 신규/변경/취하 건수
λ 재추정 주기 매 실행 (최근 14일 / 전체 두 창)
알림 채널 Windows 토스트 알림 (리포트와 독립된 out-of-band 채널)
알림 트리거 게이트 중단/보류, 셀렉터 파손, 서비스 사망, 실행 3회 연속 실패
리포트 위치 reports/DMF_YYYY-MM-DD.xlsx
로그 보존 텍스트 로그 90일, crawl_run 무기한

부록 A. 출처 목록

raw dump 에 등장한 모든 URL 261개를 하나도 빠짐없이 기록한다. 확인 열의 의미: 본문 확인 = WebFetch 로 실제로 열어 내용을 읽음 / ⚠️ 열었으나 본문 파싱 실패 = 요청은 성공했으나 로그인 페이지·빈 페이지·PDF 바이너리라 내용을 얻지 못함 / = HTTP 오류나 연결 거부로 접근 실패 / = 리다이렉트로 중단 / 🔶 검색 결과 = 검색 결과 목록에만 등장하고 직접 열어보지 않음(제목은 검색엔진이 제공한 값).

  • 총 URL: 261개
  • 이 중 WebFetch 로 직접 요청한 것: 53개
  • 본문 확인에 성공한 것: 39개

A.1 표준·공식 문서 (9개)

# 제목 URL 확인
1 Heritrix User Manual Internet Archive Kristinn Sigur#sson Michael Stack http://crawler.archive.org/articles/user_manual.pdf 🔶 검색 결과
2 RFC 9309: Robots Exclusion Protocol https://www.rfc-editor.org/rfc/rfc9309.html 본문 확인
3 RFC 9309: Robots Exclusion Protocol | RFC Editor https://www.rfc-editor.org/info/rfc9309/ 🔶 검색 결과
4 RFC 9309 Robots Exclusion Protocol Abstract https://www.ietf.org/rfc/rfc9309.pdf 🔶 검색 결과
5 Event Sourcing Pattern - Azure Architecture Center | Microsoft Learn https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing 🔶 검색 결과
6 (제목 미기재 — 본문 내 언급 URL) https://developers.google.com/search/docs/crawling-indexing/robots/robots_txt 본문 확인
7 (제목 미기재 — 본문 내 언급 URL) https://docs.python.org/3/library/urllib.robotparser.html 본문 확인
8 (제목 미기재 — 본문 내 언급 URL) https://heritrix.readthedocs.io/en/latest/configuring-jobs.html 본문 확인
9 (제목 미기재 — 본문 내 언급 URL) https://docs.scrapy.org/en/latest/topics/autothrottle.html 본문 확인

A.2 학술 논문·서지 (118개)

# 제목 URL 확인
1 (제목 미기재 — 본문 내 언급 URL) https://www.semanticscholar.org/paper/Mercator:-A-scalable 🔶 검색 결과
2 Mercator: A scalable, extensible Web crawler | World Wide Web | Springer Nature Link https://link.springer.com/article/10.1023/A:1019213109274 🔶 검색 결과
3 Mercator: A scalable, extensible Web crawler: World Wide Web: Vol 2, No 4 https://dl.acm.org/doi/10.1023/A:1019213109274 🔶 검색 결과
4 (PDF) Mercator: A scalable, extensible Web crawler (1999) | Allan Heydon | 690 Citations https://scispace.com/papers/mercator-a-scalable-extensible-web-crawler-3pk6ponb3v 🔶 검색 결과
5 Web Crawling | Foundations and Trends in Information Retrieval | Emerald Publishing https://www.emerald.com/ftinr/article-abstract/4/3/175/1328663/Web-Crawling?redirectedFrom=PDF 본문 확인
6 Google Scholar https://scholar.google.com/scholar_lookup?title=Web+crawling&author=Olston%2C+C.&author=Najork%2C+M.&publication_year=2010&journal=Found.+Trends+Inf.+Retr.&volume=4&pages=175%E2%80%93246&doi=10.1561%2F1500000017 🔶 검색 결과
7 now publishers - Web Crawling https://www.nowpublishers.com/article/Details/INR-017 403 Forbidden
8 (PDF) Web Crawling https://www.researchgate.net/publication/225844302_Web_Crawling 🔶 검색 결과
9 Effective page refresh policies for Web crawlers | ACM Transactions on Database Systems https://dl.acm.org/doi/10.1145/958942.958945 403 Forbidden
10 Effective Page Refresh Policies for Web Crawlers JUNGHOO CHO https://dl.acm.org/doi/pdf/10.1145/958942.958945 🔶 검색 결과
11 [PDF] Effective page refresh policies for Web crawlers | Semantic Scholar https://www.semanticscholar.org/paper/Effective-page-refresh-policies-for-Web-crawlers-Cho-Garcia-Molina/07523653232201428925b335ee5efd67ec54765e ⚠️ 열었으나 본문 파싱 실패
12 (PDF) Effective Page Refresh Policies for Web Crawlers https://www.researchgate.net/publication/200111226_Effective_Page_Refresh_Policies_for_Web_Crawlers 🔶 검색 결과
13 A Dynamic Page-Refresh Index Policy for Web Crawlers | Springer Nature Link https://link.springer.com/chapter/10.1007/978-3-319-08219-6_4 🔶 검색 결과
14 Towards a Quality-Oriented Real-Time Web Crawler | SpringerLink https://link.springer.com/chapter/10.1007/978-3-642-16515-3_10 🔶 검색 결과
15 (PDF) Clustering-based incremental web crawling https://www.academia.edu/115717545/Clustering_based_incremental_web_crawling 🔶 검색 결과
16 Dealing with web data: history and look ahead: Proceedings of the VLDB Endowment: Vol 3, No 1-2 https://dl.acm.org/doi/10.14778/1920841.1920846 🔶 검색 결과
17 A Framework for Incremental Domain-Specific Hidden Web Crawler | SpringerLink https://link.springer.com/chapter/10.1007/978-3-642-14834-7_39 🔶 검색 결과
18 Clustering-based incremental web crawling | ACM Transactions on Information Systems https://dl.acm.org/doi/10.1145/1852102.1852103 🔶 검색 결과
19 Topical web crawlers: Evaluating adaptive algorithms: ACM Transactions on Internet Technology: Vol 4, No 4 https://dl.acm.org/doi/10.1145/1031114.1031117 🔶 검색 결과
20 A Framework for Incremental Deep Web Crawler Based on URL Classification | SpringerLink https://link.springer.com/content/pdf/10.1007/978-3-642-23982-3_37.pdf 🔶 검색 결과
21 The Evolution of the Web and Implications for an Incremental Crawler | Proceedings of the 26th Internation… https://dl.acm.org/doi/10.5555/645926.671679 403 Forbidden
22 [PDF] The Evolution of the Web and Implications for an Incremental Crawler | Semantic Scholar https://www.semanticscholar.org/paper/The-Evolution-of-the-Web-and-Implications-for-an-Cho-Garcia-Molina/ed7087560de484b922e316874a9076376a2a0186 ⚠️ 열었으나 본문 파싱 실패
23 Wrapper maintenance: a machine learning approach: Journal of Artificial Intelligence Research: Vol 18, No 1 https://dl.acm.org/doi/abs/10.5555/1622420.1622425 🔶 검색 결과
24 Populating the Semantic Web https://cdn.aaai.org/Workshops/2004/WS-04-01/WS04-01-006.pdf 🔶 검색 결과
25 Web page DOM node characterization and its application to page segmentation | Proceedings of the 3rd IEEE … https://dl.acm.org/doi/10.5555/1812598.1812659 🔶 검색 결과
26 Structured Data Extraction: Wrapper Generation | Springer Nature Link https://link.springer.com/chapter/10.1007/978-3-642-19460-3_9 🔶 검색 결과
27 Automatic wrappers for large scale web extraction | Proceedings of the VLDB Endowment https://dx.doi.org/10.14778/1938545.1938547 🔶 검색 결과
28 Automatic Wrappers for Large Scale Web Extraction Nilesh Dalvi Yahoo! Research https://www.vldb.org/pvldb/vol4/p219-dalvi.pdf 연결 거부
29 EGA:An Algorithm for Automatic Semi-structured Web Documents Extraction | Springer Nature Link https://link.springer.com/chapter/10.1007/978-3-540-24571-1_69 🔶 검색 결과
30 Wrapper Generation for Automatic Data Extraction from Large Web Sites | Springer Nature Link https://link.springer.com/chapter/10.1007/978-3-540-31970-2_3 🔶 검색 결과
31 AutoScraper: A Progressive Understanding Web Agent for Web Scraper Generation - ADS https://ui.adsabs.harvard.edu/abs/2024arXiv240412753H/abstract 🔶 검색 결과
32 AutoScraper: A Progressive Understanding Web Agent for Web Scraper Generation - ACL Anthology https://aclanthology.org/2024.emnlp-main.141/ 🔶 검색 결과
33 [PDF] AutoScraper: A Progressive Understanding Web Agent for Web Scraper Generation | Semantic Scholar https://www.semanticscholar.org/paper/AutoScraper:-A-Progressive-Understanding-Web-Agent-Huang-Gu/6c076122ea53e18180255bb96c9ad547bb88d283 🔶 검색 결과
34 Detecting Off-Topic Pages in Web Archives | SpringerLink https://link.springer.com/chapter/10.1007/978-3-319-24592-8_17 🔶 검색 결과
35 Web-Based Relation Extraction for the Food Domain | Springer Nature Link https://link.springer.com/chapter/10.1007/978-3-642-31178-9_25 🔶 검색 결과
36 The Wayback Machine: notes on a re-enchantment | Archival Science | Springer Nature Link https://link.springer.com/article/10.1007/s10502-020-09345-w 🔶 검색 결과
37 Collecting Diachronic Affiliation Data for Faculty at HBCUs Using Memento - Zarrillo - 2022 - Proceedings o… https://asistdl.onlinelibrary.wiley.com/doi/10.1002/pra2.664 🔶 검색 결과
38 Exploring a Big Data Approach to Building a List Frame for Urban Agriculture: A Pilot Study in the City of … https://journals.sagepub.com/doi/10.2478/jos-2018-0015?icid=int.sj-abstract.citing-articles.26 🔶 검색 결과
39 RFC 9309: Robots Exclusion Protocol | Guide books https://dl.acm.org/doi/10.17487/RFC9309 🔶 검색 결과
40 Detection of near-duplicate documents https://d3s.mff.cuni.cz/legacy/~holub/sw/shash/ 🔶 검색 결과
41 A Fusion of Algorithms in Near Duplicate Document Detection | Springer Nature Link https://link.springer.com/chapter/10.1007/978-3-642-28320-8_20 🔶 검색 결과
42 Detecting near-duplicates for web crawling | Request PDF https://www.researchgate.net/publication/221022983_Detecting_near-duplicates_for_web_crawling 🔶 검색 결과
43 Probabilistic near-duplicate detection using simhash | Proceedings of the 20th ACM international conferenc… https://dl.acm.org/doi/10.1145/2063576.2063737 🔶 검색 결과
44 (PDF) Detection Of Duplicate And Near-Duplicate Content For Web Crawlers https://www.researchgate.net/publication/326553400_Detection_of_Duplicate_and_Near-Duplicate_Content_for_Web_Crawlers 🔶 검색 결과
45 [PDF] Detecting near-duplicates for web crawling | Semantic Scholar https://semanticscholar.org/paper/Detecting-near-duplicates-for-web-crawling-Manku-Jain/2ef82a2621f237dcdca546658a6a4ea1c69a5a41 🔶 검색 결과
46 Probabilistic Near-Duplicate Detection Using Simhash https://www.researchgate.net/publication/221615307_Probabilistic_Near-Duplicate_Detection_Using_Simhash 🔶 검색 결과
47 Mind2Web: Towards a Generalist Agent for the Web https://papers.nips.cc/paper_files/paper/2023/hash/5950bf290a1570ea401bf98882128160-Abstract-Datasets_and_Benchmarks.html 본문 확인
48 Mind2Web: Towards a Generalist Agent for the Web | OpenReview https://openreview.net/forum?id=kiYqbO3wqw 🔶 검색 결과
49 NeurIPS Poster Mind2Web: Towards a Generalist Agent for the Web https://neurips.cc/virtual/2023/poster/73485 🔶 검색 결과
50 MIND2WEB: Towards a Generalist Agent for the Web Xiang Deng Yu Gu Boyuan Zheng https://proceedings.neurips.cc/paper_files/paper/2023/file/5950bf290a1570ea401bf98882128160-Paper-Datasets_and_Benchmarks.pdf 🔶 검색 결과
51 [PDF] Mind2Web: Towards a Generalist Agent for the Web | Semantic Scholar https://www.semanticscholar.org/paper/Mind2Web:-Towards-a-Generalist-Agent-for-the-Web-Deng-Gu/58f8925a8b87054ad0635a6398a7fe24935b1604 🔶 검색 결과
52 On the Resemblance and Containment of Documents | BibSonomy https://www.bibsonomy.org/bibtex/278b3f3faced79adfcda4e3a57f7e57ff/schmitz 🔶 검색 결과
53 (PDF) On the Resemblance and Containment of Documents https://www.researchgate.net/publication/262333747_On_the_Resemblance_and_Containment_of_Documents 🔶 검색 결과
54 Set similarity search beyond MinHash | Proceedings of the 49th Annual ACM SIGACT Symposium on Theory of Co… https://dl.acm.org/doi/10.1145/3055399.3055443 🔶 검색 결과
55 [PDF] On the resemblance and containment of documents | Semantic Scholar https://www.semanticscholar.org/paper/On-the-resemblance-and-containment-of-documents-Broder/8addb1718c2bc6bbb0d82cd1a57b41198bf65965 ⚠️ 열었으나 본문 파싱 실패
56 On the resemblance and containment of documents https://www.researchgate.net/profile/Andrei-Broder/publication/262333747_On_the_Resemblance_and_Containment_of_Documents/links/02e7e529d69d5de80a000000/On-the-Resemblance-and-Containment-of-Documents.pdf 🔶 검색 결과
57 Web data extraction, applications and techniques | Knowledge-Based Systems https://dl.acm.org/doi/abs/10.1016/j.knosys.2014.07.007 🔶 검색 결과
58 Robust Web Data Extraction: A Novel Approach Based on Minimum Cost Script Edit Model | SpringerLink https://link.springer.com/chapter/10.1007/978-3-642-33469-6_62 🔶 검색 결과
59 Early Steps Toward WebScale Information Extraction with LODIE | AI Magazine https://dl.acm.org/doi/10.1609/aimag.v36i1.2567 🔶 검색 결과
60 Chinese News Data Extraction System Based on Readability Algorithm | SpringerLink https://link.springer.com/chapter/10.1007/978-981-15-8083-3_14 🔶 검색 결과
61 Research on Adaptive Wrapper in Deep Web Data Extraction | Springer Nature Link https://link.springer.com/chapter/10.1007/978-3-319-27293-1_36 🔶 검색 결과
62 Robust web extraction: An approach based on a probabilistic tree-edit model | Request PDF https://www.researchgate.net/publication/221214620_Robust_web_extraction_An_approach_based_on_a_probabilistic_tree-edit_model 🔶 검색 결과
63 DIADEM: Domains to Databases | Springer Nature Link https://link.springer.com/chapter/10.1007/978-3-642-32600-4_1 🔶 검색 결과
64 Visual Template Inference for Data Extraction from Documents | Proceedings of the ACM on Management of Data https://dl.acm.org/doi/abs/10.1145/3769840 🔶 검색 결과
65 dblp: Nilesh N. Dalvi https://dblp2.uni-trier.de/pers/hd/d/Dalvi:Nilesh_N= 🔶 검색 결과
66 Robula+: an algorithm for generating robust XPath locators for web testing - Leotta - 2016 - Journal of Sof… https://onlinelibrary.wiley.com/doi/10.1002/smr.1771 🔶 검색 결과
67 Robula+: An algorithm for generating robust XPath locators for web testing | Request PDF https://www.researchgate.net/publication/299336358_Robula_An_algorithm_for_generating_robust_XPath_locators_for_web_testing 🔶 검색 결과
68 Reducing Web Test Cases Aging by Means of Robust XPath Locators | Request PDF https://www.researchgate.net/publication/266206039_Reducing_Web_Test_Cases_Aging_by_Means_of_Robust_XPath_Locators 🔶 검색 결과
69 [PDF] Robula+: an algorithm for generating robust XPath locators for web testing | Semantic Scholar https://www.semanticscholar.org/paper/Robula+:-an-algorithm-for-generating-robust-XPath-Leotta-Stocco/8d184cf0e7af185f7fe9eedaf66f2f6e8edf4091 🔶 검색 결과
70 Robula+: an algorithm for generating robust XPath locators for web testing: Journal of Software: Evolution … https://dl.acm.org/doi/10.1002/smr.1771 🔶 검색 결과
71 A Hybrid LLM and Supervised Model Pipeline for Polymer Property Extraction from Tables in Scientific Litera… https://aclanthology.org/2025.wasp-main.11/ 🔶 검색 결과
72 (PDF) A Reliability Evaluation of Hybrid Deterministic-LLM Based Approaches for Academic Course Registratio… https://www.researchgate.net/publication/401715527_A_Reliability_Evaluation_of_Hybrid_Deterministic-LLM_Based_Approaches_for_Academic_Course_Registration_PDF_Information_Extraction 🔶 검색 결과
73 (제목 미기재 — 본문 내 언급 URL) http://www.vldb.org/conf/2001/P109.pdf ⚠️ 열었으나 본문 파싱 실패
74 Tractable near-optimal policies for crawling | PNAS https://www.pnas.org/doi/10.1073/pnas.1801519115 🔶 검색 결과
75 Tractable near-optimal policies for crawling - PMC https://pmc.ncbi.nlm.nih.gov/articles/PMC6094111/ 본문 확인
76 (PDF) Tractable near-optimal policies for crawling https://www.researchgate.net/publication/326564976_Tractable_near-optimal_policies_for_crawling 🔶 검색 결과
77 Tractable near-optimal policies for crawling - PubMed https://pubmed.ncbi.nlm.nih.gov/30038026/ 🔶 검색 결과
78 [PDF] Wrapper Induction for Information Extraction | Semantic Scholar https://www.semanticscholar.org/paper/Wrapper-Induction-for-Information-Extraction-Kushmerick-Weld/f9e7402ad740b73cc0bb64178f86df3478c3aaf5 🔶 검색 결과
79 Wrapping Web Information Providers by Transducer Induction | SpringerLink https://link.springer.com/chapter/10.1007/3-540-44795-4_6 🔶 검색 결과
80 The Wrapper Induction Environment Nicholas Kushmerick Dublin City University https://cdn.aaai.org/Workshops/1998/WS-98-10/WS98-10-022.pdf 🔶 검색 결과
81 Regression testing for wrapper maintenance Nicholas Kushmerick https://cdn.aaai.org/AAAI/1999/AAAI99-011.pdf ⚠️ 열었으나 본문 파싱 실패
82 Staying up to Date with Online Content Changes Using Reinforcement Learning for Scheduling https://papers.nips.cc/paper/2019/hash/ad13a2a07ca4b7642959dc0c4c740ab6-Abstract.html 본문 확인
83 Staying up to Date with Online Content Changes Using Reinforcement Learning for Scheduling | OpenReview https://openreview.net/forum?id=ByxjGvuKoE 🔶 검색 결과
84 Processing Web Tables https://hpi.de/naumann/teaching/teaching/ss-19/processing-web-tables.html 🔶 검색 결과
85 WebTables: Exploring the Power of Tables on the Web Michael J. Cafarella http://www.vldb.org/pvldb/vol1/1453916.pdf 🔶 검색 결과
86 Schema Extraction for Tabular Data on the Web Marco D. Adelfio Hanan Samet http://www.vldb.org/pvldb/vol6/p421-adelfio.pdf 🔶 검색 결과
87 WebTables: exploring the power of tables on the web: Proceedings of the VLDB Endowment: Vol 1, No 1 https://dl.acm.org/doi/10.14778/1453856.1453916 🔶 검색 결과
88 (PDF) WebTables: Exploring the power of tables on the web https://www.researchgate.net/publication/220538521_WebTables_Exploring_the_power_of_tables_on_the_web 🔶 검색 결과
89 Identifying Web Tables: Supporting a Neglected Type of Content on the Web | Springer Nature Link https://link.springer.com/chapter/10.1007/978-3-319-24543-0_4 🔶 검색 결과
90 (제목 미기재 — 본문 내 언급 URL) https://api.semanticscholar.org/graph/v1/paper/07523653232201428925b335ee5efd67ec54765e?fields=title 🔶 검색 결과
91 (제목 미기재 — 본문 내 언급 URL) https://api.semanticscholar.org/graph/v1/paper/ed7087560de484b922e316874a9076376a2a0186?fields=title 🔶 검색 결과
92 (제목 미기재 — 본문 내 언급 URL) https://api.semanticscholar.org/graph/v1/paper/8addb1718c2bc6bbb0d82cd1a57b41198bf65965?fields=title 🔶 검색 결과
93 (제목 미기재 — 본문 내 언급 URL) https://api.semanticscholar.org/graph/v1/paper/f9e7402ad740b73cc0bb64178f86df3478c3aaf5?fields=title 🔶 검색 결과
94 (제목 미기재 — 본문 내 언급 URL) https://api.semanticscholar.org/graph/v1/paper/search?query=RoadRunner+Towards+Automatic+Data+Extraction+from+Large+Web+Sites&fields=title 🔶 검색 결과
95 (제목 미기재 — 본문 내 언급 URL) https://api.semanticscholar.org/graph/v1/paper/search?query=An+Introduction+to+Heritrix+open+source+archival+quality+web+crawler&fields=title 🔶 검색 결과
96 (제목 미기재 — 본문 내 언급 URL) https://api.semanticscholar.org/graph/v1/paper/search?query=Similarity+estimation+techniques+from+rounding+algorithms+Charikar&fields=title 🔶 검색 결과
97 (제목 미기재 — 본문 내 언급 URL) https://api.semanticscholar.org/graph/v1/paper/8d184cf0e7af185f7fe9eedaf66f2f6e8edf4091?fields=title 🔶 검색 결과
98 (PDF) Automatic wrappers for large scale web extraction https://www.academia.edu/89369081/Automatic_wrappers_for_large_scale_web_extraction 🔶 검색 결과
99 From one tree to a forest: a unified solution for structured web data extraction. | Request PDF https://www.researchgate.net/publication/221299838_From_one_tree_to_a_forest_a_unified_solution_for_structured_web_data_extraction 🔶 검색 결과
100 ZeroShotCeres: Zero-Shot Relation Extraction from Semi-Structured Webpages | Request PDF https://www.researchgate.net/publication/343297191_ZeroShotCeres_Zero-Shot_Relation_Extraction_from_Semi-Structured_Webpages 🔶 검색 결과
101 Automatic Wrappers for Large Scale Web Extraction | Request PDF https://www.researchgate.net/publication/50367565_Automatic_Wrappers_for_Large_Scale_Web_Extraction 🔶 검색 결과
102 Automatic XPath generation agents for vertical websites by LLMs | Journal of King Saud University Computer… https://link.springer.com/article/10.1007/s44443-025-00071-w ↪ 리다이렉트(재요청 필요)
103 AutoScraper: A Progressive Understanding Web Agent for Web Scraper Generation | alphaXiv https://www.alphaxiv.org/abs/2404.12753 🔶 검색 결과
104 (PDF) Wrapper Maintenance https://www.researchgate.net/publication/242026909_Wrapper_Maintenance 🔶 검색 결과
105 Wrapper Maintenance | SpringerLink https://link.springer.com/rwe/10.1007/978-1-4614-8265-9_1158 🔶 검색 결과
106 (PDF) Accurately and Reliably Extracting Data from the Web: A Machine Learning Approach https://www.researchgate.net/publication/220282644_Accurately_and_Reliably_Extracting_Data_from_the_Web_A_Machine_Learning_Approach 🔶 검색 결과
107 Wrapper Maintenance | Springer Nature Link https://link.springer.com/referenceworkentry/10.1007/978-0-387-39940-9_1158 🔶 검색 결과
108 Schema-guided wrapper maintenance for web-data extraction | Proceedings of the 5th ACM international works… https://dl.acm.org/doi/abs/10.1145/956699.956701 🔶 검색 결과
109 Intelligent Self-repairable Web Wrappers | SpringerLink https://link.springer.com/chapter/10.1007/978-3-642-23954-0_26 🔶 검색 결과
110 (제목 미기재 — 본문 내 언급 URL) https://doi.org/10.1561/1500000017 🔶 검색 결과
111 (제목 미기재 — 본문 내 언급 URL) https://doi.org/10.1002/smr.1771 🔶 검색 결과
112 (제목 미기재 — 본문 내 언급 URL) https://idp.springer.com/authorize?response_type=cookie&client_id=springerlink&redirect_uri=https%3A%2F%2Flink.springer.com%2Farticle%2F10.1007%2Fs44443-025-00071-w ↪ 리다이렉트(재요청 필요)
113 (제목 미기재 — 본문 내 언급 URL) https://api.semanticscholar.org/graph/v1/paper/DOI:10.1145/958942.958945?fields=title 🔶 검색 결과
114 (제목 미기재 — 본문 내 언급 URL) https://idp.springer.com/transit?redirect_uri=https%3A%2F%2Flink.springer.com%2Farticle%2F10.1007%2Fs44443-025-00071-w&code=52df85a8-32c8-4d47-beaf-5cbd0fa0a5fe 🔶 검색 결과
115 (제목 미기재 — 본문 내 언급 URL) https://link.springer.com/article/10.1007/s44443-025-00071-w?error=cookies_not_supported&code=52df85a8-32c8-4d47-beaf-5cbd0fa0a5fe 본문 확인
116 (제목 미기재 — 본문 내 언급 URL) https://api.semanticscholar.org/graph/v1/paper/search?query=Introduction+to+Heritrix+Mohr+Stack+Ranitovic+Avery+Kimpton&fields=title 🔶 검색 결과
117 (제목 미기재 — 본문 내 언급 URL) https://api.semanticscholar.org/graph/v1/paper/search?query=The+Evolution+of+the+Web+and+Implications+for+an+Incremental+Crawler&fields=title 🔶 검색 결과
118 (제목 미기재 — 본문 내 언급 URL) https://api.semanticscholar.org/graph/v1/paper/search?query=Introduction+to+Heritrix+archival+quality+web+crawler&fields=title 🔶 검색 결과

A.3 arXiv (45개)

# 제목 URL 확인
1 A Brief History of Web Crawlers https://arxiv.org/pdf/1405.0749 🔶 검색 결과
2 iCrawl: Improving the Freshness of Web Collections by Integrating Social Web and Focused Web Crawling https://arxiv.org/pdf/1612.06202 🔶 검색 결과
3 A Scalable Crawling Algorithm Utilizing Noisy Change-Indicating Signals https://arxiv.org/html/2502.02430 🔶 검색 결과
4 Age of Information for Updates with Distortion: Constant and Age-Dependent Distortion Constraints https://arxiv.org/pdf/1912.13493 🔶 검색 결과
5 Multiple-Goal Heuristic Search https://arxiv.org/pdf/1109.6618 🔶 검색 결과
6 Management Of Volatile Information In Incremental Web Crawler https://arxiv.org/pdf/0910.1869 🔶 검색 결과
7 [2404.12753] AutoScraper: A Progressive Understanding Web Agent for Web Scraper Generation https://arxiv.org/abs/2404.12753 본문 확인
8 Computation and Language Apr 2024 https://arxiv.org/list/cs.CL/2024-04?skip=825&show=250 🔶 검색 결과
9 The AI Committee: A Multi-Agent Framework for Automated Validation and Remediation of Web-Sourced Data https://arxiv.org/pdf/2512.21481 🔶 검색 결과
10 Co-Scraper: query-aware DOM Pruning and Reusable Scraper Synthesis for Lightweight Web Data Extraction https://arxiv.org/pdf/2606.14821 🔶 검색 결과
11 AutoScraper: A Progressive Understanding Web Agent for Web Scraper Generation https://arxiv.org/pdf/2404.12753 🔶 검색 결과
12 A Framework for Evaluation of Composite Memento Temporal Coherence https://arxiv.org/pdf/1402.0928 🔶 검색 결과
13 Adapting the Hypercube Model to Archive Deferred Representations and Their Descendants https://arxiv.org/pdf/1601.05142 🔶 검색 결과
14 arXiv:2504.01382v2 [cs.AI] 11 May 2025 https://arxiv.org/pdf/2504.01382 🔶 검색 결과
15 OpenWebVoyager: Building Multimodal Web Agents via Iterative Real-World Exploration, Feedback and Optimization https://arxiv.org/pdf/2410.19609 🔶 검색 결과
16 (제목 미기재 — 본문 내 언급 URL) https://arxiv.org/abs/2606.14821 본문 확인
17 (제목 미기재 — 본문 내 언급 URL) https://arxiv.org/abs/2401.13919 본문 확인
18 (제목 미기재 — 본문 내 언급 URL) https://arxiv.org/abs/2512.21481 본문 확인
19 C-MinHash: Practically Reducing Two Permutations to Just One https://arxiv.org/pdf/2109.04595 🔶 검색 결과
20 C-MinHash: Rigorously Reducing K Permutations to Two https://arxiv.org/pdf/2109.03337 🔶 검색 결과
21 b-Bit Minwise Hashing https://arxiv.org/pdf/0910.3349 🔶 검색 결과
22 Exact Weighted Minwise Hashing in Constant Time https://arxiv.org/pdf/1602.08393 🔶 검색 결과
23 HyperMinHash: MinHash in LogLog space https://arxiv.org/pdf/1710.08436 🔶 검색 결과
24 Erratum: Leveraging Flexible Tree Matching to Repair Broken Locators in Web Automation Scripts https://arxiv.org/pdf/2106.04916 🔶 검색 결과
25 DELM: a Python toolkit for Data Extraction with Language Models https://arxiv.org/pdf/2509.20617 🔶 검색 결과
26 Prompt2DAG: A Modular Methodology for LLM-Based Data Enrichment Pipeline Generation https://arxiv.org/html/2509.13487v1 🔶 검색 결과
27 RATE: An LLM-Powered Retrieval Augmented Generation Technology-Extraction Pipeline https://arxiv.org/html/2507.21125v1 🔶 검색 결과
28 AXE: Low-Cost Cross-Domain Web Structured Information Extraction https://arxiv.org/html/2602.01838 🔶 검색 결과
29 Tabular PDF Information Extraction with Local LLMs and Layout-Aware Parsing: A Reliability Evaluation https://arxiv.org/pdf/2604.00003 🔶 검색 결과
30 (제목 미기재 — 본문 내 언급 URL) https://arxiv.org/abs/2502.02430 본문 확인
31 (제목 미기재 — 본문 내 언급 URL) https://arxiv.org/abs/2602.01838 본문 확인
32 Learning to Crawl https://arxiv.org/pdf/1905.12781 🔶 검색 결과
33 Automatic Wrappers for Large Scale Web Extraction Nilesh Dalvi Yahoo! Research https://arxiv.org/pdf/1103.2406 🔶 검색 결과
34 A Scalable Crawling Algorithm Utilizing Noisy Change-Indicating Signals https://arxiv.org/pdf/2502.02430 🔶 검색 결과
35 Online Learning for Active Cache Synchronization https://arxiv.org/pdf/2002.12014 🔶 검색 결과
36 Look back, look around: a systematic analysis of effective predictors for new outlinks in focused Web crawling https://arxiv.org/pdf/2111.05062 🔶 검색 결과
37 On Extracting Data from Tables that are Encoded using HTML https://arxiv.org/pdf/1903.08305 🔶 검색 결과
38 Identifying Web Tables - Supporting a Neglected Type of Content on the Web https://arxiv.org/pdf/1503.06598 🔶 검색 결과
39 An Annotated Corpus of Webtables for Information Extraction Tasks https://arxiv.org/pdf/2008.07680 🔶 검색 결과
40 WEB TABLE EXTRACTION, RETRIEVAL AND AUGMENTATION: A SURVEY A PREPRINT https://arxiv.org/pdf/2002.00207 🔶 검색 결과
41 (제목 미기재 — 본문 내 언급 URL) https://arxiv.org/abs/1103.2406 본문 확인
42 AXE: Low-Cost Cross-Domain Web Structured Information Extraction https://arxiv.org/pdf/2602.01838 🔶 검색 결과
43 Wrapper Maintenance: A Machine Learning Approach https://arxiv.org/pdf/1106.4872 🔶 검색 결과
44 (제목 미기재 — 본문 내 언급 URL) https://arxiv.org/abs/1405.0749 본문 확인
45 (제목 미기재 — 본문 내 언급 URL) https://arxiv.org/abs/1106.4872 본문 확인

A.4 저자·기관 페이지 / PDF 원문 (17개)

# 제목 URL 확인
1 Mercator: A Scalable, Extensible Web Crawler Allan Heydon and Marc Najork https://courses.cs.washington.edu/courses/cse454/15wi/papers/mercator.pdf 본문 확인
2 Mercator: A Scalable, Extensible Web Crawler https://research.google/pubs/mercator-a-scalable-extensible-web-crawler/ 🔶 검색 결과
3 September 26, 2001 SRC Research Report 173 High-Performance Web Crawling https://www.cs.cornell.edu/courses/cs685/2002fa/mercator.pdf 본문 확인
4 Mercator: A Scalable, Extensible Web Crawler Allan Heydon and Marc Najork http://www.cs.ucr.edu/~vagelis/classes/CS242/publications/scalable-crawler.pdf 🔶 검색 결과
5 Foundations and Trends R ⃝in Information Retrieval Vol. 4, No. 3 (2010) 175246 http://i.stanford.edu/~olston/publications/crawling_survey.pdf 연결 거부
6 Web Crawling https://research.google/pubs/web-crawling/ 본문 확인
7 Effective Page Refresh Policies For Web Crawlers JUNGHOO CHO http://oak.cs.ucla.edu/~cho/papers/cho-tods03.pdf 연결 거부
8 Site-Wide Wrapper Induction for Life Science Deep Web Databases | SpringerLink http://link-springer-com-443.webvpn.fjmu.edu.cn/chapter/10.1007/978-3-642-02879-3_9 🔶 검색 결과
9 An Introduction To Heritrix Gordon Mohr Chief Technologist, Web Projects https://docs.huihoo.com/heritrix/An-Introduction-To-Heritrix.ppt 🔶 검색 결과
10 Detecting Near-Duplicates for Web Crawling Gurmeet Singh Manku Google Inc. https://research.google.com/pubs/archive/33026.pdf ↪ 리다이렉트(재요청 필요)
11 Mind2Web: Towards a Generalist Agent for the Web https://osu-nlp-group.github.io/Mind2Web/ 본문 확인
12 (제목 미기재 — 본문 내 언급 URL) https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/33026.pdf ⚠️ 열었으나 본문 파싱 실패
13 Eric Horvitz: Publications http://erichorvitz.com/abstracts.htm 🔶 검색 결과
14 Eric Horvitz: Publications by topic area https://www.erichorvitz.com/abstracts_by_topic.htm 🔶 검색 결과
15 Eric Horvitz: Selected publications by topic area https://erichorvitz.com/selected_refs_by_topic.htm 🔶 검색 결과
16 COMPUTER SCIENCES Tractable near-optimal policies for crawling https://erichorvitz.com/Crawl_1801519115.full.pdf 🔶 검색 결과
17 Publications by Daniel S. Weld https://homes.cs.washington.edu/~weld/pubs.html 본문 확인

A.5 코드 저장소 (9개)

# 제목 URL 확인
1 Users of Heritrix https://github.com/internetarchive/heritrix3/wiki/Users-of-Heritrix 🔶 검색 결과
2 GitHub - chanhee-kang/DBpia_crawler: 국내 논문 서지정보 사이트 DBpia 크롤링 프로그램 https://github.com/chanhee-kang/DBpia_crawler 🔶 검색 결과
3 GitHub - scrapinghub/python-simhash: An efficient simhash implementation for python · GitHub https://github.com/scrapinghub/python-simhash 본문 확인
4 GitHub - OSU-NLP-Group/Mind2Web: [NeurIPS'23 Spotlight] "Mind2Web: Towards a Generalist Agent for the Web" … https://github.com/OSU-NLP-Group/Mind2Web 🔶 검색 결과
5 (제목 미기재 — 본문 내 언급 URL) https://github.com/EZ-hwh/AutoScraper 본문 확인
6 GitHub - cyluxx/robula-plus: An algorithm for generating robust XPath locators for web testing. · GitHub https://github.com/cyluxx/robula-plus 본문 확인
7 GitHub - ZeusFSX/robula-plus: An algorithm for generating robust XPath locators for web testing. Python ver… https://github.com/ZeusFSX/robula-plus 🔶 검색 결과
8 (제목 미기재 — 본문 내 언급 URL) https://github.com/internetarchive/heritrix3 본문 확인
9 My notes on the talk "The Many Meanings of Event-Driven Architecture" by Martin Fowler (GOTO 2017) · GitHub https://gist.github.com/xpepper/36beda855540b0c1dde6c4c417dafec9 🔶 검색 결과

A.6 국내 학술(KCI/DBpia/RISS/ScienceON) (16개)

# 제목 URL 확인
1 환자 맞춤형 의약품 투약량 적정성 제공 시스템 - 김정훈 - 경희대학교 : 논문 - DBpia https://www.dbpia.co.kr/journal/detail?nodeId=T15064378 🔶 검색 결과
2 DBpia - 국내 논문, 학술지, 잡지까지 제공하는 학술 AI 플랫폼 https://www.dbpia.co.kr/ 🔶 검색 결과
3 의약품 안전관리를 위한 빅데이터의 활용 - 대한의사협회지 - 대한의사협회 : 논문 - DBpia https://www.dbpia.co.kr/journal/articleDetail?nodeId=NODE10887640 🔶 검색 결과
4 의약품 이력추적 관리 탐정 - 전기의세계 - 대한전기학회 : 논문 - DBpia https://www.dbpia.co.kr/journal/articleDetail?nodeId=NODE02482264 🔶 검색 결과
5 웹 사이트 컨텐츠 변경 모니터링 시스템 - of DSpace - KCI https://dspace.kci.go.kr/handle/kci/355955 🔶 검색 결과
6 [논문]실시간 웹 크롤링 분산 모니터링 시스템 설계 및 구현 https://scienceon.kisti.re.kr/srch/selectPORSrchArticle.do?cn=JAKO201909258120005 본문 확인
7 웹 사이트 컨텐츠 변경 모니터링 시스템 https://www.kci.go.kr/kciportal/ci/sereArticleSearch/ciSereArtiView.kci?sereArticleSearchBean.artiId=ART000881131 본문 확인
8 한국학술지인용색인(Korea Citation Index) https://www.kci.go.kr/kciportal/main.kci 🔶 검색 결과
9 [논문]동적인 URL 수집 정책을 이용한 분산 웹 크롤링 시스템 https://scienceon.kisti.re.kr/srch/selectPORSrchArticle.do?cn=DIKO0014012605 🔶 검색 결과
10 KCI 문헌 유사도 검사 서비스 https://check.kci.go.kr/ 🔶 검색 결과
11 KCI 국내 학술지 인용색인 정보 포털입니다. https://www.kci.go.kr/ 🔶 검색 결과
12 실시간 웹 게시판 모니터링 및 모바일웹을 이용한 알람 서비스 개발 https://www.kci.go.kr/kciportal/ci/sereArticleSearch/ciSereArtiView.kci?sereArticleSearchBean.artiId=ART001648031 본문 확인
13 RISS(리스,학술연구정보서비스) - 국내·국외 학술정보를 제공하는 대국민 서비스 https://www.riss.kr/index.do 🔶 검색 결과
14 나라장터 입찰공고 인공지능 검색모델 개발에 관한 연구 https://www.kci.go.kr/kciportal/ci/sereArticleSearch/ciSereArtiView.kci?sereArticleSearchBean.artiId=ART002883147 본문 확인
15 KCI 국내학술지 인용색인 정보 포털입니다. https://www.kci.go.kr/kciportal/po/search/poArtiSearList.kci?sereId=000614 🔶 검색 결과
16 2024년도 학술지평가 결과 공고 https://www.kci.go.kr/kciportal/mobile/bbs/bbsNoticeView.kci?boardBean.bullScriSequ=000000039930 🔶 검색 결과

A.7 국내 데이터 출처 (9개)

# 제목 URL 확인
1 https://nedrug.mfds.go.kr/index https://nedrug.mfds.go.kr/index 🔶 검색 결과
2 의약품안전나라 > 의약품등 검색 - 식품의약품안전처 https://nedrug.mfds.go.kr/searchDrug 🔶 검색 결과
3 약품/식품정보 | 국가건강정보포털 | 질병관리청 https://health.kdca.go.kr/healthinfo/biz/health/gnrlzHealthInfo/healthInfo/medcinFoodInfoMain.do 🔶 검색 결과
4 약학정보원 - 대한민국 의약품정보의 표준 https://health.kr/ 🔶 검색 결과
5 https:/nedrug.mfds.go.kr - 식품의약품안전처 https://nedrug.mfds.go.kr/ 🔶 검색 결과
6 식의약 데이터 포털 https://data.mfds.go.kr/ 🔶 검색 결과
7 식품의약품안전평가원 https://www.nifds.go.kr/ 🔶 검색 결과
8 의약품안전나라 > 고객지원 > 통합자료실 > eCTD민원서식작성기 https://nedrug.mfds.go.kr/bbs/43 🔶 검색 결과
9 국가철도공단 KR전자조달시스템 https://ebid.kr.or.kr/ 🔶 검색 결과

A.8 특허 (5개)

# 제목 URL 확인
1 KR101757822B1 - 웹 크롤링 모니터링 시스템 및 방법 - Google Patents https://patents.google.com/patent/KR101757822B1/ko 🔶 검색 결과
2 Robust wrappers for web extraction https://image-ppubs.uspto.gov/dirsearch-public/print/downloadPdf/8762829 🔶 검색 결과
3 KR101213930B1 - 결정 이론 웹 크롤링, 및 웹 페이지 변경의 예측 - Google Patents https://patents.google.com/patent/KR101213930B1/ko 🔶 검색 결과
4 Visual and interactive wrapper generation, automated information extraction from Web pages, and translation… https://image-ppubs.uspto.gov/dirsearch-public/print/downloadPdf/7581170 🔶 검색 결과
5 METHOD AND APPARATUS FOR ELECTRONICALLY EXTRACTING APPLICATION SPECIFIC MULTIDIMENSIONAL INFORMATION FROM D… https://image-ppubs.uspto.gov/dirsearch-public/print/downloadPdf/6965900 🔶 검색 결과

A.9 위키백과 (6개)

# 제목 URL 확인
1 Moses Charikar https://en.wikipedia.org/wiki/Moses_Charikar 🔶 검색 결과
2 (제목 미기재 — 본문 내 언급 URL) https://en.wikipedia.org/wiki/Wrapper_(data_mining 🔶 검색 결과
3 Korea Citation Index https://en.wikipedia.org/wiki/Korea_Citation_Index 🔶 검색 결과
4 (제목 미기재 — 본문 내 언급 URL) https://en.wikipedia.org/wiki/Web_crawler 본문 확인
5 (제목 미기재 — 본문 내 언급 URL) https://en.wikipedia.org/wiki/SimHash 본문 확인
6 (제목 미기재 — 본문 내 언급 URL) https://en.wikipedia.org/wiki/MinHash 본문 확인

A.10 실무 블로그·기타 (27개)

# 제목 URL 확인
1 Web Crawling (Foundations and Trends(r) in Information Retrieval) - Olston, Christopher; Najork, Marc: 9781… https://www.abebooks.com/9781601983220/Web-Crawling-Foundations-Trendsr-Information-1601983220/plp 🔶 검색 결과
2 Amazon.com: Web Crawling (Foundations and Trends(r) in Information Retrieval): 9781601983220: Olston, Chris… https://www.amazon.com/Crawling-Foundations-Trends-Information-Retrieval/dp/1601983220 🔶 검색 결과
3 How Attackers Exploit robots.txt? | Baeldung on Computer Science https://www.baeldung.com/cs/robots-txt-risk-threat 🔶 검색 결과
4 Respecting Robots Exclusion Protocol or robots.txt at Scale | by Rashad Moarref | GumGum Tech Blog | Medium https://medium.com/gumgum-tech/respecting-robots-exclusion-protocol-or-robots-txt-at-scale-60ee57dc1295 🔶 검색 결과
5 RFC 9309: Robots.txt Is Now an Official IETF Internet Standard (Robots Exclusion Protocol) https://www.searchengineworld.com/rfc9309-robots-txt-quietly-became-an-official-internet-standard 🔶 검색 결과
6 RFC 9309 — Robots Exclusion Protocol — status, mechanics & checks — AgentGrade https://agentgrade.com/standards/rfc-9309 🔶 검색 결과
7 How to Fix Web Scraping Errors: 2026 Complete Troubleshooting Guide https://www.promptcloud.com/blog/how-to-fix-web-scraping-errors-2026/ 🔶 검색 결과
8 Web Scraping With XPath and CSS Selectors: which selector to reach for, and when https://crawlbase.com/blog/web-scraping-with-xpath-and-css-selectors/ 🔶 검색 결과
9 When the Scraper Breaks Itself: Building a Self-Healing CSS Selector Repair System - DEV Community https://dev.to/viniciuspuerto/when-the-scraper-breaks-itself-building-a-self-healing-css-selector-repair-system-312d 🔶 검색 결과
10 Managing Change in Web Scraping: 10 Critical Challenges https://www.promptcloud.com/blog/managing-change-in-web-scraping-10-challenges/ 🔶 검색 결과
11 What are CSS selectors and XPath in web extraction? | Firecrawl Glossary https://www.firecrawl.dev/glossary/web-extraction-apis/what-are-css-selectors-xpath-web-extraction 🔶 검색 결과
12 What is an xpath selector in web scraping? | Firecrawl Glossary https://www.firecrawl.dev/glossary/web-scraping-apis/what-is-xpath-selector-in-web-scraping 🔶 검색 결과
13 Robula+: An algorithm for generating robust XPath locators for web testing - Technical University of Munich https://portal.fis.tum.de/en/publications/robula-an-algorithm-for-generating-robust-xpath-locators-for-web- 본문 확인
14 Reinforcement Learning Papers Accepted to NeurIPS 2019 | endtoend.ai https://www.endtoend.ai/explore/neurips2019-rl/ 🔶 검색 결과
15 NeurIPS 2019 https://nips.cc/Conferences/2019/ScheduleMultitrack?event=14033 🔶 검색 결과
16 NeurIPS 2020 https://nips.cc/Conferences/2020/ScheduleMultitrack?event=17616 🔶 검색 결과
17 Event Sourcing | arc42 Quality Model https://quality.arc42.org/approaches/event-sourcing 🔶 검색 결과
18 Event Sourcing https://martinfowler.com/eaaDev/EventSourcing.html 본문 확인
19 Change Data Capture: From Batch to Real-Time | Capital One https://www.capitalone.com/tech/software-engineering/batch-to-real-time-with-change-data-capture/ 🔶 검색 결과
20 The Many Meanings of Event-Driven Architecture - Martin Fowler (GOTO 2017) - HackMD https://hackmd.io/@pierodibello/The-Many-Meanings-of-Event-Driven-Architecture 🔶 검색 결과
21 Understanding Event Sourcing: Key Principles and Benefits https://www.baytechconsulting.com/blog/event-sourcing-explained-2025 🔶 검색 결과
22 Event Sourcing Explained: Capturing State Changes at Scale - DevX https://www.devx.com/technology/event-sourcing-explained-capturing-state-changes-at-scale/ 🔶 검색 결과
23 event sourcing https://www.gatlin.io/content/event-sourcing 🔶 검색 결과
24 나라**(국가종합전자조달시스템) 입찰공고 크롤링, 크롤링·스크래핑 포트폴리오 - 크몽 https://kmong.com/portfolio/view/171486 🔶 검색 결과
25 나라장터 발주/입찰 공고 검색/알림 자체시스템구축 - 크몽 https://kmong.com/gig/613778 🔶 검색 결과
26 (제목 미기재 — 본문 내 언급 URL) http://www.research.compaq.com/SRC/Compaq 🔶 검색 결과
27 (제목 미기재 — 본문 내 언급 URL) https://www.kimballgroup.com/data-warehouse-business-intelligence-resources/kimball-techniques/dimensional-modeling-techniques/type-2/ 본문 확인

A.11 URL 추출 시 주의 사항

raw dump 에서 기계적으로 추출한 URL 중 다음 2개는 원문에서 잘리거나 PDF 텍스트 추출 과정에서 손상된 것이다. 삭제하지 않고 그대로 남기되 사용 시 주의한다.

손상된 URL 원래 형태(추정) 비고
https://www.semanticscholar.org/paper/Mercator:-A-scalable https://www.semanticscholar.org/paper/Mercator:-A-scalable,-extensible-Web-crawler-Heydon-Najork/10e3137023969b3cf74b3488d5f5a29e7dd5bd80 원문 검색 결과에 쉼표가 포함돼 있어 추출 시 잘림. 전체 형태는 raw dump 의 [SEARCH #1] 블록에 있음
http://www.research.compaq.com/SRC/Compaq http://www.research.compaq.com/SRC/ SRC Research Report 173 PDF 본문의 줄바꿈 없는 텍스트에서 추출됨. 도메인은 현재 존재하지 않음(Compaq → HP 흡수)

또한 raw dump 의 [CMD] 블록에 텍스트가 포함된 크롤링 관련 대법원 판결 뉴스레터 PDF(2022-06-20, 법무법인 세종 IP그룹으로 추정)원본 URL 이 raw dump 에 기록되어 있지 않다. §9.3 의 내용은 이 PDF 본문에서 직접 추출한 것이며, 출처 URL 미확인 상태다(부록 B 항목).


부록 B. 미해결 질문 / 실측 필요 항목

B.1 대상 사이트 실측 (최우선 — 이것 없이는 코드를 완성할 수 없다)

  • nedrug.mfds.go.kr/robots.txt 를 실제로 취득해 내용을 확인한다. Disallow 경로에 게시판이 포함되는가? Crawl-delay 가 명시되어 있는가? Sitemap 이 있는가?
  • DMF(원료의약품 등록) 공고 게시판의 정확한 URL 을 확정한다. https://nedrug.mfds.go.kr/bbs/{번호} 형태로 보이나(예: /bbs/43 = eCTD 민원서식작성기 자료실), DMF 공고의 번호를 확인하지 못했다.
  • DMF 등록 현황(목록) 페이지의 URL 과 페이징 파라미터를 확정한다. GET 쿼리인가, POST 인가? POST 라면 크롤 전략이 달라진다.
  • 목록이 정적 HTML 인가 JavaScript 렌더링 인가? 후자면 Playwright 등이 필요하고 politeness/비용 계산이 달라진다.
  • DMF 등록번호가 존재하는가? 존재한다면 정확한 형식(정규식)은? — §10.6 의 1순위 키 확정에 필수
  • 목록 테이블의 열 구성과 헤더 텍스트 실제 값 (등록번호 / 주성분명 / 업체명 / 등록일 / 상태 의 실제 명칭)
  • 상태 필드의 실제 값 집합 (등록/변경/취하/취소/말소 중 무엇이 쓰이는가? 상태 열 자체가 없을 수도 있다)
  • 페이지당 행 수(→ min_expected / max_expected 확정)와 총 페이지 수
  • "취하"가 상태 변경으로 표현되는가, 목록에서 사라지는 것으로 표현되는가? — diff 엔진의 WITHDRAWN 판정 로직이 이에 따라 달라진다
  • 응답 헤더에 ETag / Last-Modified 가 실제로 오는가? 매 요청마다 바뀌지는 않는가?
  • 총 게시물 수 표시가 목록 페이지에 있는가? (§4.6 의 noisy signal 1순위)
  • RSS/Atom 피드가 제공되는가?
  • 사이트 이용약관에 크롤링 금지 문구가 있는가? (§9.3 안전선 확인)

B.2 API 대안 조사 (크롤링보다 우선 검토)

  • 식의약 데이터 포털(https://data.mfds.go.kr/) 에 DMF 관련 OpenAPI 가 있는가? 있다면 크롤링 대신 API 를 1순위 소스로 전환한다. (raw dump 확인: "허가 의약품·의료기기 정보를 XML/JSON 등 다양한 데이터 형식으로 제공")
  • 공공데이터포털(data.go.kr)에 DMF 데이터셋이 등록되어 있는가?
  • API 가 있다면 갱신 주기·인증 방식·호출 제한(rate limit)은?
  • API 와 웹 게시판의 데이터가 일치하는가? (불일치 시 웹이 진실, API 는 지연될 수 있음)

B.3 논문 원문 미확인 항목 (인용 정확성)

  • Cho & Garcia-Molina (TODS 2003) 원문 PDF 확보 — oak.cs.ucla.edu ECONNREFUSED, dl.acm.org 403. §4.2 의 freshness/age 수식은 표준 Poisson 유도로 재구성한 것이며 원문 표기와 다를 수 있다. 원문의 uniform vs proportional 정리 조건과 change frequency 추정기(§4.4 의 λ̂ 유도가 논문의 추정기와 같은지)를 확인해야 한다.
  • Cho & Garcia-Molina (VLDB 2000) 원문 확보 — ACM 403. 페이지 변경률 실측치, 페이지 half-life 수치, 증분 크롤러 아키텍처 상세를 확인하지 못했다.
  • Olston & Najork (FnTIR 2010) 전문 확보 — i.stanford.edu ECONNREFUSED, nowpublishers.com 403. 목차와 재방문 정책 절의 상세를 확인하지 못했다(초록만 확인).
  • Charikar (STOC 2002) 원문 확보 — Semantic Scholar API 429. SimHash 의 원 정의와 가중치 부여 방식을 Wikipedia 경유로만 확인했다.
  • Broder (1997) 원문 확보 — Semantic Scholar 빈 페이지. shingling 크기, sketch 크기 등 파라미터를 확인하지 못했다.
  • Mercator 원논문(WWW journal 1999) 확보 — UW 강의 사이트는 로그인 페이지 반환. SRC Research Report 173(2001)으로 대체 확인했으나 1999 논문 자체의 표기는 미확인.
  • Heritrix 원논문(IWAW'04) 확보 — 검색 결과로만 확인. 저자 표기(Mohr, Stack, Ranitovic, Avery, Kimpton)와 페이지(109115)를 2차 출처로만 확인했다.
  • Dalvi, Bohannon, Sha (SIGMOD 2009) 저자 확정 — raw dump 의 검색 결과가 Dalvi/Kumar/Soliman 과 혼동해 서술한다. 실제 SIGMOD 2009 논문의 저자를 확인해야 한다.
  • Kushmerick, Weld, DoorenbosArtificial Intelligence 저널 버전(2000?) 존재 여부 — Weld 개인 출판 목록에서는 IJCAI-97 항목만 확인됐다.
  • Mind2Web MindAct 의 후보 랭킹 메커니즘과 cross-task/cross-website/cross-domain 분할 수치 — 공식 페이지에서 확인 실패. arXiv:2306.06070 또는 GitHub 확인 필요.
  • Kolobov et al. (NeurIPS 2019) 의 harmonic policy 정의와 complete vs incomplete observability 구분 — 초록 수준까지만 확인.
  • RAPTURE(AAAI-99) 의 정확한 특징 목록과 Gaussian 모델 결합 방식 — PDF 앞 2페이지만 텍스트 추출했다. §5.6 구현은 검색 결과의 서술("단어 수, 평균 단어 길이, 타입 밀도 / 평균·분산 / 개별 확률 결합")에 기반하므로 원문과 세부가 다를 수 있다.

B.4 국내 문헌 추가 조사 (WebSearch 예산 소진으로 미수행)

  • DBpia / RISS / KCI 에서 "의약품안전나라 크롤링", "식약처 공고 자동 수집", "DMF 원료의약품 등록 데이터" 직접 검색
  • Lerman, Minton, Knoblock "Wrapper maintenance: a machine learning approach" JAIR 2003 의 wrapper reinduction 알고리즘 상세 (arXiv:1106.4872 초록까지만 확인)
  • Kimball SCD Type 2 의 Type 1 / Type 3 / Type 6 과의 비교 — 우리가 Type 2 를 고른 것이 최적인지 재확인
  • 특허 KR101757822B1(웹 크롤링 모니터링 시스템 및 방법), KR101213930B1(결정 이론 웹 크롤링, 및 웹 페이지 변경의 예측)의 청구항 확인

B.5 법적 확인

  • §9.3 의 근거가 된 크롤링 대법원 판결 뉴스레터 PDF 의 원본 URL 특정 (법무법인 세종 IP그룹 2022-06-20 추정, raw dump 에 URL 미기록)
  • 대법원 2022. 5. 12. 선고 2021도1533 판결의 원문(대법원 종합법률정보) 대조
  • 김현숙, "크롤링을 이용한 공개데이터의 수집·활용의 법적 쟁점에 대한 비판적 검토", 「강원법학」 제61권(2020), 237면 — 원문 확인
  • nedrug.mfds.go.kr 이용약관 전문 확인 및 크롤링 관련 조항 유무 판단

B.6 구현 후 실측이 필요한 파라미터

  • λ 초기 사전값 0.5 건/일 이 실제 DMF 게시판에 맞는가? 30일 관측 후 재설정
  • 행 수 급변 임계 ±30%(게이트 G-2) 가 오탐을 얼마나 내는가? 실 운영 30일 후 조정
  • 대량 취하 임계 10%(게이트 G-5) — 실제 취하 건수 분포를 보고 조정. 하루 취하가 10%를 넘는 정상 상황이 있는가?
  • RAPTURE 결합 확률 임계 0.05(게이트 G-9) — Lerman et al. 의 recall 0.95 를 목표로 실측 조정
  • 자유서술 필드 SimHash k=3 이 한국어 짧은 텍스트에 적절한가? — Manku et al. 은 영어 웹페이지 전체 기준이다. 한국어 문자 3-gram 에서 k=3 이 너무 관대하거나 엄격할 수 있다. 실 데이터로 ROC 확인 필요
  • 최소 요청 간격 2.0초 가 대상 서버에 안전한가? 응답 시간 분포를 보고 조정
  • 하루 총 요청 상한 500건 이 충분한가? 총 페이지 수 실측 후 재설정
  • agy -p정확한 CLI 플래그와 JSON 출력 방식 — §6.4 코드는 agy -p <prompt> 가 stdout 으로 응답한다고 가정했다. 에이전트 CLI 축 문서에서 확정 후 반영해야 한다
  • 원본 HTML 보존 180일 이 디스크 용량상 타당한가? 페이지당 크기 실측 후 재설정

B.7 설계 결정 재검토 시점

  • 대상 게시판이 10개를 넘거나 실행 시간이 30분을 넘으면 → Azar et al. (PNAS 2018) 의 utility/change-rate 정렬 기반 차등 스케줄 도입 검토 (§4.7)
  • 첨부파일 대량 비교 요구가 생기면 → MinHash + LSH banding 추가 (§7.2)
  • 셀렉터 자동 복구가 월 1회 이상 발동하면 → 사이트가 불안정하다는 뜻. API 전환을 재검토한다 (B.2)
  • LLM 요약 품질에 불만이 생기면 → 요약 기능을 제거한다. 리포트의 본질은 표이지 산문이 아니다