공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
167 lines
6.5 KiB
Python
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()
|