DB_JOB/작업파일/완료_1-2주차/2주차/검토_리포트/crawl_gnb.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

131 lines
5.5 KiB
Python

"""외교부·성평등가족부 메인 페이지에서 GNB+하위 메뉴 트리 추출.
목적: breadcrumb 없는 사이트의 실제 메뉴 구조를 파악하기 위해
GNB(상단 전체메뉴 영역)의 다단계 ul/li 트리를 가져옴.
방법:
1. 메인 페이지 HTML에서 nav.gnb, #gnb, .navigation 같은 컨테이너 찾기
2. 또는 "전체메뉴/사이트맵" 버튼을 누르면 펼쳐지는 영역
3. URL → 메뉴 path 매핑 사전 만들기
"""
import sys, io, json, re
from urllib.parse import urljoin, urlparse
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
import requests, urllib3
urllib3.disable_warnings()
HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,*/*;q=0.9',
'Accept-Language': 'ko-KR,ko;q=0.9',
}
s = requests.Session(); s.headers.update(HEADERS)
from bs4 import BeautifulSoup
# 사이트별 메인 URL과 GNB 컨테이너 셀렉터 후보
SITES = {
'외교부': {
'main': 'https://www.mofa.go.kr/www/index.do',
'base': 'https://www.mofa.go.kr',
'gnb_sel': ['#gnb', '.gnb', 'nav.gnb', '.gnb_wrap', '.top-menu',
'.allmenu', '.sitemap', '#allMenu', '#sitemap',
'.global_nav', '#mainNav', '.main_menu',
'header nav', '.nav-list'],
},
'성평등가족부': {
'main': 'https://www.mogef.go.kr/',
'base': 'https://www.mogef.go.kr',
'gnb_sel': ['#gnb', '.gnb', '.gnbWrap', '.allMenu', '#allmenu',
'.sitemap_wrap', 'nav.gnb', '.lnb', '.main_menu',
'.menu_wrap', '#menu', '.mainMenu', '.snb',
'header nav', '#header nav'],
},
}
# 메뉴 트리 추출 — 다단계 ul/li 구조 재귀 파싱
def parse_menu(ul, depth=1, path=None, out=None):
if out is None: out = []
if path is None: path = []
for li in ul.find_all('li', recursive=False):
# a 태그의 텍스트와 href
a = li.find('a', recursive=False)
if not a:
# children 만 있을 수도
sub_ul = li.find('ul', recursive=False)
if sub_ul:
parse_menu(sub_ul, depth+1, path, out)
continue
text = a.get_text(' ', strip=True)
href = a.get('href', '').strip()
new_path = path + [text] if text else path
out.append({'depth': depth, 'path': new_path, 'text': text, 'href': href})
# 하위 ul
sub_ul = li.find('ul', recursive=False)
if sub_ul:
parse_menu(sub_ul, depth+1, new_path, out)
return out
# 메인 페이지에서 GNB 추출 (또는 전체 사이트맵 페이지)
for name, conf in SITES.items():
print(f'\n========== {name} ==========')
try:
r = s.get(conf['main'], verify=False, timeout=20)
print(f'main HTTP {r.status_code} / len={len(r.text)}')
soup = BeautifulSoup(r.text, 'html.parser')
found_ul = None
used_sel = None
for sel in conf['gnb_sel']:
cont = soup.select_one(sel)
if cont:
# 컨테이너 안에서 다단계 ul 찾기 — 가장 li가 많은 ul
uls = cont.find_all('ul', recursive=True)
if uls:
best = max(uls, key=lambda u: len(u.find_all('li', recursive=False)))
if len(best.find_all('li', recursive=False)) >= 3:
found_ul = best
used_sel = sel
break
# 전체 페이지에서 가장 큰 nav ul 찾기 (백업)
if not found_ul:
# ul 중에서 깊이 2+ 이고 li 5+ 인 것
for ul in soup.find_all('ul'):
lis = ul.find_all('li', recursive=False)
if len(lis) >= 5:
has_nested = any(li.find('ul') for li in lis)
if has_nested:
found_ul = ul
used_sel = 'auto-detect'
break
if found_ul:
print(f'GNB ul 발견 (셀렉터: {used_sel})')
print(f' 최상위 li: {len(found_ul.find_all("li", recursive=False))}')
tree = parse_menu(found_ul)
print(f' 전체 노드: {len(tree)}')
# 깊이별 통계
from collections import Counter
print(f' 깊이 분포: {dict(Counter(n["depth"] for n in tree))}')
# 샘플 출력 (depth 1)
for n in tree:
if n['depth'] == 1:
print(f' [D] {n["text"]}')
# 절대 URL
for n in tree:
if n['href'] and not n['href'].startswith('http'):
if n['href'].startswith('/'):
n['absUrl'] = conf['base'] + n['href']
elif n['href'].startswith('javascript:') or n['href'].startswith('#'):
n['absUrl'] = None
else:
n['absUrl'] = urljoin(conf['main'], n['href'])
else:
n['absUrl'] = n['href'] if n['href'] else None
out = rf'D:\01.프로젝트\DB수집\2주차\검토_리포트\{name}_gnb.json'
with open(out, 'w', encoding='utf-8') as f:
json.dump(tree, f, ensure_ascii=False, indent=2)
print(f' saved: {out}')
else:
print('GNB ul 못 찾음')
except Exception as e:
print(f'ERR: {e}')