공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
171 lines
7.9 KiB
Python
171 lines
7.9 KiB
Python
# -*- 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'<div class="basic_tab[^"]*">(.*?)</div>', 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()
|