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

243 lines
9.0 KiB
Python

"""공주시 Phase 1 — 사이트맵 → 엑셀 (D~K + 병합·정렬·스타일).
매뉴얼: D:\\01.프로젝트\\DB수집\\사이트맵_수집_매뉴얼.md
"""
import re
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://www.gongju.go.kr'
SITEMAP_URL = 'https://www.gongju.go.kr/kr/sitemap.do'
SHEET_NAME = '02_공주시'
TEMPLATE = r'D:\01.프로젝트\DB수집\자료_취합_예시.xlsx'
OUTPUT = r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\2.공주시\충청남도_공주시.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}
ONCLICK_PAT = re.compile(r"location\.href\s*=\s*encodeURI\(\s*['\"]([^'\"]+)['\"]\s*\)")
def extract_href(a):
"""공주시는 href가 #enURI..., 실제 URL은 onclick의 encodeURI 인자."""
href = (a.get('href') or '').strip()
if href and not href.startswith('#') and not href.startswith('javascript:'):
return href
onclick = a.get('onclick') or ''
m = ONCLICK_PAT.search(onclick)
if m:
return m.group(1)
return ''
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 'gongju.go.kr' not in url
def walk_ul(ul, depth, base_path, out):
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')
dls = soup.select('.sitemap dl')
# 헤더 메뉴 dl 같은 노이즈가 섞일 수 있어 dt에 a 있고 dd 있는 것만
dls = [dl for dl in dls if dl.find('dt') and dl.find('dd')]
print(f' 대분류 dl: {len(dls)}')
raw_rows = []
for dl in dls:
dt = dl.find('dt')
dt_a = dt.find('a') if dt else None
menu_name = dt_a.get_text(strip=True) if dt_a else ''
for dd in dl.find_all('dd', recursive=False):
b = dd.find('b')
b_a = b.find('a') if b else None
mid_name = b_a.get_text(strip=True) if b_a else ''
mid_href = extract_href(b_a) if b_a else ''
nested = dd.find('ul', recursive=False)
if nested:
tmp = []
walk_ul(nested, 0, [], tmp)
for item in tmp:
p = item['path']
row = {'D': menu_name, '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)
else:
raw_rows.append({'D': menu_name, 'E': mid_name, 'href': mid_href,
'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
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)
# 병합 순서 F→E→D (D를 먼저 병합하면 2행부터 D=None이 되어 E 그룹키가 깨짐)
n_f = merge_runs('F', 6, group_cols=(4, 5))
n_e = merge_runs('E', 5, group_cols=(4,))
n_d = merge_runs('D', 4)
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(f'대분류별 행수:')
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()