DB_JOB/_스크립트/_geumsan_subtree_M.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

92 lines
4.0 KiB
Python

# -*- coding: utf-8 -*-
"""금산군 메뉴 랜딩행 M = 서브트리 전체 leaf 합 (2026-05-31, 사용자 규칙)
여성가족=10 패턴: 메뉴(6자리 prefix)의 모든 자식페이지 X01..X0N에 대해
leaf = 라이브 ui-nav_tabs 개수 (탭 없으면 1)
M(랜딩행) = Σ leaf. 단 메뉴의 자식 중 게시판이 있으면 SKIP(분리 필요, 수동).
대상: 랜딩행 >= 143(여성가족) 이고 그 prefix의 시트행이 1개(sheet=1)인 메뉴.
sheet>1(이미 G확장됨: 군민복지050307·하수처리050601·주민참여060603)은 SKIP.
"""
import sys, re, json, openpyxl
import urllib.request, ssl
from bs4 import BeautifulSoup
from concurrent.futures import ThreadPoolExecutor
PATH = r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\3.금산군\충청남도_금산군.xlsx'
WRITE = '--write' in sys.argv
ctx = ssl.create_default_context(); ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE
def fetch(u):
req = urllib.request.Request(u, headers={'User-Agent': 'Mozilla/5.0'})
try:
return urllib.request.urlopen(req, context=ctx, timeout=15).read().decode('utf-8', 'replace')
except Exception:
return None
def analyze(code):
sub = 'sub06' if code.startswith('06') else 'sub05'
h = fetch(f'https://www.geumsan.go.kr/kr/html/{sub}/{code}.html')
if h is None:
return None
soup = BeautifulSoup(h, 'html.parser')
t = soup.select_one('.location_wrap li:last-child, h2.h2')
title = t.get_text(strip=True) if t else '?'
ntab = len(soup.select('ul.ui-nav_tabs a.ui-tabs_link'))
board = bool(soup.select('table.bbs,.board_list,.bbs_list,.pagination')) or bool(re.search(r'\s*[\d,]+\s*건', str(soup)))
return dict(code=code, title=title, ntab=ntab, board=board, leaf=max(1, ntab))
wb = openpyxl.load_workbook(PATH); ws = wb.active
# prefix -> sheet rows (sub05/sub06 8-digit)
pref_rows = {}
for r in range(3, ws.max_row + 1):
k = ws.cell(r, 11).value
if not k:
continue
m = re.search(r'/sub0[56]/(\d{6})(\d{2})\.html', str(k))
if m:
pref_rows.setdefault(m.group(1), []).append((r, m.group(0)))
targets = {}
for pfx, rows in pref_rows.items():
landing = min(r for r, _ in rows)
if landing < 143:
continue # 여성가족(143) 위는 제외
if len(rows) != 1:
print(f'SKIP {pfx} (sheet={len(rows)} rows={[r for r,_ in rows]}) — 이미 확장됨')
continue
targets[pfx] = landing
print(f'대상 sheet=1 메뉴: {len(targets)}')
plan = []
for pfx, landing in sorted(targets.items(), key=lambda x: x[1]):
codes = [f'{pfx}{xx:02d}' for xx in range(1, 16)]
kids = [k for k in ThreadPoolExecutor(8).map(analyze, codes) if k]
leaf_sum = sum(k['leaf'] for k in kids)
nboard = sum(1 for k in kids if k['board'])
cur_m = ws.cell(landing, 13).value
cur_f = ws.cell(landing, 6).value or ws.cell(landing, 7).value
flag = ' ⚠GESIPAN' if nboard else ''
plan.append(dict(pfx=pfx, row=landing, m_old=cur_m, m_new=leaf_sum, nkids=len(kids), nboard=nboard, kids=kids))
print(f'row{landing} {pfx} [{cur_f}] M {cur_m}->{leaf_sum} (자식{len(kids)},board{nboard}){flag}')
if nboard:
for k in kids:
print(f' {k["code"]} leaf{k["leaf"]} board={k["board"]} {k["title"][:20]}')
# 적용: 게시판 없는 메뉴만 M 갱신
applied = 0
for p in plan:
if p['nboard'] == 0:
ws.cell(p['row'], 13).value = p['m_new']
applied += 1
else:
print(f' ⚠ row{p["row"]} {p["pfx"]} 게시판 포함 → M 미적용(수동 검토)')
print(f'적용 대상(게시판없음): {applied}/{len(plan)}')
json.dump([{k: v for k, v in p.items() if k != 'kids'} for p in plan],
open(r'D:\01.프로젝트\DB수집\_temp\_geumsan_subtreeM_plan.json', 'w', encoding='utf8'), ensure_ascii=False)
if WRITE:
wb.save(PATH); print('SAVED')
else:
print('DRY-RUN (--write 로 저장)')