자료를 저장했어도 검색을 조사한 것은 아닐 수 있다. 나는 이 차이를 보이려고 웹과 모바일의 관측 상태를 따로 받는 합성 모듈을 만들었다. 실제 검색이나 다운로드 기능은 없다. 이미 전달된 자료가 어떤 상태라고 선언됐는지 검사하고, 없는 순위를 0이나 큰 숫자로 채우지 않는 작은 표현 계약이다.
본문 저장과 검색 관측은 서로 다른 입력이다
summarize는 합성 게시자·글 ID, 웹과 모바일의 Observation, 본문 저장 선언을 받는다. content_saved는 정확한 참·거짓이어야 한다. 저장이 참이어도 두 관측이 not_performed라면 순위는 모두 None으로 남는다. 반대로 검색에서 글을 봤다고 본문 파일까지 저장됐다는 선언을 만들지 않는다.
글의 동일성도 명시한 합성 ID로만 연결한다. 제목을 정리해 같은 글로 추정하거나 실제 플랫폼 주소를 파싱하지 않는다. 원래 프로그램에는 식별자가 없는 경우의 제목 보조 키가 있지만 이 예제는 누락된 ID를 거절하도록 범위를 좁혔다. 실서비스의 식별 규칙을 그대로 복제했다고 소개하지 않는다.
네 상태가 서로 다른 다음 일을 뜻한다
not_performed는 조사를 하지 않았다는 선언이다. 순위와 조사 범위를 넣을 수 없다. not_found_in_scope는 선언한 범위에서 찾지 못했다는 뜻이며 범위가 필요하다. unavailable은 확인할 수 없었던 경우다. 이때 범위는 시도하려던 범위이며 전부 읽었다는 증거가 아니다. observed만 실제로 전달된 위치를 갖는다.
네 상태 중 어느 것도 코드가 실제 검색 화면을 확인했다는 인증은 아니다. 이 모듈은 입력의 의미와 일관성을 계산한다. 신뢰할 수 있는 관찰과 검색 조건, 수집 허가와 사이트 접근은 호스트가 따로 확인해야 한다. 값이 없다는 이유로 상태를 자동 추정하는 기능도 넣지 않았다.
범위를 벗어난 숫자는 순위로 받아들이지 않는다
observed의 위치는 선언한 시작과 끝 사이의 양의 정수여야 한다. 참·거짓, 숫자 문자열, 0과 범위 밖 숫자는 거절한다. 나머지 상태에는 순위를 넣을 수 없다. 원래 자료의 정렬용 대체 숫자를 관측값처럼 옮기면 이 계약에서는 실패한다. 정렬 규칙과 공개할 순위 값의 의미를 분리하기 위해서다.
범위의 상한은 작은 합성 실험을 위한 제한이다. 실제 검색 서비스가 그만큼 결과를 제공하거나 그 범위가 통계적으로 충분하다는 주장이 아니다. 비교할 실제 조건은 별도 정책으로 정해야 한다. 전체 코드는 아래에 넣었으며 브라우저나 파일을 열어 실제 순위를 읽는 부분은 없다.
코드와 구현 살펴보기
독립 합성 전체 구현
"""Declared synthetic observations, not search measurement or indexing proof."""
from dataclasses import dataclass
def alias(value):
if not isinstance(value, str) or not value.strip() or len(value) > 80:
raise ValueError('A bounded nonempty alias is required')
@dataclass(frozen=True)
class Observation:
channel: str
sample: str
query: str
state: str
position: int | None = None
start: int | None = None
end: int | None = None
def __post_init__(self):
alias(self.sample); alias(self.query)
if self.channel not in ('web', 'mobile'):
raise ValueError('Unknown source channel')
if self.state not in ('not_performed', 'not_found_in_scope', 'unavailable', 'observed'):
raise ValueError('Unknown observation state')
if self.state == 'not_performed':
if any(v is not None for v in (self.position, self.start, self.end)):
raise ValueError('An unperformed check has no measured rank or checked range')
return
if (type(self.start) is not int or type(self.end) is not int
or not 1 <= self.start <= self.end <= 10000):
raise ValueError('A bounded declared/requested range is required')
if self.state == 'observed':
if type(self.position) is not int or not self.start <= self.position <= self.end:
raise ValueError('Observed position must be inside the declared range')
elif self.position is not None:
raise ValueError('No rank value without an observed position')
def _display(o):
if o.state == 'observed':
ko, en = str(o.position)+'위 (관측 범위)', 'Position '+str(o.position)+' in this observation'
else:
ko, en = {
'not_performed': ('조사 안 함', 'Not checked'),
'not_found_in_scope': ('조사 범위에서 찾지 못함', 'Not found in the declared scope'),
'unavailable': ('확인 불가', 'Unavailable'),
}[o.state]
return {'state': o.state, 'position': o.position,
'range': [o.start, o.end] if o.start is not None else None,
'display_ko': ko, 'display_en': en}
def summarize(*, publisher, item, web, mobile, content_saved):
alias(publisher); alias(item)
if not isinstance(web, Observation) or not isinstance(mobile, Observation):
raise ValueError('Both source observations must be explicit')
if web.channel != 'web' or mobile.channel != 'mobile':
raise ValueError('Channel slots cannot be swapped or duplicated')
if (web.sample, web.query) != (mobile.sample, mobile.query):
raise ValueError('Different observation samples or query contexts')
if type(content_saved) is not bool:
raise ValueError('Content-save declaration must be a boolean')
return {'identity': {'publisher': publisher, 'item': item},
'sample': web.sample, 'query': web.query,
'content_saved_declared': content_saved,
'web': _display(web), 'mobile': _display(mobile),
'observed_channels': [o.channel for o in (web, mobile) if o.state == 'observed'],
'observation_authenticated': False, 'global_rank_proven': False}
같은 글이라도 관측 맥락은 다시 맞춘다
웹과 모바일은 각각 명시한 칸에 들어가야 하며 sample과 query 별칭이 같아야 한다. 다른 실행이나 다른 질의를 섞는 입력을 거절하려고 만든 경계다. 두 쪽의 범위는 다를 수 있고 결과에 그대로 남는다. 웹 21~30 구간과 모바일 1~10 구간을 같은 전체 모집단처럼 합쳐 계산하지 않는다.
두 위치의 최소값이나 평균을 대표 순위로 만들지 않는다. observed_channels에는 실제로 관측됐다고 선언한 쪽만 들어간다. 별칭이 같다는 사실만으로 실제 시간·지역·개인화와 조사 방법이 같다고 인증하지도 않는다. 호스트는 실제 비교 맥락을 더 정확히 묶어야 하며 이번 코드는 그 선언을 대신 만들어 주지 않는다.
다섯 기록에서 빈 값의 차이를 읽는다
아래 기록은 직접 저장 후 검색 미실시, 서로 다른 두 위치, 웹 범위 내 미검출, 웹 확인 불가, 양쪽 범위 내 미검출을 보여 준다. 같은 None이라도 상태와 안내가 다르다는 것을 비교할 수 있다. 모두 Python으로 실제 생성한 합성 결과이며 고객 자료나 실제 검색순위는 아니다.
선택기는 저장한 결과만 보여 준다. 실제 인라인 스크립트의 초기 표시와 다섯 선택을 한국어·영어에서 DOM 대역으로 대조한다. 새 검색 요청, 파일 다운로드나 방문자 계정 접근은 없다. 이 검사를 실제 검색 서비스나 모바일 화면 검수로 확대하지 않는다.
적용할 때는 관찰을 만든 근거를 더 연결한다
16개 시험은 직접 저장과 검색의 분리, 두 채널의 위치, 미실시·미검출·확인 불가, 관측 맥락 불일치, 잘못된 범위·자료형과 입력 보존을 다룬다. ZIP을 푼 별도 폴더에서도 같은 시험을 실행한다. 코드·시험·기록 생성기·결과·README·MIT 라이선스를 함께 공개한다.
관찰이 실제였는지와 전체 순위가 증명됐는지는 항상 거짓으로 남긴다. 기존 필드별 근거 모듈은 어떤 값에 어떤 근거가 붙는지 검토하는 별도 부품이며 이번 코드와 자동으로 결합하지 않았다. 실제 수집기·로그·권한·조건·보관 정책을 연결하고 나서 그 통합 범위를 따로 시험해야 한다.
수집기에 반영한 수정과 이 예제의 역할을 구별한다
후속 개발에서는 실제 수집기의 두 순위가 모두 비어 있을 때 WEB_ONLY 대신 UNRECORDED를 기록하도록 고쳤다. best_rank는 None으로 남기고 JSON은 null, CSV는 빈 칸으로 저장한다. 화면과 저장 HTML도 순위 기록이 없다고 표현한다. 이것은 원래 프로그램의 출력 오류를 고친 작업이며 이 ZIP의 네 상태 모델 전체를 수집기에 설치한 것은 아니다.
UNRECORDED만 보고 not_performed나 not_found_in_scope를 자동으로 선택해서는 안 된다. 이유를 모르는 기록 없음과 조사를 하지 않았다는 선언은 다르다. 이 예제와 연결하려면 호스트가 실제 조사 여부·시도와 확인한 범위를 추가해야 한다. 아직 근거가 없으면 통합을 미루거나 별도 미확인 상태를 설계해야 하며, 빈 값 하나에서 상세 상태를 추측하지 않는다.
수집기 수정의 실제 오프라인 시험 15개와 복구본의 같은 시험은 별도 제작기에 설명했다. 이 ZIP의 합성 시험 16개와 결과 뷰어는 그대로 유지한다. 이번 보완은 적용 경계 설명이며 공개 패키지 바이트나 기존 입력 계약을 바꾸지 않았다. 실제 검색·이미지 다운로드·브라우저 검증을 추가로 마쳤다는 뜻도 아니다.
목록 번호를 관측 위치로 넘기지 않는다
추가 후속 개발에서는 전체 블로그 실행기가 글 목록의 번호를 mobile_rank에 넣던 경로도 고쳤다. 이번 조회의 응답 순서는 list_position으로 별도 목록에 남기고 검색순위는 None으로 전달한다. 기존 저장 엔진은 UNRECORDED와 null, 순위 기록 없음으로 저장한다. 이 수정은 앞서 고친 출력 함수에 들어오는 입력의 의미를 바로잡은 것이며, 새 모듈을 설치한 작업이 아니다.
이 네 상태 예제의 observed 위치에는 실제 관측이 뒷받침돼야 한다. 목록 번호가 양의 정수라는 이유만으로 관측값이 되지 않는다. 반대로 UNRECORDED를 받은 것만으로 not_performed라고 정해서도 안 된다. 전체수집처럼 검색을 호출하지 않는 경로임을 호스트가 확인했다면 명시적인 근거로 미실시를 선언할 수 있지만, 그 변환 어댑터를 이번에 만들지는 않았다.
후속 원제품 시험은 실제 main·저장 12개와 기존 검색 15개, 복구본의 같은 27개 및 측정 순위 병합 80개 조합이다. 이 ZIP의 합성 16개 시험과 결과 뷰어, 바이트와 입력 계약은 그대로다. 과거 저장 파일을 자동으로 고치거나 실제 검색·이미지·브라우저 검수를 새로 완료한 결과로 읽지 않는다.