Excel 파일이 커졌을 때 셀·서식·이미지 크기 비교하기
입력한 행 수는 비슷한데 Excel 파일만 커졌다면, 먼저 저장된 셀 수·서식 정의 수·이미지 바이트를 나눠 보세요. 값 100개를 넣은 파일도 셀마다 다른 색을 넣었는지, 멀리 떨어진 빈 셀에 서식을 남겼는지, 그림을 추가했는지에 따라 내부 구조가 달라집니다.
이 실습은 Python으로 .xlsx 안의 ZIP/XML을 읽고 새 JSON 보고서를 만드는 방법입니다. 통합문서를 열어 저장하거나 셀을 지우지 않습니다. 느려진 원인, 손상 여부, 삭제해도 되는 범위를 이 숫자만으로 확정할 수는 없습니다.
1. 실습 파일을 받고 먼저 한 번 검산하기
XLSX 구조 비교 실습 ZIP 다운로드에는 진단 코드, 합성 Excel 파일 5개, 실제 측정 결과, 검산 코드가 들어 있습니다. 실제 업무 파일 대신 첨부된 예제로 시작하세요. Python 3.12 이상이 필요하며 진단·검산 실행에는 추가 패키지 설치가 없습니다.
- ZIP을 새 폴더에 풀고 그 폴더에서 터미널을 엽니다.
- Windows에서
py --version으로 버전을 확인합니다. - 아래 검산을 실행하고 마지막
PASS를 확인합니다.
py verify_practice.py
macOS/Linux에서는 py를 python3로 바꿉니다. 검산은 첨부한 5개 파일의 SHA-256, 측정 결과, 명령행 출력, 기존 출력 파일 보존을 확인합니다. 예제를 다시 생성하는 선택 코드에만 openpyxl·Pillow가 필요합니다.

2. 기본 파일과 서식이 많은 파일 비교하기
첫 파일은 A1:J10에 숫자 1~100만 넣었습니다. 두 번째는 같은 위치, 같은 값 100개에 서로 다른 채우기 서식 100종을 적용했습니다. 두 파일을 한 번에 읽습니다.
py xlsx_structure.py fixtures/01_basic.xlsx fixtures/02_unique_styles.xlsx --out my_report.json
실행하면 터미널에 비교 요약이 나오고 my_report.json에 상세 결과가 생깁니다. 같은 이름의 출력이 있으면 덮어쓰지 않고 오류로 멈춥니다. 다시 실행할 때는 --out my_report_2.json처럼 새 이름을 지정하세요.
file | archive bytes | stored/value/formula | cellXfs / cell refs | media bytes
01_basic.xlsx | 5221 | 100/100/0 | 1 / 1 | 0
02_unique_styles.xlsx | 6127 | 100/100/0 | 101 / 100 | 0
둘 다 값 셀은 100개입니다. 서식 정의는 1개에서 101개로 늘고, 저장 셀이 참조한 서로 다른 서식 인덱스는 1개에서 100개로 늘었습니다. 정의 101개와 참조 100개가 다른 이유는 기본 인덱스 0도 정의에 남아 있기 때문입니다. 두 번째 파일의 셀은 인덱스 1~100을 참조합니다.

