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")