# -*- coding: utf-8 -*- """URL 패턴으로 각 사이트의 CMS 지문 산출 → 검수완료(정답) vs 전개대상 커버리지 매핑. (시트만 읽음, 무fetch)""" import openpyxl, os, re, sys, glob from collections import Counter sys.stdout.reconfigure(encoding='utf-8') ROOT = r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵' EXCLUDE = {'서천군'} INSPECTED = {'계룡시','공주시','금산군','논산시','보령시','당진시','부여군', '고창군','김제시','남원시','군산시','무주군'} def pick_xlsx(folder): cand=[f for f in glob.glob(os.path.join(folder,'*.xlsx')) if 'backup' not in os.path.basename(f).lower() and not os.path.basename(f).startswith(('_','~$'))] cand.sort(key=lambda f:(0 if re.match(r'^[가-힣]+도?_',os.path.basename(f)) else 1,len(f))) return cand[0] if cand else None def url_sig(u): """URL을 CMS 패턴 토큰으로 환원.""" u=u.lower() toks=[] if 'selectboardlist' in u or 'bbsmstr' in u: toks.append('bbs:BBSMSTR') elif '/cop/bbs/' in u: toks.append('bbs:cop') elif 'selectbbsnttlist' in u or 'bbsno=' in u: toks.append('bbs:nttList') elif '/bbs/' in u and 'list.do' in u: toks.append('bbs:bbsList') if 'menucd=' in u: toks.append('pg:menuCd') elif 'contents.do?key=' in u or re.search(r'/sub\.do\?.*key=',u): toks.append('pg:key') elif re.search(r'/kor/sub[\d_]+\.do',u) or re.search(r'/[a-z]{2,4}/sub[\d_]+\.do',u): toks.append('pg:subNN.do') elif 'prog/' in u and 'list.do' in u: toks.append('pg:prog.list') elif re.search(r'/\w+\.do\b',u): toks.append('pg:.do') elif '.jsp' in u: toks.append('pg:.jsp') elif '.php' in u: toks.append('pg:.php') return toks def fingerprint(xlsx): wb=openpyxl.load_workbook(xlsx); ws=wb.active cnt=Counter(); host=Counter() for r in range(3,ws.max_row+1): c=ws.cell(r,11) u=c.hyperlink.target if c.hyperlink else (c.value if isinstance(c.value,str) and c.value.startswith('http') else None) if not u: continue m=re.match(r'https?://([^/]+)',u) if m: host[re.sub(r'^www\.','',m.group(1))]+=1 for t in url_sig(u): cnt[t]+=1 dom=host.most_common(1)[0][0] if host else '?' # 시그니처 = 상위 토큰 집합 sig=tuple(sorted(t for t,n in cnt.items() if n>=3)) return dom, sig, cnt sites=[] for prov in sorted(os.listdir(ROOT)): pdir=os.path.join(ROOT,prov) if not os.path.isdir(pdir) or prov.startswith('__'): continue for d in sorted(os.listdir(pdir)): sdir=os.path.join(pdir,d) if not os.path.isdir(sdir): continue nm=re.sub(r'^\d+\.','',d) if nm in EXCLUDE: continue xl=pick_xlsx(sdir) if not xl: continue try: dom,sig,cnt=fingerprint(xl) except Exception as e: sig=('ERR',); dom=str(e)[:30]; cnt={} sites.append({'prov':prov[:2],'name':nm,'dom':dom,'sig':sig, 'inspected':nm in INSPECTED}) # group by sig from collections import defaultdict groups=defaultdict(list) for s in sites: groups[s['sig']].append(s) print('=== CMS 시그니처별 그룹 (★=검수완료 선생님 있음) ===') for sig,ss in sorted(groups.items(),key=lambda x:-len(x[1])): has_teacher=any(s['inspected'] for s in ss) teach=[s['name'] for s in ss if s['inspected']] targets=[f"{s['name']}({s['prov']})" for s in ss if not s['inspected']] star='★' if has_teacher else '✗미지' print(f"\n[{star}] sig={'+'.join(sig) if sig else '(무)'} ({len(ss)}곳)") if teach: print(f" 선생님(검수완료): {', '.join(teach)}") if targets: print(f" 전개대상: {', '.join(targets)}") if not has_teacher and targets: print(f" ⚠ 정답 선생님 없음 → 미지CMS(별도조사 필요)")