3. 셀 세 가지와 서식 두 가지를 구분하기
- 저장 셀(stored): 시트 XML의
sheetData에 기록된c요소 수입니다. 서식만 가진 빈 셀도 포함될 수 있습니다. - 값 셀(value): 수식 없이 비어 있지 않은 저장 값 또는 인라인 문자열이 있는 셀입니다. 숫자 0도 포함합니다.
- 수식 셀(formula):
f요소가 있는 셀입니다. 수식을 계산하지 않으며, 수식의 캐시 값은 값 셀에 다시 세지 않습니다. - 서식 정의(cellXfs): 스타일 XML의
cellXfs아래 실제xf요소 수입니다. 색의 종류나 화면에서 보이는 모양의 수와는 다릅니다. - 셀 참조(cell refs): 저장된 셀이 사용한 서로 다른
s인덱스 수입니다. 생략된s는 기본 인덱스 0으로 셉니다.
Microsoft의 cellXfs 설명에 따르면 시트의 셀은 이 서식 레코드를 0부터 시작하는 인덱스로 참조합니다. openpyxl 서식 문서도 셀 서식을 공유하는 방식과 적용 예를 설명합니다.
행·열 전체에 지정된 기본 서식 인덱스는 JSON의 row_column_style_ids로 따로 기록합니다. 조건부 서식의 dxfs는 위 cellXfs 수에 포함하지 않습니다. 서식 정의 수가 많다는 사실만으로 파일이 느리거나 손상됐다고 판정하지 마세요.
4. 다섯 파일에서 실제로 달라진 수치
다음은 첨부된 파일을 2026-10-08, Linux 클라우드의 Python 3.12.14에서 읽은 결과입니다. 파일 생성에는 openpyxl 3.1.5·Pillow 12.3.0을 사용했습니다. 기본·서식·먼 빈 셀·이미지 파일의 값은 동일한 100개이고, 빈 파일에는 값이 없습니다.
기본
- 파일 B: 5,221, 미디어 B: 0
- 저장 셀: 100, 값 / 수식: 100 / 0
- 정의 / 참조: 1 / 1
서식 100종
- 파일 B: 6,127, 미디어 B: 0
- 저장 셀: 100, 값 / 수식: 100 / 0
- 정의 / 참조: 101 / 100
먼 빈 셀 서식
- 파일 B: 5,276, 미디어 B: 0
- 저장 셀: 101, 값 / 수식: 100 / 0
- 정의 / 참조: 2 / 2
이미지 추가
- 파일 B: 449,949, 미디어 B: 443,375
- 저장 셀: 100, 값 / 수식: 100 / 0
- 정의 / 참조: 1 / 1
빈 시트
- 파일 B: 4,808, 미디어 B: 0
- 저장 셀: 0, 값 / 수식: 0 / 0
- 정의 / 참조: 1 / 0
각 항목의 파일 B는 디스크상의 전체 .xlsx 크기입니다. 미디어 B는 xl/media/ 멤버를 압축 해제해 읽은 바이트의 합입니다. 이미지 예제에는 직접 만든 384×384 노이즈 PNG 한 개를 넣었습니다. 값이나 서식 수가 늘지 않아도 이미지가 파일 크기에 기여하는 모습을 볼 수 있습니다.
5. 압축 파일 크기와 압축 전 크기는 다릅니다
JSON에는 전체 파일 크기, ZIP 멤버의 압축 데이터 크기 합, 선언된 압축 전 크기 합, 실제로 읽은 압축 전 크기 합을 구분해 기록합니다. Python 3.12 zipfile 문서의 compress_size와 file_size가 서로 다른 지표인 점을 확인하세요.
| 파일 | 전체 .xlsx B | 압축 데이터 합 B | 압축 전 읽은 합 B |
|---|---|---|---|
| 기본 | 5,221 | 4,195 | 19,561 |
| 서식 100종 | 6,127 | 5,101 | 38,251 |
| 이미지 추가 | 449,949 | 448,393 | 464,415 |
전체 .xlsx 크기에는 ZIP 헤더와 중앙 디렉터리도 들어가므로 압축 데이터 합과 일치하지 않습니다. 서식 예제의 스타일 XML은 압축 전 2,550 B에서 20,548 B로 늘었습니다. 압축 후에는 593 B에서 1,251 B가 됐습니다. 압축 전 증가량을 디스크 파일 증가량으로 바꿔 말하지 않는 것이 중요합니다.
6. 아주 넓은 dimension을 보고 셀 수를 추정하지 않기
세 번째 예제는 XFD1048576에 값 없이 채우기만 적용했습니다. 저장 셀은 101개이고 값 셀은 여전히 100개입니다. XML의 dimension과 저장 셀 경계는 A1:XFD1048576이지만, 값·수식 셀 경계는 A1:J10입니다. 이 사각형 전체에 셀이 저장됐다는 뜻은 아닙니다.
완전히 빈 시트는 XML dimension이 A1:A1이어도 저장 셀은 0개입니다. 도구가 실제 저장 좌표로 구한 경계는 null입니다. dimension 문자열만 읽고 A1에 데이터가 있다고 결론내리면 안 됩니다.

