# -*- coding: utf-8 -*- """김제시 재검토: 게시판↔페이지 재분류 + 페이지 인페이지탭(basic_tab) 부모 병합. - 판별: 본문에 게시판 리스트 클래스(bbs_list/news_list/photo_list/video_list/magazine_list/gallery_list/board_list) → 게시판, 없으면 페이지. - 외부 리다이렉트/ERR 행(새창열림 외부시스템)은 손대지 않음. - basic_tab 탭그룹: 페이지 탭만 부모(페이지)로 접고 M=페이지탭수. 게시판 탭은 별도 행 유지. 사용: python -X utf8 _김제_재분류.py dry | run """ import os, sys, re, io, hashlib, shutil, importlib.util, warnings, pickle warnings.filterwarnings('ignore') sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') import openpyxl HERE=os.path.dirname(os.path.abspath(__file__)) spec=importlib.util.spec_from_file_location('dd', os.path.join(HERE,'_스크립트','_dedup_all.py')) dd=importlib.util.module_from_spec(spec); spec.loader.exec_module(dd) XP=r"작업파일\광역_사이트맵\전북특별자치도\3.김제시\전북특별자치도_김제시.xlsx" CACHE=r"_cache_gimje" LIST=re.compile(r'class="[^"]*(bbs_list|news_list|photo_list|video_list|magazine_list|gallery_list|board_list)[^"]*"') TAB_DIV=re.compile(r'
(.*?)
', re.S) EXTERNAL={69,249,440,441,148,195,277,318,390,400,134,368} # 외부 리다이렉트/ERR/외부새창 (현행 유지) TYPE_ORDER=['어문','이미지','영상','음악','소프트웨어','데이터','3D','기타'] def union_types(vals_list): seen=[] for s in vals_list: if not s: continue for t in str(s).split(','): t=t.strip() if t and t not in seen: seen.append(t) pri=[t for t in TYPE_ORDER if t in seen]+[t for t in seen if t not in TYPE_ORDER] return ','.join(pri) if pri else None def mc_of(K): m=re.search(r'menuCd=(DOM_\w+)', str(K)); return m.group(1) if m else None def cpath(url): return os.path.join(CACHE, hashlib.md5(str(url).encode()).hexdigest()+".html") def html_of(K): p=cpath(K); return open(p,encoding='utf-8').read() if os.path.exists(p) else None def tail(mc): m=re.search(r'(\d{18})$', mc); return m.group(1) if m else None def parent_mc(mc): t=tail(mc) if not t: return None g=[t[i:i+3] for i in range(0,18,3)] for i in range(5,-1,-1): if g[i]!='000': g2=g[:]; g2[i]='000'; return mc[:mc.rindex(t)]+''.join(g2) return None def main(): mode=sys.argv[1] if len(sys.argv)>1 else 'dry' wb=openpyxl.load_workbook(XP); ws=wb.active rows=dd.load_flat(ws) # src 기준 by_src={r['src']:r for r in rows} # menuCd 매핑 (src 기준) src_mc={}; mc_src={} for r in rows: mc=mc_of(r['vals'].get(11)) if mc: src_mc[r['src']]=mc; mc_src.setdefault(mc,r['src']) # 판별 det={} for r in rows: s=r['src']; K=r['vals'].get(11) if s in EXTERNAL or s not in src_mc: det[s]=None; continue h=html_of(K) det[s]='게시판' if (h and LIST.search(h)) else '페이지' # 탭그룹 발굴 → parent별 distinct 탭 menuCd plan={} for r in rows: mc=src_mc.get(r['src']); if not mc: continue h=html_of(r['vals'].get(11)) if not h: continue m=TAB_DIV.search(h) if not m: continue tabs=re.findall(r'menuCd=(DOM_\w+)', m.group(1)) seen=set(); tabs=[t for t in tabs if not (t in seen or seen.add(t))] if len(tabs)<2: continue par=parent_mc(tabs[0]) plan.setdefault(par,set()).update(tabs) # 병합 액션 산출 fold_src=set() # 흡수(삭제)될 src survivor_M={} # src -> M(페이지탭수) survivor_N={} # src -> 저작물유형 합집합 survivor_clear_leaf=set() # 라벨 leaf 컬럼 비울 survivor actions=[] warn_data=[]; warn_new=[] for par, tabset in plan.items(): tab_srcs=[mc_src[t] for t in tabset if t in mc_src] if len(tab_srcs)<2: continue # 폴드 가능한 페이지 탭 = det 페이지 & 외부아님 page_tabs=[s for s in tab_srcs if det.get(s)=='페이지' and s not in EXTERNAL] board_tabs=[s for s in tab_srcs if not (det.get(s)=='페이지' and s not in EXTERNAL)] if len(page_tabs)<1: continue par_src=mc_src.get(par) if par_src and det.get(par_src)=='페이지' and par_src not in tab_srcs: survivor=par_src; members=page_tabs else: survivor=min(page_tabs); members=[s for s in page_tabs if s!=survivor] survivor_clear_leaf.add(survivor) if len(members)<1: continue for s in members: fold_src.add(s) survivor_M[survivor]=len(page_tabs) survivor_N[survivor]=union_types([by_src[survivor]['vals'].get(14)]+[by_src[s]['vals'].get(14) for s in members]) actions.append((survivor, members, board_tabs, len(page_tabs))) # 경고: 흡수행에 KOGL/저작물 데이터(N=14,O=15,..S=19) 있나 for s in members: v=by_src[s]['vals'] if any(v.get(c) not in (None,'') for c in (15,16,17,18)): # O~R 공공누리 warn_data.append((s, v.get(7) or v.get(6), {c:v.get(c) for c in (14,15,19)})) lab=' > '.join(str(v.get(c)) for c in range(4,11) if v.get(c)) if '새창열림' in lab: warn_new.append((s,lab)) # 새 행 구성 out=[] reclass={'게시판→페이지':0,'페이지→게시판(미적용/외부)':0,'유지':0} for r in rows: s=r['src'] if s in fold_src: continue v=r['vals'] oldL=v.get(12) d=det.get(s) # 재분류 (외부/비menuCd 제외) if d in ('게시판','페이지'): if oldL!=d and not (oldL=='페이지' and d=='게시판'): # 페이지→게시판은 외부 제외했으므로 여기 오면 진짜 게시판 pass newL=d if oldL=='페이지' and d=='게시판' and s in EXTERNAL: newL=oldL # 통계 if oldL=='게시판' and newL=='페이지': reclass['게시판→페이지']+=1 v[12]=newL # M 설정 if newL=='페이지': v[13]=survivor_M.get(s, 1) # 게시판이면 기존 M 유지 # survivor: 저작물유형 합집합 갱신 if s in survivor_N and survivor_N[s]: v[14]=survivor_N[s] # survivor 라벨 정리(탭이 survivor가 된 경우 leaf 비움) if s in survivor_clear_leaf: lc=dd.leaf_depth(v) v[lc]=None out.append(r) print(f"=== 김제시 재분류 dry ===") print(f"현재행 {len(rows)} → 병합후 {len(out)} (흡수 {len(fold_src)}행)") print(f"게시판→페이지 재분류: {reclass['게시판→페이지']}건") print(f"병합 그룹 수: {len(actions)}") print(f"\n[경고] 흡수행 중 공공누리(O~R) 데이터 보유: {len(warn_data)}건") for s,nm,d2 in warn_data: print(f" r{s} {nm}: {d2}") print(f"\n[확인] 흡수 대상 '새창열림' 페이지: {len(warn_new)}건") for s,lab in warn_new[:20]: print(f" r{s}: {lab}") # 병합 그룹 요약 print("\n=== 병합 그룹 (survivor M=페이지탭수, 게시판탭은 유지) ===") def lab(s): v=by_src[s]['vals']; return ' > '.join(str(v.get(c)) for c in range(4,11) if v.get(c)) for survivor,members,boards,m in sorted(actions): print(f" survivor r{survivor} M={m} [{lab(survivor)}] 흡수 {len(members)}행" + (f", 게시판유지 {len(boards)}행" if boards else "")) if mode=='run': bak=XP.replace('.xlsx','_backup_재분류전.xlsx') shutil.copy(XP,bak) dd.write_back(ws,out) try: wb.save(XP); print(f"\n저장완료. 백업:{os.path.basename(bak)}") except PermissionError: wb.save(XP.replace('.xlsx','_LP.xlsx')); print("\n!! 잠김→_LP") if __name__=='__main__': main()