공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
151 lines
5.9 KiB
Python
151 lines
5.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""국토안전관리원 컬럼(D~J) 재구성 — all-menu dl/dt/dd 4단 트리에서 URL별 경로 추출 후 in-place 갱신.
|
|
L~Q·K·B·C 보존(URL 매칭). 백업 후 실행. 사용: python _공공기관_colfix_kalis.py
|
|
"""
|
|
import os, re, shutil, importlib.util, warnings
|
|
from copy import copy
|
|
from urllib.parse import urljoin
|
|
import openpyxl
|
|
from bs4 import BeautifulSoup
|
|
from openpyxl.styles import Alignment
|
|
warnings.filterwarnings('ignore')
|
|
spec = importlib.util.spec_from_file_location('p1', r'D:\01.프로젝트\DB수집\_스크립트\_공공기관_phase1.py')
|
|
m1 = importlib.util.module_from_spec(spec); spec.loader.exec_module(m1)
|
|
XLSX = r'D:\01.프로젝트\DB수집\공공기관\국토안전관리원.xlsx'
|
|
BASE = 'https://www.kalis.or.kr'
|
|
|
|
|
|
def clean(s):
|
|
return re.sub(r'\s+', ' ', (s or '')).strip()
|
|
|
|
|
|
def walk(dl, path, rows):
|
|
dt = dl.find('dt', recursive=False)
|
|
a = dt.find('a') if dt else None
|
|
label = clean(a.get_text()) if a else (clean(dt.get_text()) if dt else '')
|
|
if label in ('전체메뉴 닫기', '닫기', '전체메뉴', ''):
|
|
label = ''
|
|
np = path + [label] if label else path
|
|
dd = dl.find('dd', recursive=False)
|
|
if not dd:
|
|
return
|
|
for ul in dd.find_all('ul', recursive=False):
|
|
for li in ul.find_all('li', recursive=False):
|
|
cdl = li.find('dl', recursive=False)
|
|
if cdl:
|
|
walk(cdl, np, rows)
|
|
else:
|
|
la = li.find('a')
|
|
if la:
|
|
href = (la.get('href') or '').strip()
|
|
if href and not href.startswith(('#', 'javascript')):
|
|
rows.append({'path': np + [clean(la.get_text())], 'href': urljoin(BASE + '/', href)})
|
|
|
|
|
|
def render_expanded(url):
|
|
"""all-menu의 지연로드 섹션을 모두 클릭 펼친 뒤 HTML 반환."""
|
|
from playwright.sync_api import sync_playwright
|
|
with sync_playwright() as p:
|
|
b = p.chromium.launch()
|
|
pg = b.new_page(user_agent=m1.UA, viewport={'width': 1440, 'height': 2400})
|
|
pg.goto(url, timeout=30000, wait_until='domcontentloaded')
|
|
pg.wait_for_timeout(3500)
|
|
# 전체메뉴 열기 버튼
|
|
for sel in ['button[class*=allmenu]', 'a[class*=allmenu]', '[data-selector=allMenu] button', 'button[aria-label*=전체]']:
|
|
try:
|
|
el = pg.query_selector(sel)
|
|
if el:
|
|
el.click(timeout=1500); pg.wait_for_timeout(800)
|
|
except Exception:
|
|
pass
|
|
# 모든 펼침 토글 여러 패스 클릭
|
|
for action in ['depth1', 'depth2', 'dropDown']:
|
|
for _ in range(2):
|
|
els = pg.query_selector_all(f'[data-action={action}]')
|
|
for el in els:
|
|
try:
|
|
el.click(timeout=800); pg.wait_for_timeout(120)
|
|
except Exception:
|
|
pass
|
|
pg.wait_for_timeout(500)
|
|
# 혹시 남은 hidden 제거
|
|
try:
|
|
pg.evaluate("document.querySelectorAll('[hidden]').forEach(e=>e.removeAttribute('hidden'))")
|
|
except Exception:
|
|
pass
|
|
html = pg.content()
|
|
b.close()
|
|
return html
|
|
|
|
|
|
def main():
|
|
html = render_expanded(BASE + '/')
|
|
soup = BeautifulSoup(html, 'html.parser')
|
|
cont = soup.select_one('.all-menu')
|
|
rows = []
|
|
for dl in cont.select('dl.depth-dl'):
|
|
walk(dl, [], rows)
|
|
# URL -> path (대,중,소,leaf...) ; 첫 라벨은 빈문자('사업'의 부모 None일 수 있어 제거)
|
|
url2path = {}
|
|
for r in rows:
|
|
p = [x for x in r['path'] if x]
|
|
url2path[r['href'].rstrip('/')] = p
|
|
print(f'추출 {len(rows)}행, URL맵 {len(url2path)}')
|
|
|
|
shutil.copy(XLSX, XLSX.replace('.xlsx', '_backup_컬럼재구성전.xlsx'))
|
|
wb = openpyxl.load_workbook(XLSX)
|
|
ws = wb.active
|
|
# 데이터행
|
|
END = 2
|
|
for r in range(3, ws.max_row + 1):
|
|
if ws.cell(r, 2).value is None:
|
|
break
|
|
END = r
|
|
# L~Q 스냅샷(검증용)
|
|
snap = {r: [ws.cell(r, c).value for c in range(12, 18)] for r in range(3, END + 1)}
|
|
# 먼저 병합 해제(MergedCell 쓰기금지 회피)
|
|
HEADER = {'B1:R1', 'S1:W1', 'Y1:AA1'}
|
|
for rng in [str(mm) for mm in ws.merged_cells.ranges if str(mm) not in HEADER]:
|
|
ws.unmerge_cells(rng)
|
|
matched = 0
|
|
for r in range(3, END + 1):
|
|
u = (ws.cell(r, 11).value or '').rstrip('/')
|
|
p = url2path.get(u)
|
|
if not p:
|
|
continue
|
|
# D~J 클리어 후 경로 기입(최대 7열 D..J)
|
|
for c in range(4, 11):
|
|
ws.cell(r, c).value = None
|
|
for i, lab in enumerate(p[:7]):
|
|
ws.cell(r, 4 + i).value = lab
|
|
matched += 1
|
|
center = Alignment(horizontal='center', vertical='center', wrap_text=True)
|
|
|
|
def merge(col, ci, groups):
|
|
runs = []; cv = ws.cell(3, ci).value; cg = tuple(ws.cell(3, g).value for g in groups); rs = 3
|
|
for r in range(4, END + 1):
|
|
v = ws.cell(r, ci).value; g = tuple(ws.cell(r, gg).value for gg in groups)
|
|
if v == cv and g == cg:
|
|
continue
|
|
if cv not in (None, '') and r - 1 > rs:
|
|
runs.append((rs, r - 1))
|
|
cv, cg, rs = v, g, r
|
|
if cv not in (None, '') and END > rs:
|
|
runs.append((rs, END))
|
|
for s, e in runs:
|
|
ws.merge_cells(f'{col}{s}:{col}{e}'); ws.cell(s, ci).alignment = center
|
|
merge('F', 6, (4, 5)); merge('E', 5, (4,)); merge('D', 4, ())
|
|
for r in range(3, END + 1):
|
|
for c in (4, 5, 6, 7):
|
|
if ws.cell(r, c).value is not None:
|
|
ws.cell(r, c).alignment = center
|
|
wb.save(XLSX)
|
|
# 검증: L~Q 불변
|
|
wb2 = openpyxl.load_workbook(XLSX, data_only=True); ws2 = wb2.active
|
|
diff = sum(1 for r in range(3, END + 1) if [ws2.cell(r, c).value for c in range(12, 18)] != snap[r])
|
|
print(f'URL매칭 {matched}/{END-2}행 D~J재구성 | L~Q변경 {diff}행(0이어야 정상)')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|