같은 이름의 버튼이 둘일 때 첫 번째를 누르는 대신 멈추는 규칙을 독립 Python으로 만들었다. 실제 브라우저와 계정 없이 관찰의 현재성·잘림·이름·역할·프레임 조건을 시험할 수 있다. 반환값은 선택 자료일 뿐 실제 클릭은 하지 않는다.

신뢰할 관찰을 받는다는 전제를 먼저 적었다

입력은 observation과 기대한 context, target이다. context는 tab·epoch·snapshot을 갖고 관찰의 값과 모두 같아야 한다. 기대한 자료형과 키가 아니면 거절한다. 오래된 epoch나 다른 snapshot에서 온 목록을 현재 대상으로 선택하지 않는 순수 비교 함수다.

이 함수가 실제 브라우저의 관찰을 수집하거나 그 진위를 인증하지는 않는다. 호출자가 임의로 같은 번호를 넣으면 보호를 우회할 수 있다. 실제 어댑터는 호스트에서 신뢰한 관찰과 기대 문맥을 가져와야 한다. 나는 자료 검증과 자료 출처의 인증을 하나의 기능처럼 소개하지 않는다.

이름과 역할은 부분 일치로 고르지 않는다

name과 role은 필수이며 공백을 정리하고 소문자로 바꿔 정확히 비교한다. “저장”을 요청했다고 “임시저장”까지 같은 이름으로 고르는 방식은 아니다. href와 frame, in_viewport는 명시했을 때만 추가 조건으로 쓴다. href는 선언된 문자열을 정확히 비교하며 실제 주소를 검증하는 기능은 없다.

조건을 추가하는 사람은 그 조건이 현재 요청을 뜻하는지 확인해야 한다. 두 후보 중 아무거나 하나만 남기기 위해 프레임을 추측하면 모호함을 숨긴다. 이 모듈은 조건의 의미를 추론하지 않고 정해진 계약을 비교한다. 실제 제품의 더 풍부한 참조와 프레임 지원을 그대로 옮긴 것도 아니다.

숨김과 비활성, 화면 밖 상태를 다르게 처리한다

visible이 거짓인 후보는 제외한다. disabled 후보는 이름 중복 개수에 남겨 두고 최종 하나가 비활성이면 TARGET_DISABLED로 끝낸다. 비활성 후보를 몰래 지우고 다른 버튼을 선택하지 않는다. 입력 참값 자리에 정수 1을 넣는 경우도 거절한다.

화면 밖 후보는 자동으로 무시하지 않는다. 같은 이름이 화면 안팎에 있으면 여전히 모호하다. in_viewport 조건을 명시했을 때만 그 범위를 따른다. 화면에 가까운 항목을 임의로 선호하는 판단을 빼고 호출자가 선택 기준을 드러내도록 했다. 실제 가림과 클릭 지점은 이 모듈의 범위 밖이다.

코드와 구현 살펴보기

독립 합성 전체 구현

"""Resolve one declared target in a trusted synthetic observation. No actions."""
import copy

class TargetError(ValueError):pass

def _keys(value,keys):
    if not isinstance(value,dict) or set(value)!=set(keys):raise TargetError('INVALID_FIELDS')

def _text(value):
    if not isinstance(value,str) or not value.strip() or len(value)>500:raise TargetError('INVALID_TEXT')

def _context(value):
    _keys(value,['tab','epoch','snapshot'])
    _text(value['tab']);_text(value['snapshot'])
    if type(value['epoch']) is not int or value['epoch']<0:raise TargetError('INVALID_EPOCH')

def _norm(value):return ' '.join(value.strip().split()).lower()

