공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
117 lines
4.4 KiB
Python
117 lines
4.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""E열(대분류) 병합 누락 보정 — 재크롤링 없이 기존 엑셀의 D/E/F 병합만 바로잡는다.
|
|
|
|
원인: phase1이 D를 먼저 병합 → 블록 2행부터 D=None → E 병합(같은 D 안에서만)이
|
|
D블록 첫 행에서 끊겨 첫 행 E가 단독으로 남음.
|
|
해결: 현재 병합에서 값 복원 → D/E/F 병합 해제 → 전 행에 값 채움 →
|
|
F → E → D 순서로 재병합(컬럼을 병합하면 그 컬럼 2행부터 None이 되므로 순서가 중요).
|
|
|
|
데이터(D/E/F 텍스트)는 그대로. 헤더 병합(B1:R1 등)은 건드리지 않음.
|
|
파일별 *_backup_emerge전.xlsx 백업.
|
|
"""
|
|
import sys, os, glob, shutil, warnings, openpyxl
|
|
from openpyxl.styles import Alignment
|
|
from openpyxl.utils import get_column_letter
|
|
|
|
sys.stdout.reconfigure(encoding='utf-8')
|
|
warnings.filterwarnings('ignore')
|
|
|
|
START = 3
|
|
COLS = (4, 5, 6) # D, E, F
|
|
CENTER = Alignment(horizontal='center', vertical='center', wrap_text=False)
|
|
|
|
|
|
def filled_values(ws, col):
|
|
"""현재 병합 상태에서 각 행의 실제 값을 복원(병합 top값으로 채움)."""
|
|
end = ws.max_row
|
|
vals = {r: ws.cell(r, col).value for r in range(START, end + 1)}
|
|
for mr in ws.merged_cells.ranges:
|
|
if mr.min_col == col == mr.max_col and mr.min_row >= START:
|
|
top = ws.cell(mr.min_row, col).value
|
|
for r in range(mr.min_row, mr.max_row + 1):
|
|
vals[r] = top
|
|
return vals, end
|
|
|
|
|
|
def merge_runs(ws, col, end, group_keys, fills):
|
|
"""fills[col][r] 기준으로 연속 동일 구간 병합. group_keys: 상위 컬럼 튜플."""
|
|
letter = get_column_letter(col)
|
|
runs = []
|
|
def key(r):
|
|
return tuple(fills[g][r] for g in group_keys)
|
|
cur_val = fills[col][START]
|
|
cur_grp = key(START)
|
|
run_start = START
|
|
for r in range(START + 1, end + 1):
|
|
v, g = fills[col][r], key(r)
|
|
if v == cur_val and g == cur_grp:
|
|
continue
|
|
if cur_val not in (None, '') and r - 1 > run_start:
|
|
runs.append((run_start, r - 1))
|
|
cur_val, cur_grp, run_start = v, g, r
|
|
if cur_val not in (None, '') and end > run_start:
|
|
runs.append((run_start, end))
|
|
for s, e in runs:
|
|
ws.merge_cells(f'{letter}{s}:{letter}{e}')
|
|
ws.cell(s, col).alignment = CENTER
|
|
return len(runs)
|
|
|
|
|
|
def fix(xlsx):
|
|
wb = openpyxl.load_workbook(xlsx)
|
|
ws = wb.active
|
|
# 1) 값 복원
|
|
fills, end = {}, None
|
|
for c in COLS:
|
|
fills[c], end = filled_values(ws, c)
|
|
# 2) D/E/F 데이터 병합만 해제 (단일컬럼·row>=START)
|
|
to_unmerge = [str(mr) for mr in ws.merged_cells.ranges
|
|
if mr.min_col == mr.max_col and mr.min_col in COLS and mr.min_row >= START]
|
|
for rng in to_unmerge:
|
|
ws.unmerge_cells(rng)
|
|
# 3) 전 행에 값 다시 기입
|
|
for c in COLS:
|
|
for r in range(START, end + 1):
|
|
ws.cell(r, c).value = fills[c][r]
|
|
# 4) F → E → D 순서 재병합
|
|
n_f = merge_runs(ws, 6, end, (4, 5), fills)
|
|
n_e = merge_runs(ws, 5, end, (4,), fills)
|
|
n_d = merge_runs(ws, 4, end, (), fills)
|
|
return wb, (n_d, n_e, n_f, len(to_unmerge))
|
|
|
|
|
|
def main():
|
|
sel = [a for a in sys.argv[1:] if not a.startswith('-')]
|
|
files = []
|
|
for region in ['충청남도', '충청북도']:
|
|
for d in sorted(glob.glob(region + r'\*')):
|
|
if not os.path.isdir(d):
|
|
continue
|
|
base = os.path.basename(d)
|
|
nm = base.split('.', 1)[1] if '.' in base else base
|
|
prov = os.path.basename(os.path.dirname(d))
|
|
f = os.path.join(d, f'{prov}_{nm}.xlsx')
|
|
if os.path.exists(f) and (not sel or nm in sel):
|
|
files.append((nm, f))
|
|
print(f'대상 {len(files)}개: {[n for n, _ in files]}')
|
|
ok = []
|
|
for nm, f in files:
|
|
try:
|
|
bak = f.replace('.xlsx', '') + '_backup_emerge전.xlsx'
|
|
if not os.path.exists(bak):
|
|
shutil.copy2(f, bak)
|
|
wb, (nd, ne, nf, un) = fix(f)
|
|
wb.save(f)
|
|
print(f' ✅ {nm:<6} 병합해제 {un:>3} → 재병합 D:{nd} E:{ne} F:{nf}')
|
|
ok.append(nm)
|
|
except PermissionError:
|
|
print(f' ⚠️ {nm:<6} 파일 잠김(열려있음) → 건너뜀')
|
|
except Exception as e:
|
|
import traceback; traceback.print_exc()
|
|
print(f' !! {nm:<6} 오류: {e}')
|
|
print(f'\n완료 {len(ok)}/{len(files)}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|