Microsoft의 dimension 설명은 수식·텍스트·서식이 있는 셀을 다룹니다. 다만 이 실습은 파일에 저장된 XML과 셀 요소를 읽습니다. Excel을 실행해 얻는 Worksheet.UsedRange 속성의 실제 결과는 검사하지 않았습니다.
7. 자신의 파일을 읽을 때 지킬 범위
py xlsx_structure.py "C:/work/before.xlsx" "C:/work/after.xlsx" --out comparison_01.json
위 경로를 자신의 두 .xlsx 파일로 바꾸세요. 코드에는 통합문서 저장·수정·삭제 기능이 없습니다. 각 입력은 읽기 전후 SHA-256을 대조하며, 중간에 내용이 바뀌면 보고서 생성을 멈춥니다. 외부 관계 주소는 건수만 세고 접속하지 않습니다. ZIP 파일을 디스크에 풀거나 매크로를 실행하지 않습니다.
지원 범위를 보수적으로 제한했습니다. .xlsx의 transitional OOXML 워크시트와 UTF-8 XML, stored/deflate 압축만 처리합니다. .xls·.xlsb·.xlsm, Strict OOXML, 차트 전용 시트, UTF-16/32 XML 등은 지원하지 않습니다.
- 입력 파일 64 MiB, ZIP 멤버 2,000개, 멤버별 압축 전 16 MiB, 압축 전 총 64 MiB 제한
- 저장 셀 총 500,000개, XML 파일별 노드 1,000,000개 제한
- 중복 이름·암호화·미지원 압축·CRC 실패·잘못된 XML·DTD/ENTITY 선언·로컬/중앙 ZIP 플래그 불일치 거부
선언 크기를 먼저 확인하고 실제 읽는 바이트도 제한합니다. ZIP 메타데이터에 작은 크기를 적어 더 큰 스트림을 숨긴 경우를 거부하는 회귀 테스트도 포함했습니다. XML 노드 수는 파싱 중, 저장 셀 수는 파싱 후 집계 중 확인합니다. 이 제한은 Python 프로세스의 메모리 사용량 상한이나 모든 악성 파일에 대한 보안 보증이 아닙니다.
출력은 임시 JSON을 완성한 뒤 새 이름으로 게시합니다. 같은 이름이 있거나 파일시스템이 하드 링크를 지원하지 않으면 실패하며 기존 출력은 유지합니다. 오류를 지우고 계속 진행하지 말고, 메시지와 README의 지원 범위를 먼저 확인하세요. 이 도구는 OOXML 전체 스키마 검증·손상 복구·서식 정리 도구가 아닙니다.
마무리: 크기가 늘어난 위치부터 좁히기
첫 비교에서는 값 셀 수가 달라졌는지 확인합니다. 값이 같은데 서식 정의가 늘었다면 스타일 XML을, 저장 셀 경계만 넓어졌다면 먼 빈 셀을, 미디어 바이트가 크다면 그림 영역을 살펴볼 차례입니다. 원본은 유지한 채 후속 확인 대상을 정하는 것이 이 실습의 목적입니다.
ZIP 일반 점검이 먼저 필요하면 ZIP을 풀기 전 파일 수와 선언 크기 점검을 참고하세요. Excel 차트를 만드는 작업은 openpyxl 막대 차트와 참조 범위 검산에서 이어갈 수 있습니다.
자료 확인·합성 예제 생성·코드 실행 검증: 2026-10-08. 이 글의 수치는 첨부된 합성 파일의 구조 측정값입니다. Windows/macOS 네이티브 Excel, 재계산 시간, 실제 실행 속도는 측정하지 않았습니다.