DB_JOB/_스크립트/_collapse_landing.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

127 lines
5.3 KiB
Python

# -*- coding: utf-8 -*-
"""중분류/소분류 '랜딩행' 제거 — 매뉴얼 1-5 확장(③ K동일 조건 제거).
_dedup_all.py 는 부모행 URL == 첫 자식 URL 일 때만 부모행을 삭제한다(③).
eGov '누리집지도'(고창·임실·정읍·진안 등)는 중분류 랜딩(예 …002000000 '민원안내')과
첫 소분류(…002007000)의 URL이 달라 ③에 안 걸리고 랜딩행이 남아 있다.
사용자 요청(2026-05-31): ③조건을 빼고, 자식을 거느린 부모 랜딩행을 전부 삭제.
· 부모행의 가장 깊은 카테고리 컬럼 lc, lc+1은 빈칸(부모는 그 깊이의 잎이 아님)
· 직후 자식행이 D~lc 값 동일 & lc+1 채워짐 → 부모행 삭제(자식이 병합으로 라벨 승계)
URL 무관. 카테고리 텍스트(D/E/F)는 재병합으로 보존.
평탄화/재병합/순번/하이퍼링크는 _dedup_all 재사용. 전 컬럼(B~AA, L~Q) 보존.
사용:
python -X utf8 _collapse_landing.py dry <기관명...|경로>
python -X utf8 _collapse_landing.py run <기관명...|경로>
--maxlc N : lc<=N 깊이까지만 삭제(기본 9=I, 즉 D~I 랜딩 모두). E만 원하면 --maxlc 5.
"""
import os, sys, shutil, importlib.util, warnings
import openpyxl
warnings.filterwarnings('ignore')
spec = importlib.util.spec_from_file_location('dd', __file__.replace('_collapse_landing.py', '_dedup_all.py'))
dd = importlib.util.module_from_spec(spec); spec.loader.exec_module(dd)
CAT_COLS = dd.CAT_COLS # D E F G H I J = 4..10
EXCLUDE = {'계룡시'} # 검수완료
def is_safe_shell(v):
"""순수 메뉴 셸인가: L=페이지(또는 빈칸) & M<=1 & 공공누리 미부착."""
L = v.get(12); M = v.get(13); O = v.get(15)
if isinstance(O, str) and '유형' in O:
return False
if L not in (None, '', '페이지'):
return False
if isinstance(M, (int, float)) and M and M > 1:
return False
return True
def collapse(rows, maxlc=9, safe=False):
"""랜딩행 제거. safe=True면 순수 셸(데이터 무보유)만. 반환 (남은행, 제거목록)."""
out, removed = [], []
i = 0
while i < len(rows):
v = rows[i]['vals']
if i + 1 < len(rows):
nv = rows[i + 1]['vals']
lc = dd.leaf_depth(v) # 부모의 가장 깊은 카테고리 컬럼
if 4 <= lc <= maxlc:
same_upper = all((v.get(c) or '') == (nv.get(c) or '')
for c in CAT_COLS if c <= lc)
child_next = nv.get(lc + 1) not in (None, '')
parent_is_landing = v.get(lc + 1) in (None, '')
if safe and not is_safe_shell(v):
same_upper = False # 데이터 보유 랜딩은 보존
if same_upper and child_next and parent_is_landing:
removed.append({'src': rows[i]['src'], 'lc': lc,
'label': v.get(lc), 'child': nv.get(lc + 1),
'url': v.get(11), 'curl': nv.get(11)})
i += 1
continue
out.append(rows[i]); i += 1
return out, removed
LV = {4: 'D', 5: 'E', 6: 'F', 7: 'G', 8: 'H', 9: 'I', 10: 'J'}
def resolve(args):
paths = []
for a in args:
if a.lower().endswith('.xlsx') or os.path.sep in a:
paths.append((os.path.splitext(os.path.basename(a))[0], a))
names = [a for a in args if not (a.lower().endswith('.xlsx') or os.path.sep in a)]
for prov, idx, name in dd.SITES:
if names and name in names:
paths.append((name, dd.xpath(prov, idx, name)))
return paths
def main():
mode = sys.argv[1] if len(sys.argv) > 1 else 'dry'
rest = [a for a in sys.argv[2:] if not a.startswith('--')]
maxlc = 9
if '--maxlc' in sys.argv:
maxlc = int(sys.argv[sys.argv.index('--maxlc') + 1])
safe = '--safe' in sys.argv
targets = resolve(rest)
if not targets:
print('대상 없음.'); return
grand = 0
for name, xp in targets:
if name in EXCLUDE:
print(f'{name}: 검수완료 제외'); continue
if not os.path.exists(xp):
print(f'{name}: 엑셀 없음'); continue
wb = openpyxl.load_workbook(xp); ws = wb.active
rows = dd.load_flat(ws)
out, removed = collapse(rows, maxlc, safe)
grand += len(removed)
bylv = {}
for r in removed:
bylv[r['lc']] = bylv.get(r['lc'], 0) + 1
lvstr = ' '.join(f'{LV[k]}:{v}' for k, v in sorted(bylv.items()))
print(f'\n=== {name} === {len(rows)}{len(out)} (제거 {len(removed)}) [{lvstr}]')
for r in removed[:6]:
print(f" [{LV[r['lc']]}] {r['label']} ({r['url']}) → 자식 첫행 {r['child']} ({r['curl']})")
if len(removed) > 6:
print(f' ... 외 {len(removed)-6}')
if mode == 'run' and removed:
bak = xp.replace('.xlsx', '_backup_랜딩제거전.xlsx')
shutil.copy(xp, bak)
dd.write_back(ws, out)
try:
wb.save(xp)
except PermissionError:
wb.save(xp.replace('.xlsx', '_LP.xlsx')); print(' !! 잠김→_LP')
else:
print(f' 저장 완료. 백업: {os.path.basename(bak)}')
print(f'\n총 제거 대상: {grand}행 ({mode}, maxlc={maxlc})')
if __name__ == '__main__':
main()