공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
180 lines
6.2 KiB
Python
180 lines
6.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""335~443행 분해 계획 산출(읽기전용).
|
|
- 미분해 F섹션(type1 탭 URL이 시트에 1개=자기자신만 존재) → 그 type1 탭들로 분해
|
|
- 이미 분해된 잎 행의 깊은 중첩(type3가 별도 URL, 시트에 없음) → 그 type3로 분해
|
|
- type3가 #nav 앵커면 분해 안 함(M=앵커수)
|
|
재귀로 깊은 단계까지. 결과: 분해 잡 목록 + 총 증가행수.
|
|
"""
|
|
import re, json, warnings
|
|
from urllib.parse import urlparse
|
|
import requests
|
|
from bs4 import BeautifulSoup
|
|
import openpyxl
|
|
warnings.filterwarnings('ignore')
|
|
|
|
XLSX = r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\2.공주시\충청남도_공주시.xlsx'
|
|
BASE = 'https://www.gongju.go.kr'
|
|
H = {'User-Agent': 'Mozilla/5.0 Chrome/120 Safari/537.36'}
|
|
LO, HI = 335, 443
|
|
_cache = {}
|
|
|
|
|
|
def full(h):
|
|
return h if h.startswith('http') else BASE + h
|
|
|
|
|
|
def norm(u):
|
|
"""시트 멤버십 비교용 정규화: host 제거, path(+bbs id)만"""
|
|
p = urlparse(u if u.startswith('http') else BASE + u)
|
|
path = p.path
|
|
return path
|
|
|
|
|
|
def get(u):
|
|
u = full(u)
|
|
if u in _cache:
|
|
return _cache[u]
|
|
try:
|
|
x = requests.get(u, headers=H, timeout=25, verify=False)
|
|
x.encoding = x.apparent_encoding or 'utf-8'
|
|
html = x.text
|
|
except Exception:
|
|
html = ''
|
|
_cache[u] = html
|
|
return html
|
|
|
|
|
|
def tab_groups(html):
|
|
s = BeautifulSoup(html, 'html.parser')
|
|
t1 = []
|
|
t3 = []
|
|
for ul in s.find_all('ul'):
|
|
cls = ' '.join(ul.get('class') or [])
|
|
if 'tab-ul' not in cls.lower():
|
|
continue
|
|
items = [(a.get_text(strip=True), a.get('href', '')) for a in ul.find_all('a')
|
|
if a.get_text(strip=True) and a.get('href')]
|
|
if not items:
|
|
continue
|
|
if 'type1' in cls:
|
|
t1 = items
|
|
elif not t3:
|
|
t3 = items
|
|
return t1, t3
|
|
|
|
|
|
def board_count(html):
|
|
s = BeautifulSoup(html, 'html.parser')
|
|
el = s.select_one('.program--count strong')
|
|
if el:
|
|
m = re.sub(r'[^0-9]', '', el.get_text())
|
|
return int(m) if m else None
|
|
return None
|
|
|
|
|
|
def is_board(u):
|
|
return bool(re.search(r'list\.do|/bbs/|BBSMSTR', u))
|
|
|
|
|
|
def leaf_info(name, u):
|
|
"""잎 1개의 (L,M) 결정 + 그 잎의 #nav 앵커수 반영"""
|
|
fu = full(u)
|
|
html = get(fu)
|
|
if is_board(u):
|
|
c = board_count(html)
|
|
return {'name': name, 'url': fu, 'L': '게시판', 'M': c if c is not None else 0}
|
|
# 페이지: 자기 type3가 #nav면 M=앵커수
|
|
_, t3 = tab_groups(html)
|
|
navs = [h for t, h in t3 if h.startswith('#')]
|
|
if len(navs) >= 2:
|
|
return {'name': name, 'url': fu, 'L': '페이지', 'M': len(navs)}
|
|
return {'name': name, 'url': fu, 'L': '페이지', 'M': 1}
|
|
|
|
|
|
def url_type3(u):
|
|
"""페이지의 type3 별도URL 탭들(없으면 [])"""
|
|
if is_board(u):
|
|
return []
|
|
_, t3 = tab_groups(get(full(u)))
|
|
return [(t, full(h)) for t, h in t3 if not h.startswith('#')]
|
|
|
|
|
|
def expand_nested(name, u, sheet_urls, depth=0):
|
|
"""깊이 2 평탄 분해(공주 탭 최대 2단계). 자식이 url-type3를 가지면 1단계만 더 펼침."""
|
|
grand = url_type3(u) if depth == 0 else []
|
|
if len(grand) >= 2:
|
|
return {'name': name, 'url': full(u),
|
|
'children': [leaf_info(t, h) for t, h in grand]}
|
|
return leaf_info(name, u)
|
|
|
|
|
|
def main():
|
|
wb = openpyxl.load_workbook(XLSX)
|
|
ws = wb.active
|
|
sheet_urls = set()
|
|
for r in range(3, 445):
|
|
u = ws.cell(r, 11).value
|
|
if isinstance(u, str) and u.startswith('http'):
|
|
sheet_urls.add(norm(u))
|
|
|
|
jobs = []
|
|
for r in range(LO, HI + 1):
|
|
u = ws.cell(r, 11).value
|
|
if not (isinstance(u, str) and u.startswith('http')):
|
|
continue
|
|
html = get(u)
|
|
t1, t3 = tab_groups(html)
|
|
t1_urls = [(t, full(h)) for t, h in t1 if not h.startswith('#')]
|
|
present = sum(1 for t, h in t1_urls if norm(h) in sheet_urls)
|
|
url_t3 = [(t, full(h)) for t, h in t3 if not h.startswith('#')]
|
|
|
|
label = (ws.cell(r, 6).value or '')
|
|
if ws.cell(r, 7).value:
|
|
label += ' > ' + str(ws.cell(r, 7).value)
|
|
if ws.cell(r, 8).value:
|
|
label += ' > ' + str(ws.cell(r, 8).value)
|
|
|
|
# 미분해 F섹션: type1 URL>=2 이고 시트에 자기 1개만 → type1 자식(각자 type3 1단계 더)
|
|
if len(t1_urls) >= 2 and present <= 1:
|
|
children = [expand_nested(t, h, sheet_urls, depth=0) for t, h in t1_urls]
|
|
jobs.append(('SECTION', r, label, children))
|
|
# 이미 분해된 G-잎의 깊은 type3 중첩 → type3 자식(잎)
|
|
elif len(url_t3) >= 2:
|
|
children = [leaf_info(t, h) for t, h in url_t3]
|
|
jobs.append(('NEST', r, label, children))
|
|
|
|
# 요약
|
|
def count_leaves(nodes):
|
|
n = 0
|
|
for nd in nodes:
|
|
if nd.get('children'):
|
|
n += count_leaves(nd['children'])
|
|
else:
|
|
n += 1
|
|
return n
|
|
|
|
total_add = 0
|
|
print('=== 분해 계획 (335~443) ===')
|
|
for kind, r, label, children in jobs:
|
|
leaves = count_leaves(children)
|
|
add = leaves - 1 # 기존 1행 대체
|
|
total_add += add
|
|
print('\n[%s] r%d %s → 잎 %d (기존1, +%d)' % (kind, r, label, leaves, add))
|
|
def pr(nodes, ind=1):
|
|
for nd in nodes:
|
|
tag = ('게시판%s' % nd['M']) if nd.get('L') == '게시판' else ('페이지M%s' % nd.get('M', ''))
|
|
if nd.get('children'):
|
|
print(' ' * ind + '▸ %s' % nd['name'])
|
|
pr(nd['children'], ind + 1)
|
|
else:
|
|
print(' ' * ind + '- %s [%s]' % (nd['name'], tag))
|
|
pr(children)
|
|
print('\n총 분해 잡 %d개, 총 증가 행수 +%d (최종 데이터행 ~ %d)' % (len(jobs), total_add, 443 + total_add))
|
|
json.dump([{'kind': k, 'row': r, 'label': l, 'children': c} for k, r, l, c in jobs],
|
|
open(r'D:\01.프로젝트\DB수집\_스크립트\_split_plan.json', 'w', encoding='utf-8'),
|
|
ensure_ascii=False, indent=1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|