DB_JOB/작업파일/완료_1-2주차/1주차(5.19~5.25)/과학기술정보통신부(완료)/엑셀만들기.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

294 lines
11 KiB
Python

import re
from html.parser import HTMLParser
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
# ==========================================
# 1. HTML 데이터 및 설정 정의
# ==========================================
BASE_URL = "https://www.msit.go.kr"
SITE_NAME = "과학기술정보통신부"
# 패턴 변환용 딕셔너리 (HTML 내 ID 기반 매핑 추정치 및 사용자가 준 명시적 규칙 결합)
# 패턴: fn_menulast_go('user', '상위Pid', '자신Id', '경로')
# 기본 경로 처리 규칙 적용
def parse_href(href):
if not href or "javascript:;" in href:
return ""
# 정규식을 이용해 fn_menulast_go 함수의 인자 추출
match = re.search(r"fn_menulast_go\s*\(\s*'([^']*)'\s*,\s*'([^']*)'\s*,\s*'([^']*)'\s*,\s*'([^']*)'\s*\)", href)
if match:
sCode, mPid, mId, path = match.groups()
if path == "#" or path == "":
return f"{BASE_URL}/contents/cont.do?sCode={sCode}&mPid={mPid}&mId={mId}"
else:
if path.startswith("http"):
return path
return f"{BASE_URL}{path}?sCode={sCode}&mPid={mPid}&mId={mId}"
if href.startswith("http"):
return href
if href.startswith("/"):
return f"{BASE_URL}{href}"
return href
# ==========================================
# 2. 내장 HTML Parser를 이용한 트리 구조 추출
# ==========================================
class GNBParser(HTMLParser):
def __init__(self):
super().__init__()
self.menu_tree = []
self.current_d1 = None
self.current_d2 = None
self.current_d3 = None
self.in_a = False
self.current_href = ""
# 계층 식별을 위한 플래그
self.state_stack = [] # (tag, class_name)
def handle_starttag(self, tag, attrs):
attrs_dict = dict(attrs)
cls = attrs_dict.get("class", "")
href = attrs_dict.get("href", "")
self.state_stack.append((tag, cls))
if tag == "a":
self.in_a = True
self.current_href = parse_href(href)
def handle_endtag(self, tag):
if tag == "a":
self.in_a = False
if self.state_stack:
self.state_stack.pop()
def handle_data(self, data):
text = data.strip()
if not text or not self.in_a:
return
# 현재 스택을 기반으로 Depth 판별
classes = [c for t, c in self.state_stack]
# Depth 1 판별 (gnb 바로 밑의 span이나 a)
if "depth_box" not in ''.join(classes) and any("gnb" in c for c in classes):
self.current_d1 = {"text": text, "url": self.current_href, "children": []}
self.menu_tree.append(self.current_d1)
self.current_d2 = None
self.current_d3 = None
# Depth 2 판별 (class="depth" 내부의 li > a)
elif "depth" in classes and "depth02" not in classes:
if self.current_d1:
self.current_d2 = {"text": text, "url": self.current_href, "children": []}
self.current_d1["children"].append(self.current_d2)
self.current_d3 = None
# Depth 3 판별 (class="depth02" 내부의 li > a)
elif "depth02" in classes:
if self.current_d2:
self.current_d3 = {"text": text, "url": self.current_href, "children": []}
self.current_d2["children"].append(self.current_d3)
# 사용자 입력 데이터 파싱 실행
html_data = """[사용자가 제공한 HTML 소스코드 문장 생략 - 파싱용 변수 배치]"""
# 실제 실행 시에는 상단의 HTML 텍스트가 들어갑니다.
parser = GNBParser()
# 줄바꿈 깨짐 방지 처리 후 피딩
parser.feed(html_data)
# 평탄화(Flatten)하여 로우 데이터 배열 생성
rows_data = []
for d1 in parser.menu_tree:
d1_txt = d1["text"]
d1_url = d1["url"]
if not d1["children"]:
rows_data.append([d1_txt, "", "", "", d1_url])
continue
for d2 in d1["children"]:
d2_txt = d2["text"]
d2_url = d2["url"]
if not d2["children"]:
rows_data.append([d1_txt, d2_txt, "", "", d2_url if d2_url else d1_url])
continue
for d3 in d2["children"]:
d3_txt = d3["text"]
d3_url = d3["url"]
rows_data.append([d1_txt, d2_txt, d3_txt, "", d3_url if d3_url else d2_url])
# ==========================================
# 3. OpenPyXL 기반 엑셀 파일 생성 및 디자인
# ==========================================
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "사이트맵_감사양식"
# 1) 눈금선 활성화 설정 (필수 구문)
ws.views.sheetView[0].showGridLines = True
# 2) 스타일 테마 정의
font_title = Font(name="맑은 고딕", size=11, bold=True)
font_red_bold = Font(name="맑은 고딕", size=11, bold=True, color="FF0000")
font_header = Font(name="맑은 고딕", size=11, bold=True)
font_url_header = Font(name="맑은 고딕", size=11, bold=True, color="375623")
font_data = Font(name="맑은 고딕", size=10)
fill_green = PatternFill(start_color="E2EFDA", end_color="E2EFDA", fill_type="solid")
fill_orange = PatternFill(start_color="FFF2CC", end_color="FFF2CC", fill_type="solid")
fill_blue = PatternFill(start_color="D9E1F2", end_color="D9E1F2", fill_type="solid")
align_center = Alignment(horizontal="center", vertical="center", wrap_text=True)
align_left = Alignment(horizontal="left", vertical="center")
border_thin = Border(
left=Side(style='thin', color='A6A6A6'),
right=Side(style='thin', color='A6A6A6'),
top=Side(style='thin', color='A6A6A6'),
bottom=Side(style='thin', color='A6A6A6')
)
# 3) 헤더 배치 및 서식 적용
# 1행 그룹 타이틀
ws["G1"] = "조사원 업무 범위"
ws["G1"].font = font_title
ws["R1"] = "문정원/ITN 업무(신청기관 100개 기관 대상)"
ws["R1"].font = font_red_bold
# 2~3행 복합 헤더 기입
headers_v2 = {
"B2": "순번", "C2": "사이트명", "D2": "메뉴명",
"E2": "게시판명/카테고리(대)", "F2": "게시판명/카테고리(중)",
"G2": "게시판명/카테고리(소)", "H2": "게시판명/카테고리(소-세부사항)",
"I2": "주소(url)", "J2": "게시판형태", "K2": "수량",
"L2": "저작물 유형", "M2": "공공누리 부착", "N2": "마크 부착 위치",
"O2": "마크 하이퍼링크", "P2": "공공누리 연계", "Q2": "비고(특이사항)",
"R2": "신유형 적용 여부", "S2": "적용 사유", "T2": "검토 대상",
"U2": "안내 발송(문정원/ITN)"
}
for cell_id, text in headers_v2.items():
ws[cell_id] = text
ws.merge_cells("W2:X2")
ws["W2"] = "결과"
ws["W3"] = "완료여부(Y,N)"
ws["X3"] = "수집 담당자"
# 헤더 영역 색상 및 정렬 채우기
for col in range(2, 25): # B열(2) ~ X열(24)
col_letter = get_column_letter(col)
if col_letter == 'V': continue
# 2행 처리
cell_2 = ws[f"{col_letter}2"]
cell_2.alignment = align_center
cell_2.border = border_thin
# 3행 처리
cell_3 = ws[f"{col_letter}3"]
cell_3.border = border_thin
if col >= 22: # W, X열의 3행 헤더 정렬
cell_3.alignment = align_center
# 영역별 색상 매핑
if 2 <= col <= 17: # B~Q: 조사원 범위
cell_2.fill = fill_green
cell_2.font = font_url_header if col_letter == 'I' else font_header
cell_3.fill = fill_green
elif 18 <= col <= 21: # R~U: 문정원
cell_2.fill = fill_orange
cell_2.font = font_header
cell_3.fill = fill_orange
elif col >= 23: # W~X: 결과
cell_2.fill = fill_blue
cell_2.font = font_header
cell_3.fill = fill_blue
# 4) 데이터 채우기 (실데이터는 B~U열은 3행부터 시작, W~X열은 4행 대응 보정)
start_row = 3
for idx, data in enumerate(rows_data, start=1):
r = start_row + idx - 1
ws[f"B{r}"] = idx # 순번
ws[f"C{r}"] = SITE_NAME # 사이트명
ws[f"D{r}"] = data[0] # 메뉴명 (depth1)
ws[f"E{r}"] = data[1] # 카테고리 대 (depth2)
ws[f"F{r}"] = data[2] # 카테고리 중 (depth3)
ws[f"G{r}"] = data[3] # 카테고리 소 (depth4)
ws[f"I{r}"] = data[4] # 주소(url)
# 기본 데이터 스타일 및 기본값 초기화 서식 부여
for col in range(2, 25):
col_letter = get_column_letter(col)
if col_letter == 'V': continue
cell = ws[f"{col_letter}{r}"]
cell.font = font_data
cell.border = border_thin
# 정렬 규칙: I열(주소)만 좌측, 나머지는 무조건 가운데 정렬
if col_letter == 'I':
cell.alignment = align_left
else:
cell.alignment = align_center
end_row = start_row + len(rows_data) - 1
# 5) 상위 부모 일치 여부를 판별하는 정교한 계층형 셀 병합 함수
def merge_hierarchical_column(sheet, col_letter, parent_cols, start_r, end_r):
current_start = start_r
for r in range(start_r + 1, end_r + 1):
# 현재 행과 이전 행의 본인 값 및 모든 상위 부모의 값이 일치하는지 체크
match = True
for p_col in [col_letter] + parent_cols:
if sheet[f"{p_col}{r}"].value != sheet[f"{p_col}{current_start}"].value:
match = False
break
if not match:
if r - 1 > current_start:
sheet.merge_cells(f"{col_letter}{current_start}:{col_letter}{r-1}")
current_start = r
if end_r > current_start:
sheet.merge_cells(f"{col_letter}{current_start}:{col_letter}{end_r}")
# 계층 병합 실행
merge_hierarchical_column(ws, "C", [], start_row, end_row) # 사이트명 전 범위 병합
merge_hierarchical_column(ws, "D", ["C"], start_row, end_row) # 메뉴명
merge_hierarchical_column(ws, "E", ["C", "D"], start_row, end_row) # 카테고리(대)
merge_hierarchical_column(ws, "F", ["C", "D", "E"], start_row, end_row) # 카테고리(중)
merge_hierarchical_column(ws, "G", ["C", "D", "E", "F"], start_row, end_row) # 카테고리(소)
# 6) 한국어 가중치(2)를 적용한 열 너비 자동 최적화 공식
for col in range(2, 25):
col_letter = get_column_letter(col)
if col_letter == 'V':
ws.column_dimensions[col_letter].width = 3
continue
max_len = 0
for r in range(2, end_row + 1):
val = ws[f"{col_letter}{r}"].value
if val is not None:
val_str = str(val)
# 한글(128자 이상 익스텐션 코딩 스페이스) 가중치 2 연산식
length = sum(2 if ord(char) > 128 else 1 for char in val_str)
if length > max_len:
max_len = length
ws.column_dimensions[col_letter].width = max(max_len + 4, 13)
# 엑셀 파일 영구 저장
wb.save("과학기술정보통신부_사이트맵_감사양식.xlsx")