def resolve_target(observation,expected,target):
    """All metadata is supplied, not authenticated. Recheck real state at dispatch."""
    _keys(observation,['context','has_more','elements']);_context(observation['context']);_context(expected)
    if observation['context']!=expected:raise TargetError('STALE_OBSERVATION')
    if type(observation['has_more']) is not bool:raise TargetError('INVALID_COMPLETENESS')
    _keys(target,['name','role','href','frame','in_viewport'])
    _text(target['name']);_text(target['role'])
    for field in ['href','frame']:
        if target[field] is not None:_text(target[field])
    if target['in_viewport'] is not None and type(target['in_viewport']) is not bool:raise TargetError('INVALID_VIEWPORT')
    elements=observation['elements']
    if not isinstance(elements,list) or len(elements)>1000:raise TargetError('INVALID_ELEMENTS')
    seen=set();matches=[]
    for element in elements:
        _keys(element,['id','name','role','href','frame','visible','in_viewport','disabled'])
        for field in ['id','name','role','frame']:_text(element[field])
        if element['href'] is not None:_text(element['href'])
        if any(type(element[k]) is not bool for k in ['visible','in_viewport','disabled']):raise TargetError('INVALID_STATE')
        if element['id'] in seen:raise TargetError('DUPLICATE_REFERENCE')
        seen.add(element['id'])
        if not element['visible']:continue
        if _norm(element['name'])!=_norm(target['name']) or _norm(element['role'])!=_norm(target['role']):continue
        if any(target[k] is not None and target[k]!=element[k] for k in ['href','frame','in_viewport']):continue
        matches.append(element)
    if observation['has_more']:raise TargetError('TARGET_QUERY_TRUNCATED')
    if len(matches)>1:raise TargetError('TARGET_AMBIGUOUS')
    if not matches:raise TargetError('TARGET_NOT_FOUND')
    if matches[0]['disabled']:raise TargetError('TARGET_DISABLED')
    return {'context':copy.deepcopy(expected),'target':copy.deepcopy(matches[0]),'authorized':False,'dispatched':False}

하나만 보여도 잘린 관찰이면 거절한다

has_more가 참이면 현재 조각에 대상이 하나여도 TARGET_QUERY_TRUNCATED다. 서로 다른 노드에 같은 id를 넣는 오염도 DUPLICATE_REFERENCE로 거절한다. 목록 안의 첫 항목이 그럴듯하다는 사실은 유일성의 근거가 아니다.

21개 시험은 유일·중복·숨김 중복·비활성 중복과 단일 비활성, 화면 밖 중복과 명시 범위, 프레임·href 조건을 다룬다. 탭·epoch·snapshot의 변경과 잘린 목록, 잘못된 자료형·미지 필드·입력 불변성도 확인한다. 성공뿐 아니라 어떤 상황에서 선택을 멈추는지 실제 코드로 시험했다.

합성 선택 기록을 실제로 만들었다

다섯 기록은 정상 하나 선택, 모호한 이름, 잘린 관찰, 오래된 문서, 명시한 프레임으로 좁히기를 보여 준다. 로컬 Python 실행으로 records를 만들고 아래 화면은 저장된 결과만 읽는다. 방문자 탭을 찾거나 현재 사이트의 버튼을 누르지 않는다.

원본 시험 뒤 공개 ZIP의 파일 해시를 대조하고 별도 폴더에 추출해 같은 21개 시험을 실행한다. 한영 인라인 선택기는 DOM 대역에서 실제 records와 대조한다. 이것은 합성 코드와 기록 표시의 검증이며 실브라우저 렌더링·현재 사이트 조작·키보드 검수를 완료했다는 뜻이 아니다.

합성 로컬 기록의 읽기 뷰어

실제 클릭 앞에 남겨 둔 책임

공개 ZIP에는 전체 구현·시험·예제 자료·기록 생성기와 README를 넣었다. 표준 라이브러리만 쓰며 수정·재사용할 수 있다. 반환값의 authorized와 dispatched는 항상 거짓이다. 선택 성공이 실제 계정 권한이나 현재 노드의 행동 가능성을 보장하지 않는다.

어댑터는 실제 문서와 노드, 가림과 참조 유효성, 작업 권한을 조작 직전에 다시 확인하고 행동 뒤 결과도 읽어야 한다. 선택과 전달 사이의 변경을 이 함수가 잠그지 않는다. 나는 작은 선택 부품을 브라우저 자동화 전체로 부르지 않고 모호함을 어떻게 남기고 좁히는지 살펴볼 수 있는 자료로 공유한다.

코드·시험·실행 기록 ZIP 받기
Python 표준 라이브러리 · 합성 자료 · 수정과 재사용 가능