# -*- coding: utf-8 -*- import openpyxl, copy, sys, shutil, os, datetime from openpyxl.utils import get_column_letter sys.stdout.reconfigure(encoding='utf-8') FN = '한국청소년상담복지개발원.xlsx' BASE = 'https://www.kyci.or.kr' BLUE = 'FFBDD7EE' wb = openpyxl.load_workbook(FN) ws = wb.active MAXR = ws.max_row NCOL = 27 # up to AA # ---- resolve merged effective values ---- merged = list(ws.merged_cells.ranges) def eff(r, c): v = ws.cell(r, c).value if v is not None: return v for rng in merged: if rng.min_row <= r <= rng.max_row and rng.min_col <= c <= rng.max_col: return ws.cell(rng.min_row, rng.min_col).value return None # capture style snapshots for original rows def snap(r): return [copy.copy(ws.cell(r, c)._style) for c in range(1, NCOL+1)] # template data-row style = row 3 TPL = snap(3) # ---- build flat records for rows 3..MAXR ---- # record = dict: vals[1..27] (1-indexed col -> value), style list, blue set (cols to paint blue), is_new def base_record(r): vals = {} for c in range(1, NCOL+1): if 4 <= c <= 10: # D..J use effective (merge-resolved) vals[c] = eff(r, c) else: vals[c] = ws.cell(r, c).value return {'vals': vals, 'style': snap(r), 'blue': set(), 'new': False} def new_record(path_vals, leafcol, label, url, L, M, N, O, ext=False): vals = {c: None for c in range(1, NCOL+1)} vals[3] = '한국청소년상담복지개발원' # C for c in range(4, 11): vals[c] = path_vals.get(c) # inherit D..J path vals[leafcol] = label vals[11] = url # K vals[12] = L vals[13] = M vals[14] = N vals[15] = O rec = {'vals': vals, 'style': [copy.copy(s) for s in TPL], 'blue': set(), 'new': True} rec['blue'] = set(range(2, 16)) # B..O painted blue for new rows return rec # ---- expansion definitions (keyed by original sheet row) ---- def u(p): if p.startswith('http'): return p return BASE + p # children: (label, url, L, M, N, O) EXP = { 14: ('E', '사업소개', [('사업개요','/userSite/sub02_1_info.asp','페이지',1,'어문','미부착'), ('주요내용','/userSite/sub02_1_cont.asp','페이지',1,'어문','미부착')]), 15: ('E', '사업소개', [('사업개요','/userSite/sub02_14_info.asp','페이지',1,'어문','미부착'), ('주요내용','/userSite/sub02_14_cont.asp','페이지',1,'어문','미부착')]), 16: ('E', '사업소개', [('사업개요','/userSite/sub02_2_info.asp','페이지',1,'어문','미부착'), ('주요내용','/userSite/sub02_2_cont.asp','페이지',1,'어문','미부착'), ('홍보자료','/userSite/sub02_2_gallery.asp','페이지',1,'어문,이미지','미부착')]), 17: ('E', '사업소개', [('사업개요','/userSite/sub02_3_info.asp','페이지',1,'어문','미부착'), ('주요내용','/userSite/sub02_3_cont.asp','페이지',1,'어문','미부착'), ('홍보자료','/userSite/sub02_3_gallery.asp','페이지',1,'어문,이미지','미부착'), ('자료실','/userSite/sub02_3_file.asp','페이지',1,'어문','미부착')]), 18: ('E', '사업소개', [('사업개요','/userSite/sub02_4_info.asp','페이지',1,'어문','미부착'), ('주요내용','/userSite/sub02_4_cont.asp','페이지',1,'어문','미부착'), ('자료실','/userSite/sub02_4_file.asp','페이지',1,'어문','미부착')]), 19: ('E', '사업소개', [('사업개요','/userSite/sub02_6_info.asp','페이지',1,'어문','미부착'), ('주요내용','/userSite/sub02_6_cont.asp','페이지',1,'어문','미부착'), ('심리검사','/userSite/sub02_6_test.asp','페이지',1,'어문','미부착'), ('홍보자료','/userSite/sub02_6_gallery.asp','페이지',1,'어문,이미지','미부착')]), 20: ('E', '사업소개', [('사업개요','/userSite/sub02_7_info.asp','페이지',1,'어문','미부착'), ('주요내용','/userSite/sub02_7_cont.asp','페이지',1,'어문','미부착')]), 21: ('E', '사업소개', [('사업개요','/userSite/sub02_8_info.asp','페이지',1,'어문','미부착'), ('주요내용','/userSite/sub02_8_cont.asp','페이지',1,'어문','미부착')]), 22: ('E', '사업소개', [('사업개요','/userSite/sub02_10_info.asp','페이지',1,'어문','미부착'), ('주요내용','/userSite/sub02_10_cont.asp','페이지',1,'어문,이미지','미부착')]), 23: ('E', '사업소개', [('사업개요','/userSite/sub02_11_info.asp','페이지',1,'어문','미부착'), ('주요내용','/userSite/sub02_11_cont.asp','페이지',1,'어문','미부착'), ('권리교육 관련 자료','https://edu.kyci.or.kr/community/edu_files.asp','사이트',None,None,None)]), 24: ('E', '사업소개', [('사업개요','/userSite/sub02_13_info.asp','페이지',1,'어문','미부착'), ('주요내용','/userSite/sub02_13_cont.asp','페이지',1,'어문','미부착'), ('홍보자료','/userSite/sub02_13_gallery.asp','페이지',1,'어문,이미지','미부착')]), 45: ('E', '정보공개 제도란?', [ ('제도의 필요성','/userSite/sub06_1_2.asp','페이지',1,'어문','미부착'), ('정보공개제도 도입배경','/userSite/sub06_1_3.asp','페이지',1,'어문','미부착'), ('정보공개 청구권자','/userSite/sub06_1_4.asp','페이지',1,'어문','미부착'), ('공공기관과 청구인의 의무','/userSite/sub06_1_5.asp','페이지',1,'어문','미부착'), ('정보공개 처리절차','/userSite/sub06_1_6.asp','페이지',1,'어문','미부착'), ('정보공개 방법','/userSite/sub06_1_7.asp','페이지',1,'어문','미부착'), ('정보공개 수수료','/userSite/sub06_1_8.asp','페이지',1,'어문','미부착'), ('불복구제 절차','/userSite/sub06_1_9.asp','페이지',1,'어문','미부착'), ('정보공개 대상 정보와 비공개 정보','/userSite/sub06_1_10.asp','페이지',1,'어문','미부착'), ('관련 법령','/boardManagement/Board.asp?bid=bid_35','게시판',3,'어문','미부착'), ('청구 서식','/boardManagement/Board.asp?bid=bid_36','게시판',6,'어문','미부착')]), 52: ('E', '입찰정보', [('계약현황','/boardManagement/board.asp?bid=bid_6&menuCategory=6','게시판',580,'어문','미부착')]), 77: ('F', '상담직', [('행정직','/userSite/sub060304-2.asp','페이지',1,'어문','미부착'), ('운전직','/userSite/sub060304-3.asp','페이지',1,'어문','미부착')]), } COLIDX = {'D':4,'E':5,'F':6,'G':7,'H':8,'I':9,'J':10} # self-row L/M corrections (입찰정보 is a board, existing was 페이지/1) SELF_FIX = {52: {12:'게시판', 13:175}} records = [] for r in range(3, MAXR+1): rec = base_record(r) if r in EXP: leafletter, self_label, children = EXP[r] leafcol = COLIDX[leafletter] childcol = leafcol + 1 # set self label on the existing row at leaf+1 rec['vals'][childcol] = self_label rec['blue'].add(childcol) if r in SELF_FIX: for c, v in SELF_FIX[r].items(): rec['vals'][c] = v rec['blue'].add(c) path = {c: rec['vals'][c] for c in range(4, 11)} # D..J of parent path[childcol] = None # children carry parent path up to leafcol only records.append(rec) for (label, url, L, M, N, O) in children: pv = {c: path.get(c) for c in range(4, 11)} records.append(new_record(pv, childcol, label, u(url), L, M, N, O)) else: records.append(rec) print('original data rows:', MAXR-2, '-> new total:', len(records)) # ---- write to fresh sheet ---- title = ws.title # copy header rows 1..2 first hdr = {} for r in (1,2): for c in range(1, NCOL+1): cell = ws.cell(r, c) hdr[(r,c)] = (cell.value, copy.copy(cell._style)) hdr_merges = [str(rng) for rng in merged if rng.min_row <= 2] colwidths = {k: v.width for k, v in ws.column_dimensions.items()} del wb[title] ns = wb.create_sheet(title) wb.move_sheet(title, -(len(wb.sheetnames)-1)) # header for (r,c),(v,st) in hdr.items(): cc = ns.cell(r, c); cc.value = v; cc._style = st for m in hdr_merges: ns.merge_cells(m) for col, w in colwidths.items(): if w: ns.column_dimensions[col].width = w # data from openpyxl.styles import PatternFill, Font bluefill = PatternFill(fill_type='solid', fgColor=BLUE, bgColor=BLUE) linkfont = Font(name='맑은 고딕', size=10, color='0000FF', underline='single') for i, rec in enumerate(records): R = 3 + i for c in range(1, NCOL+1): cell = ns.cell(R, c) cell._style = rec['style'][c-1] cell.value = rec['vals'][c] # B renumber continuous ns.cell(R, 2).value = i + 1 # K hyperlink kv = rec['vals'][11] if kv and isinstance(kv, str) and kv.startswith('http'): kc = ns.cell(R, 11) kc.hyperlink = kv kc.font = linkfont # blue paint for c in rec['blue']: ns.cell(R, c).fill = bluefill # ---- re-merge D..J by boundary-aware equal runs ---- DATA0 = 3 DATAN = 3 + len(records) - 1 def val(R, c): return ns.cell(R, c).value for c in range(4, 11): # D..J R = DATA0 while R <= DATAN: v = val(R, c) if v is None or v == '': R += 1; continue r2 = R while r2+1 <= DATAN and val(r2+1, c) == v and all(val(r2+1, pc) == val(R, pc) for pc in range(4, c)): r2 += 1 if r2 > R: for rr in range(R+1, r2+1): ns.cell(rr, c).value = None ns.merge_cells(start_row=R, start_column=c, end_row=r2, end_column=c) R = r2 + 1 # freeze panes ns.freeze_panes = 'A3' # ---- save with backup ---- bdir = '_backup' os.makedirs(bdir, exist_ok=True) ts = datetime.datetime.fromtimestamp(os.path.getmtime(FN)).strftime('%Y%m%d_%H%M%S') shutil.copy2(FN, os.path.join(bdir, f'_탭전개전_{ts}.xlsx')) wb.save(FN) print('SAVED', FN, 'rows', DATAN)