DB_JOB/작업파일/완료/광역_사이트맵/충청남도/4.논산시/_phase1.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

245 lines
9.2 KiB
Python

"""논산시 Phase 1 — 사이트맵 → 엑셀 (D~K + 병합·정렬·스타일).
매뉴얼: D:\\01.프로젝트\\DB수집\\사이트맵_수집_매뉴얼.md
사이트맵 구조: div.sitemap.type1 > [div.s_1th > a (D)] + [div.inner > div.s_2th > a (E) + ul > li > a (F) + (ul.s_4th_ul > li > a (G))]*
"""
import shutil
import warnings
from copy import copy
from urllib.parse import urljoin
import openpyxl
import requests
from bs4 import BeautifulSoup
from openpyxl.styles import Alignment, Font
warnings.filterwarnings('ignore')
INSTITUTION = '논산시'
BASE = 'https://nonsan.go.kr'
SITEMAP_URL = 'https://nonsan.go.kr/kor/html/sub07/0701.html'
SHEET_NAME = '04_논산시'
TEMPLATE = r'D:\01.프로젝트\DB수집\자료_취합_예시.xlsx'
OUTPUT = r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\4.논산시\충청남도_논산시.xlsx'
UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
H = {'User-Agent': UA}
def extract_href(a):
if a is None:
return ''
href = (a.get('href') or '').strip()
if not href or href.startswith('#') or href.lower().startswith('javascript:'):
return ''
return href
def abs_url(href):
if not href:
return ''
href = href.strip()
if href.startswith(('javascript:', '#')):
return ''
if href.startswith(('http://', 'https://')):
return href
return urljoin(BASE + '/', href)
def is_external(url):
return url.startswith(('http://', 'https://')) and 'nonsan.go.kr' not in url
def walk_ul(ul, depth, base_path, out):
"""Recursive walk for nested ul > li > a (s_4th_ul handling)."""
for li in ul.find_all('li', recursive=False):
a = li.find('a', recursive=False)
if not a:
continue
text = a.get_text(strip=True).replace('\xa0', '').strip()
href = extract_href(a)
path = base_path[:depth] + [(text, href)]
nested = li.find('ul', recursive=False)
if nested:
out.append({'path': list(path), 'href': href})
walk_ul(nested, depth + 1, path, out)
else:
out.append({'path': list(path), 'href': href})
def main():
print('[1] 사이트맵 가져오기')
r = requests.get(SITEMAP_URL, headers=H, timeout=20, verify=False)
r.encoding = r.apparent_encoding
soup = BeautifulSoup(r.text, 'html.parser')
sitemap = soup.select_one('div.sitemap.type1') or soup.select_one('div.sitemap')
if sitemap is None:
raise RuntimeError('div.sitemap.type1 영역을 찾지 못했습니다.')
raw_rows = []
# Iterate child elements in document order:
# div.s_1th = 대분류 (D)
# div.inner = 중분류 + 소분류 그룹
current_D = ''
for child in sitemap.find_all(['div'], recursive=False):
cls = child.get('class', [])
if 's_1th' in cls:
a = child.find('a')
current_D = a.get_text(strip=True) if a else ''
elif 'inner' in cls:
# div.s_2th > a → 중분류 (E)
s2 = child.find('div', class_='s_2th')
mid_a = s2.find('a') if s2 else None
mid_name = mid_a.get_text(strip=True) if mid_a else ''
mid_href = extract_href(mid_a) if mid_a else ''
# Each direct ul under .inner is a separate group of leaf items (F)
uls = child.find_all('ul', recursive=False)
if not uls:
raw_rows.append({'D': current_D, 'E': mid_name, 'href': mid_href,
'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
continue
for ul in uls:
tmp = []
walk_ul(ul, 0, [], tmp)
for item in tmp:
p = item['path']
row = {'D': current_D, 'E': mid_name, 'href': item['href'],
'F': '', 'G': '', 'H': '', 'I': '', 'J': ''}
for di, (t, _) in enumerate(p):
col = 'FGHIJ'[di] if di < 5 else 'J'
row[col] = t
raw_rows.append(row)
print(f'[2] 원본 행: {len(raw_rows)}')
# 부모-자식 URL 중복 제거
final_rows = []
i = 0
removed = 0
while i < len(raw_rows):
row = raw_rows[i]
if (i + 1 < len(raw_rows)
and row.get('G', '') == ''
and raw_rows[i + 1].get('D') == row.get('D')
and raw_rows[i + 1].get('E') == row.get('E')
and raw_rows[i + 1].get('F') == row.get('F')
and raw_rows[i + 1].get('G', '') != ''
and raw_rows[i + 1].get('href') == row.get('href')):
removed += 1
i += 1
continue
final_rows.append(row)
i += 1
print(f'[3] 부모-자식 URL 중복 삭제: {removed}개 → 최종 {len(final_rows)}')
print('[4] 엑셀 템플릿 복사')
shutil.copy(TEMPLATE, OUTPUT)
wb = openpyxl.load_workbook(OUTPUT)
ws = wb.active
ws.title = SHEET_NAME
HEADER = {'B1:R1', 'S1:W1', 'Y1:AA1'}
for rng in [str(m) for m in ws.merged_cells.ranges if str(m) not in HEADER]:
ws.unmerge_cells(rng)
for row in ws.iter_rows(min_row=3, max_row=ws.max_row, min_col=1, max_col=ws.max_column):
for cell in row:
cell.value = None
START = 3
template_r = 3
cur_max = ws.max_row
for idx, item in enumerate(final_rows, start=START):
if idx > cur_max:
for c in range(1, ws.max_column + 1):
srcc = ws.cell(template_r, c)
tgt = ws.cell(idx, c)
if srcc.has_style:
tgt.font = copy(srcc.font)
tgt.fill = copy(srcc.fill)
tgt.border = copy(srcc.border)
tgt.alignment = copy(srcc.alignment)
tgt.number_format = srcc.number_format
tgt.protection = copy(srcc.protection)
url = abs_url(item.get('href', ''))
ws.cell(idx, 2).value = idx - 2
ws.cell(idx, 3).value = INSTITUTION
ws.cell(idx, 4).value = item.get('D', '')
ws.cell(idx, 5).value = item.get('E', '')
ws.cell(idx, 6).value = item.get('F', '')
ws.cell(idx, 7).value = item.get('G', '')
ws.cell(idx, 8).value = item.get('H', '')
ws.cell(idx, 9).value = item.get('I', '')
ws.cell(idx, 10).value = item.get('J', '')
ws.cell(idx, 11).value = url
if is_external(url):
ws.cell(idx, 19).value = '외부링크'
END = START + len(final_rows) - 1
print(f'[5] 데이터 기입: {START}~{END}')
center = Alignment(horizontal='center', vertical='center', wrap_text=True)
left = Alignment(horizontal='left', vertical='center', wrap_text=False)
def merge_runs(col_letter, col_idx, group_cols=()):
runs = []
cur_val = ws.cell(START, col_idx).value
cur_grp = tuple(ws.cell(START, g).value for g in group_cols)
run_start = START
for r in range(START + 1, END + 1):
v = ws.cell(r, col_idx).value
g = tuple(ws.cell(r, gg).value for gg in group_cols)
if v == cur_val and g == cur_grp:
continue
if cur_val not in (None, '') and r - 1 > run_start:
runs.append((run_start, r - 1))
cur_val, cur_grp, run_start = v, g, r
if cur_val not in (None, '') and END > run_start:
runs.append((run_start, END))
for s, e in runs:
ws.merge_cells(f'{col_letter}{s}:{col_letter}{e}')
ws.cell(s, col_idx).alignment = center
return len(runs)
n_d = merge_runs('D', 4)
n_e = merge_runs('E', 5, group_cols=(4,))
n_f = merge_runs('F', 6, group_cols=(4, 5))
for r in range(START, END + 1):
for c in (4, 5, 6, 7):
if ws.cell(r, c).value is not None:
ws.cell(r, c).alignment = center
print(f'[6] 병합 — D:{n_d} E:{n_e} F:{n_f}')
for r in range(1, END + 1):
ws.row_dimensions[r].height = 15
link_n = 0
for r in range(START, END + 1):
cell = ws.cell(r, 11)
u = cell.value
if u and isinstance(u, str) and u.startswith(('http://', 'https://')):
cell.hyperlink = u
old = cell.font
cell.font = Font(name=old.name or '맑은 고딕', size=old.size or 11,
bold=old.bold, italic=old.italic,
color='0000FF', underline='single')
cell.alignment = left
link_n += 1
print(f'[7] 행 높이 15 + K 하이퍼링크 {link_n}개 (좌측 정렬)')
wb.save(OUTPUT)
print(f'[8] 저장: {OUTPUT}')
print('\n=== 통계 ===')
from collections import Counter
cnt = Counter(item.get('D') for item in final_rows)
print('대분류별 행수:')
for k, v in cnt.items():
print(f' {k}: {v}')
ext_n = sum(1 for it in final_rows if is_external(abs_url(it.get('href', ''))))
print(f'외부링크: {ext_n}')
if __name__ == '__main__':
main()