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