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

167 lines
6.5 KiB
Python

# -*- coding: utf-8 -*-
"""규칙 B 재귀판: 탭 레벨(G→H→I…)에서 '자식이 전부 페이지'면 1행으로 합치고 M=합.
메뉴 카테고리(D·E·F)는 보존 — 합치기는 lc>=7(G 이하 탭 레벨)에서만 수행.
각 레벨 lc 에서:
부모(D..lc-1) 동일 + lc 비어있지 않음 으로 형제 묶음 → 그 묶음이
· 전부 L=페이지, · lc보다 깊은 카테고리열 모두 비어있음(잎)
이면 1행으로 합침: lc 이하 카테고리열 비움, M=합, K=첫 행 URL,
공공누리 O=유형 합집합(오름차순,콤마,공백없음)/P=게시판우선/Q=하나라도 Y.
깊은→얕은 순으로 반복(안정될 때까지) → G→H 합친 뒤 F→G까지 자연 연쇄.
평탄화/재병합(D/E/F)/순번/하이퍼링크는 _dedup_all.write_back 재사용.
사용:
python -X utf8 _collapse_recursive.py dry <엑셀경로 | 기관명...>
python -X utf8 _collapse_recursive.py run <엑셀경로 | 기관명...>
(기관명 없이 경로 1개만 줘도 됨)
"""
import os, re, sys, shutil, importlib.util, warnings
import openpyxl
warnings.filterwarnings('ignore')
spec = importlib.util.spec_from_file_location('dd', __file__.replace('_collapse_recursive.py', '_dedup_all.py'))
dd = importlib.util.module_from_spec(spec); spec.loader.exec_module(dd)
CAT = [4, 5, 6, 7, 8, 9, 10] # D E F G H I J
COLLAPSE_LEVELS = [10, 9, 8, 7] # 탭 레벨만(깊은→얕은). E(6)·D(5)는 제외=카테고리 보존
TYPE_PAT = re.compile(r'(\d)\s*유형')
EXCLUDE = {'계룡시'} # 검수완료
def mval(x):
try:
return int(x)
except Exception:
return 1
def merge_kogl(grp):
types = set()
for g in grp:
o = g['vals'].get(15)
if isinstance(o, str):
for m in TYPE_PAT.finditer(o):
types.add(int(m.group(1)))
if types:
O = ','.join(f'{n}유형' for n in sorted(types))
else:
O = '미부착'
Ps = [g['vals'].get(16) for g in grp]
P = '게시판' if '게시판' in Ps else ('게시물' if '게시물' in Ps else None)
Qs = [g['vals'].get(17) for g in grp]
Q = 'Y' if 'Y' in Qs else ('N' if 'N' in Qs else None)
return O, (P if O != '미부착' else None), (Q if O != '미부착' else None)
def collapse_level(rows, lc):
parent = [c for c in CAT if c < lc]
deeper = [c for c in CAT if c > lc]
out, groups = [], []
i, n = 0, len(rows)
while i < n:
v = rows[i]['vals']
if v.get(lc) not in (None, ''):
j = i
while (j < n and all(rows[j]['vals'].get(c) == v.get(c) for c in parent)
and rows[j]['vals'].get(lc) not in (None, '')):
j += 1
grp = rows[i:j]
all_page = all(g['vals'].get(12) == '페이지' for g in grp)
leaf = all(all(g['vals'].get(c) in (None, '') for c in deeper) for g in grp)
has_attach = any(g['vals'].get(15) not in (None, '', '미부착') for g in grp)
if len(grp) >= 2 and all_page and leaf:
msum = sum(mval(g['vals'].get(13)) for g in grp)
O, P, Q = merge_kogl(grp)
first = dict(grp[0]); nv = dict(first['vals'])
for c in [lc] + deeper:
nv[c] = None
nv[13] = msum; nv[15] = O; nv[16] = P; nv[17] = Q
first['vals'] = nv
out.append(first)
groups.append({'lc': lc, 'path': [v.get(c) for c in parent],
'label': v.get(lc), 'n': len(grp),
'ms': [mval(g['vals'].get(13)) for g in grp],
'sum': msum, 'O': O, 'attach': has_attach,
'names': [g['vals'].get(lc) for g in grp]})
i = j; continue
out.extend(grp); i = j; continue
out.append(rows[i]); i += 1
return out, groups
def collapse_all_levels(rows):
all_groups = []
changed = True
while changed:
changed = False
for lc in COLLAPSE_LEVELS:
rows, groups = collapse_level(rows, lc)
if groups:
changed = True
all_groups.extend(groups)
return rows, all_groups
def resolve_paths(args):
"""경로 또는 기관명 목록 → [(name, xlsx경로)]"""
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 not in names:
continue
if not names:
continue
paths.append((name, dd.xpath(prov, idx, name)))
return paths
LV = {7: 'F→G', 8: 'G→H', 9: 'H→I', 10: 'I→J'}
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('--')]
targets = resolve_paths(rest)
if not targets:
print('대상 없음. 경로 또는 기관명을 지정하세요.'); return
for name, xp in targets:
if name in EXCLUDE:
print(f'{name}: 검수완료 제외'); continue
if not os.path.exists(xp):
print(f'{name}: 엑셀 없음 ({xp})'); continue
wb = openpyxl.load_workbook(xp); ws = wb.active
rows = dd.load_flat(ws)
out, groups = collapse_all_levels(rows)
removed = len(rows) - len(out)
bylv = {}
for g in groups:
bylv.setdefault(g['lc'], 0)
bylv[g['lc']] += 1
lvstr = ' '.join(f"{LV.get(k,k)}:{v}" for k, v in sorted(bylv.items()))
print(f'\n=== {name} === {len(rows)}{len(out)} (제거 {removed}) [{lvstr}]')
for g in groups:
ms = '+'.join(str(m) for m in g['ms'])
star = ' ★부착' if g['attach'] else ''
print(f" [{LV.get(g['lc'],g['lc'])}] {' > '.join(str(x) for x in g['path'] if x)} > {g['label']}"
f" ({g['n']}개) → M={ms}={g['sum']} O={g['O']}{star}")
if mode == 'run' and groups:
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(f' !! {name} 잠김→_LP')
else:
print(f' 저장 완료. 백업: {os.path.basename(bak)}')
elif mode != 'run':
print(' (DRY)')
if __name__ == '__main__':
main()