import json,sys,platform from html.parser import HTMLParser import re class Main(HTMLParser): def __init__(self):super().__init__();self.depth=0;self.skip=0;self.text=[];self.links=[];self.headings=[];self.mainCount=0;self.verbatim=False def handle_starttag(self,t,a): if t=='main':self.depth+=1;self.mainCount+=1 if self.depth and t in ['pre','code']:self.verbatim=True if self.depth and t in ['script','style','nav','aside']:self.skip+=1 if self.depth and not self.skip and t=='a':self.links.append(dict(a).get('href','')) def handle_endtag(self,t): if self.depth and t in ['script','style','nav','aside']:self.skip-=1 if t=='main':self.depth-=1 def handle_data(self,v): if self.depth and not self.skip:self.text.append(v) def extract(s): p=Main();p.feed(s);text=re.sub(r'\s+',' ',' '.join(p.text)).strip();reasons=[] if p.mainCount!=1:reasons.append('main-count-must-be-one') if not text:reasons.append('empty-main-text') if p.depth!=0 or p.skip!=0:reasons.append('unbalanced-tracked-tags') if p.verbatim:reasons.append('verbatim-policy-required') return {'text':text,'links':p.links,'mainCount':p.mainCount,'comparisonReady':not reasons,'holdReasons':reasons} def compare(before,after): left=extract(before);right=extract(after);ready=left['comparisonReady'] and right['comparisonReady'] return {'comparisonStatus':'compared' if ready else 'hold-extraction','textEqual':left['text']==right['text'] if ready else None,'linksEqual':left['links']==right['links'] if ready else None,'before':left,'after':right} a='

검사

기한은 10월 20일.

자료
' b='

검사

기한은 10월 20일.

자료
' c=b.replace('20일','21일');e=b.replace('/v1','/v2') cases=[('navigation-only',b),('deadline-changed',c),('href-only',e)] result={'inputs':{'baseline':a,**dict(cases)},'baseline':extract(a),'results':[]} for name,document in cases: comparison=compare(a,document);result['results'].append({'name':name,'rawEqual':a==document,'comparisonStatus':comparison['comparisonStatus'],'textEqual':comparison['textEqual'],'linksEqual':comparison['linksEqual'],'extracted':comparison['after']}) assert [(r['textEqual'],r['linksEqual']) for r in result['results']]==[(True,True),(False,True),(True,False)] contract_inputs=[('missing-main','
로그인 안내
','
로그인 안내
'),('empty-main','
','
'),('multiple-main','
첫째
둘째
','
첫째
둘째
'),('pre-whitespace','
a  b
','
a b
')] result['contractCases']=[{'name':name,'inputs':[before,after],'result':compare(before,after)}for name,before,after in contract_inputs] assert all(x['result']['comparisonStatus']=='hold-extraction' and x['result']['textEqual']is None and x['result']['linksEqual']is None for x in result['contractCases']) result['comparisonContract']='Exactly one nonempty main; no pre/code without a separate whitespace policy. Failure is hold, never equal. This parser does not reproduce browser DOM correction or prove page identity.' from datetime import datetime as _runtime_datetime, timezone as _runtime_timezone result.update({'fixtureCreatedOn':'2026-10-15','executedAt':_runtime_datetime.now(_runtime_timezone.utc).isoformat(),'runtime':platform.python_version(),'synthetic':True}) print(json.dumps(result,ensure_ascii=False,indent=2))