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