공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
131 lines
5.5 KiB
Python
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}')
|