# -*- coding: utf-8 -*- """미검수 기관 새창열림(target=_blank)→사이트 일괄 (사용자룰 2026-06-04). 각 사이트 GNB/메뉴(홈+샘플 콘텐츠페이지)에서 target=_blank 앵커 수집. 같은 정규화URL에서 _blank 우세(blank수>=일반수)인 것만 '사이트'(내부보드 배너 _blank 오탐 방지). xlsx의 그 URL 행(현재 페이지/게시판) → L=사이트, M·N 비움. 사용: python -X utf8 _재검수_blanksite.py <기관...> [--write] """ import sys, os, re, shutil, importlib.util from collections import Counter from urllib.parse import urljoin, urldefrag, urlparse import openpyxl # 실게시판 보호: 게시판 엔드포인트 URL이면서 현재 L=게시판이면 사이트로 안 뒤집음 BOARD_URL = re.compile(r'selectBbsNttList|/bbs/|BBSMSTR|board/list|list\.buan|selectBoardList|selectCntrct|selectPric|/list\.do|nttList|selectEminwonNoticeList|selectInhAuthPgmList', re.I) HERE = os.path.dirname(os.path.abspath(__file__)) MODULES = ['_chungnam_phase234_all.py','_chungbuk_phase234_all.py','_jeonbuk_phase234_all.py'] # 파일문서(_blank여도 보류) 제외 안 함 — 사용자 확정: 새창이면 PDF도 사이트. def load(): sites,mods={},{} for f in MODULES: sp=importlib.util.spec_from_file_location(f[:-3],os.path.join(HERE,f));m=importlib.util.module_from_spec(sp);sp.loader.exec_module(m) for k,v in m.SITES.items(): sites[k]=v;mods[k]=m return sites,mods def mkfetch(M,cfg): if hasattr(M,'make_session'): s=M.make_session(weak_ssl=cfg.get('weak_ssl',False)); return lambda u:M.fetch(s,u) return lambda u:M.fetch(u) def norm(base,h): h=urljoin(base,h); h=urldefrag(h)[0] return h.replace('http://','https://').rstrip('/') def collect_blank(fetch, sample_urls): """샘플 페이지들에서 _blank 우세 정규화URL 집합 반환.""" bl,nb=Counter(),Counter() pages=0 for su in sample_urls: soup=fetch(su) if soup is None: continue pages+=1 for a in soup.find_all('a',href=True): h=a['href'] if not h or h.startswith('#') or 'javascript' in h.lower(): continue key=norm(su,h) if not key.startswith('http'): continue if (a.get('target') or '')=='_blank': bl[key]+=1 else: nb[key]+=1 site=set(k for k in bl if bl[k]>=1 and bl[k]>=nb[k]) return site,pages def run(name,cfg,M,write,minrow=3): xlsx=cfg['xlsx'] wb=openpyxl.load_workbook(xlsx); ws=wb.active # 샘플: 홈 + xlsx 상위 데이터 URL 3개(헤더 GNB 확보) urls=[ws.cell(r,11).value for r in range(3,ws.max_row+1) if isinstance(ws.cell(r,11).value,str) and ws.cell(r,11).value.startswith('http')] if not urls: print(f'{name}: URL없음'); return host=urlparse(urls[0]).netloc; scheme='https' # 권위 있는 메뉴(GNB)만 사용 — 임의 콘텐츠페이지의 _blank 관련링크 노이즈 차단 samples=[f'{scheme}://{host}/', f'{scheme}://{host}/main/', f'{scheme}://{host}/main/main.do', f'{scheme}://{host}/index.do'] fetch=mkfetch(M,cfg) site_urls,pages=collect_blank(fetch,samples) def lab(r): for c in range(10,3,-1): v=ws.cell(r,c).value if v not in (None,''): return str(v).strip()[:18] return '' flips=[] for r in range(max(3,minrow),ws.max_row+1): K=ws.cell(r,11).value if not (isinstance(K,str) and K.startswith('http')): continue curL=ws.cell(r,12).value if curL=='사이트': continue if curL=='게시판' and BOARD_URL.search(K): # 실게시판 보호 continue if norm(samples[0],K) in site_urls: flips.append((r,lab(r),curL,K)) if write and flips: bak=xlsx.replace('.xlsx','_backup_새창사이트전.xlsx') if not os.path.exists(bak): shutil.copy(xlsx,bak) for r,l,L,K in flips: ws.cell(r,12).value='사이트'; ws.cell(r,13).value=None; ws.cell(r,14).value=None wb.save(xlsx) print(f'{name}: 메뉴페이지{pages}개·_blank우세링크{len(site_urls)} → 사이트전환 {len(flips)}행 {"[적용]" if write else "[DRY]"}') for r,l,L,K in sorted(flips)[:20]: print(f' r{r} {L}→사이트 {l} | {K[:58]}') if len(flips)>20: print(f' …외 {len(flips)-20}행') def main(): args=sys.argv[1:]; write='--write' in args minrow=3 for a in args: if a.startswith('--minrow='): minrow=int(a.split('=')[1]) names=[a for a in args if not a.startswith('--')] sites,mods=load() for n in names: if n in sites: run(n,sites[n],mods[n],write,minrow) else: print(f'{n}: 없음') if __name__=='__main__': main()