DB_JOB/_스크립트/_공공기관_colfix_kalis.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

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