다운로드한 .zip이나 .xlsx 파일을 Python으로 열 때 BadZipFile 또는 File is not a zip file이 나오면, 먼저 확장자가 아니라 실제 파일이 ZIP인지 확인해야 합니다. 그 다음에 다운로드가 완전했는지, Excel 형식과 엔진이 맞는지, 제공자가 DRM이나 다른 형식의 파일을 보낸 것은 아닌지를 차례로 나누어 보시면 됩니다. 엔진을 무작정 바꾸거나 확장자만 고치는 것으로는 실제 형식 오류가 해결되지 않습니다.
BadZipFile은 무엇을 말하나
zipfile.BadZipFile은 Python이 파일을 정상적인 ZIP 아카이브로 해석하지 못했다는 뜻입니다. 따라서 파일명이 .zip 또는 .xlsx라고 해서 ZIP 구조가 보장되는 것은 아닙니다. 로그인 화면을 저장한 HTML, 구형 .xls, 다운로드 중 잘린 파일, 제공자가 내려준 별도 보호 파일이 같은 오류의 출발점이 될 수 있습니다.
여기서 중요한 구분은 “확장자가 틀렸는가”와 “파일 내용이 ZIP 구조인가”입니다. zipfile.is_zipfile()으로 먼저 파일의 ZIP 식별 정보를 확인하고, 결과가 거짓이면 Excel 엔진을 바꾸기 전에 원본 파일을 다시 확보하거나 제공자에게 실제 형식을 확인하는 편이 맞습니다.
from pathlib import Path
from zipfile import is_zipfile
path = Path("input.xlsx")
if not path.is_file():
raise FileNotFoundError(path)
if not is_zipfile(path):
raise ValueError("확장자와 달리 ZIP 구조가 아닌 파일입니다.")
print("ZIP 구조로 확인되었습니다.")
이 검사는 파일 전체가 정상적으로 열리고 모든 내용이 온전한지까지 보장하지는 않습니다. 다만 “애초에 ZIP이 아닌가”를 “ZIP이지만 내부가 손상됐는가”와 분리하는 첫 단계로 쓸 수 있습니다.
실제 ZIP인지 확인한 뒤 손상을 구분하기
is_zipfile()이 참이어도 압축 파일 내부의 데이터가 손상되었을 수 있습니다. 이때는 ZipFile.testzip()으로 아카이브 안의 파일을 읽어 보며 CRC와 파일 헤더를 확인합니다. 문제가 없으면 None을 반환하고, 손상된 파일을 처음 발견하면 그 파일명을 반환합니다.
from zipfile import ZipFile
with ZipFile("input.zip") as archive:
damaged_name = archive.testzip()
if damaged_name is None:
print("압축 내부 검사가 끝났습니다.")
else:
print(f"손상된 항목: {damaged_name}")
is_zipfile()에서 바로 거짓이 나오면 HTML 응답이나 다른 형식, 잘린 다운로드를 먼저 의심합니다. 반대로 is_zipfile()은 통과하지만 testzip()에서 파일명이 나오면 재다운로드나 원본 교체가 우선입니다. 압축 해제를 중간에 중단했다면 일부만 풀린 결과를 정상 파일로 오해하지 않도록 작업 디렉터리도 함께 정리해야 합니다.
.xlsx인데 openpyxl에서 실패하는 이유
.xlsx와 .xlsm 계열은 OOXML 형식이며, openpyxl은 이 계열의 ZIP 기반 구조를 읽습니다. 그래서 확장자가 .xlsx여도 실제 내용이 HTML이거나 구형 .xls이면 openpyxl에서 BadZipFile이 발생할 수 있습니다. 이 경우는 openpyxl의 설정 문제라기보다 파일 형식과 읽기 도구의 대상이 맞지 않는 상황입니다.
확장자와 실제 형식이 일치한다는 전제에서만 다음처럼 pandas와 엔진을 연결합니다.
import pandas as pd
table = pd.read_excel("report.xlsx", engine="openpyxl")
구형 .xls는 xlrd, 바이너리 .xlsb는 pyxlsb처럼 형식에 맞는 엔진을 선택해야 합니다. 다만 .xlsx라는 이름만 보고 openpyxl을 고르는 것은 충분하지 않습니다. 먼저 파일 자체가 전달 과정에서 바뀌지 않았는지 확인한 뒤 엔진을 판단해야 합니다.
HTML·DRM·잘린 다운로드를 구분하는 실무 순서
자동 다운로드에서 자주 놓치는 부분은 “파일 저장은 성공했지만 업무 파일을 받은 것은 아닌” 경우입니다. 인증이 풀린 요청이 로그인 HTML을 저장했거나, 서버가 오류 안내 문서를 파일명만 바꾸어 내려줄 수 있습니다. 이 사례는 제공된 파일의 응답과 내용 확인이 필요하므로, 오류 메시지만 보고 특정 서비스의 동작이라고 단정해서는 안 됩니다.
운영에서는 다음 순서가 판단에 도움이 됩니다.
- 저장 경로와 파일 크기, 다운로드 응답 상태를 확인합니다.
is_zipfile()로 ZIP 구조인지 확인합니다.- ZIP이면
testzip()으로 내부 손상 여부를 확인합니다. - ZIP이 아니면 파일 앞부분과 응답 내용을 확인해 HTML·오류 문서인지 살핍니다.
- 파일 형식이 맞지 않거나 DRM 보호 파일이면 엔진 변경을 중단하고 원본 재확보 또는 제공자 확인으로 전환합니다.
압축 파일이 아닌 것을 억지로 .zip 또는 .xlsx로 이름만 바꾸는 방법은 구조를 바꾸지 않습니다. DRM을 우회하는 방법도 이 진단 절차의 해결책으로 삼지 않습니다.
결론: 엔진보다 먼저 파일의 정체를 확인하기
BadZipFile의 핵심은 “openpyxl을 다른 엔진으로 바꿀까”가 아니라 “지금 받은 파일이 실제로 무엇인가”입니다. is_zipfile()은 ZIP 여부를, testzip()은 ZIP 내부 손상을 가르는 기준으로 사용하고, 그 결과에 따라 재다운로드·파일 제공자 확인·형식에 맞는 Excel 엔진 선택을 나누면 됩니다.
다음으로 pandas에서 .xls, .xlsx, .xlsb 엔진을 선택해야 한다면 확장자와 실제 파일 형식을 함께 확인하는 별도 점검 절차를 정리해 두는 것이 좋습니다. 다운로드 코드라면 응답 상태와 저장 결과를 로그에 남겨, 다음 장애에서 파일 자체의 문제인지 읽기 코드의 문제인지 바로 구분할 수 있게 하십시오.