# -*- coding: utf-8 -*- """Python Playwright로 공공기관2 정크사이트 GNB 권위트리 추출 → _gnb\{num}.json. 컨테이너 자동탐지(계층 앵커 최다) + 정크라벨 필터 + 스플래시 진입. 사용: python _gnb_extract.py [num ...]""" import sys, io, os, json, re, warnings sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') warnings.filterwarnings('ignore') from playwright.sync_api import sync_playwright GNB_DIR = r"D:\01.프로젝트\DB수집\작업파일\공공기관2\_gnb" # num: (name, base, entry_url, container_sel(None=auto)) SITES = { 1: ("한국전기안전공사", "https://www.kesco.or.kr", "https://www.kesco.or.kr/main/main.do", "ul#gnb"), 6: ("한국중부발전", "https://www.komipo.co.kr", "https://www.komipo.co.kr/kor/main/main.do", "#gnbmenu"), 7: ("한국지능정보사회진흥원", "https://www.nia.or.kr", "https://www.nia.or.kr/site/nia_kor/main.do", ".gnb_area"), 8: ("한국지식재산보호원", "https://www.koipa.re.kr", "https://www.koipa.re.kr/home/main.do", "#gnb"), 10: ("한국지역난방공사", "https://www.kdhc.co.kr", "https://www.kdhc.co.kr/", "#gnb"), 11: ("한국직업능력연구원", "https://www.krivet.re.kr", "https://www.krivet.re.kr/", "#k_gnb"), 13: ("한국청소년상담복지개발원", "https://www.kyci.or.kr", "https://www.kyci.or.kr/userSite/index.asp", ".mb_gnb"), 15: ("한국청소년활동진흥원", "https://www.kywa.or.kr", "https://www.kywa.or.kr/main/main.jsp", ".gnb ul.one_depth"), 16: ("한국체육산업개발", "https://www.ksponco.or.kr", "https://www.ksponco.or.kr/", "#gnb"), } JUNK = re.compile(r'바로가기|닫기|열기|검색|전체\s*메뉴|메뉴\s*보기|메인메뉴|서브메뉴|본문\s*바로|주\s*메뉴|TOP|위로|맨위|로그인|로그아웃|회원가입|마이페이지|언어|english|日本|中文|sitemap|즐겨찾기|새창|팝업|로고|이동$|^\s*$', re.I) EXTRACT_JS = """ (containerSel) => { const cont = containerSel ? document.querySelector(containerSel) : null; if(!cont) return {err:'no container', rows:[]}; function labelA(li){ for(const a of li.querySelectorAll('a, button')){ let p=a.parentElement, inside=false; while(p&&p!==li){ if(p.tagName==='UL'){inside=true;break;} p=p.parentElement; } if(inside) continue; const t=(a.textContent||'').trim(); if(t) return a; // 빈 텍스트(아이콘링크) 건너뜀 } return null; } function topUls(el){ return [...el.querySelectorAll('ul')].filter(ul=>{ let p=ul.parentElement; while(p&&p!==el){ if(p.tagName==='UL') return false; p=p.parentElement; } return true; }); } const rows=[]; function walk(ul, depth){ [...ul.children].filter(li=>li.tagName==='LI').forEach(li=>{ const a=labelA(li); if(a){ const label=(a.textContent||'').trim().replace(/\\s+/g,' '); const href=a.tagName==='A'?(a.href||''):''; if(label) rows.push({depth, label, href}); } topUls(li).forEach(sub=>walk(sub, depth+1)); }); } const starts = cont.tagName==='UL' ? [cont] : topUls(cont); starts.forEach(u=>walk(u, 1)); return {sel: containerSel, rows}; } """ def clean_rows(rows): out=[] for r in rows: lab=r['label'] if JUNK.search(lab): continue if len(lab)>60: continue h=r['href'] or '' if h.startswith('javascript') or h.endswith('#') or h=='': # 링크없는 카테고리노드는 유지(라벨만), 단 정크 아닐때 pass out.append({'depth':min(r['depth'],5), 'label':lab, 'href':h}) # depth 정규화: 최소깊이를 1로 if out: md=min(r['depth'] for r in out) if md>1: for r in out: r['depth']-=(md-1) return out def run(nums): with sync_playwright() as p: br=p.chromium.launch(headless=True) ctx=br.new_context(ignore_https_errors=True, viewport={'width':1920,'height':1080}, user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36') pg=ctx.new_page() for num in nums: name, base, url, sel = SITES[num] try: pg.goto(url, wait_until='domcontentloaded', timeout=30000) pg.wait_for_timeout(2500) res = pg.evaluate(EXTRACT_JS, sel) rows = clean_rows(res.get('rows', [])) d1 = [r['label'] for r in rows if r['depth']==1] data={'num':num,'name':name,'base':base,'gnb':rows} with open(os.path.join(GNB_DIR, f'{num}.json'),'w',encoding='utf-8') as f: json.dump(data,f,ensure_ascii=False,indent=1) print(f"[{num}.{name}] sel={res.get('sel')} rows={len(rows)} d1={len(d1)} :: {d1[:12]}") except Exception as e: print(f"[{num}.{name}] 실패: {type(e).__name__}: {e}") br.close() if __name__=='__main__': nums=[int(x) for x in sys.argv[1:]] or list(SITES.keys()) run(nums)