DB_JOB/작업파일/공공기관2/_gnb_extract.py
hehihoho3 df16c98366 백업: DB수집 전체 스냅샷 (공공기관2 정리 전)
공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 18:15:40 +09:00

108 lines
5.1 KiB
Python

# -*- 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)