월별 실적이나 부서별 목록처럼 열 구조가 같은 Excel 파일이 여러 개 있을 때, Python으로 하나의 파일에 행 방향으로 합칠 수 있습니다.
이 글에서는 다음 조건을 포함한 실무형 예제를 사용합니다.
- 입력 폴더의
.xlsx파일만 읽기 - Excel 임시 파일
~$제외 - 결과 파일을 다시 입력으로 읽지 않기
- 파일별 출처 열 추가
- 열 구조가 다른 파일 탐지
- 빈 파일과 읽기 오류 처리
- 결과를
merged.xlsx로 저장
핵심 요약
pandas.read_excel()로 각 파일을 DataFrame으로 읽고,pandas.concat()으로 행 방향 결합한 뒤to_excel()로 저장합니다..xlsx파일 처리를 위해openpyxl을 함께 설치합니다.
1. 패키지 설치
python -m pip install pandas openpyxl
설치 확인:
python -c "import pandas, openpyxl; print(pandas.__version__, openpyxl.__version__)"
2. 폴더 구조
excel-merge/
├─ input/
│ ├─ sales-2026-01.xlsx
│ ├─ sales-2026-02.xlsx
│ └─ sales-2026-03.xlsx
├─ output/
└─ merge_excel.py
각 입력 파일의 첫 번째 시트에 다음과 같은 동일한 열이 있다고 가정합니다.
날짜 | 상품 | 수량 | 금액
3. 전체 코드
from __future__ import annotations
from pathlib import Path
import sys
import pandas as pd
INPUT_DIR = Path("input")
OUTPUT_DIR = Path("output")
OUTPUT_FILE = OUTPUT_DIR / "merged.xlsx"
def find_excel_files(input_dir: Path) -> list[Path]:
if not input_dir.exists():
raise FileNotFoundError(f"입력 폴더가 없습니다: {input_dir.resolve()}")
files = [
path
for path in input_dir.glob("*.xlsx")
if not path.name.startswith("~$")
and path.resolve() != OUTPUT_FILE.resolve()
]
return sorted(files)
def read_excel_file(path: Path) -> pd.DataFrame:
frame = pd.read_excel(path, sheet_name=0, engine="openpyxl")
if frame.empty:
print(f"[SKIP] 데이터가 없는 파일: {path.name}")
return frame
frame = frame.dropna(how="all")
frame["원본파일"] = path.name
return frame
def validate_columns(
frame: pd.DataFrame,
expected_columns: list[str] | None,
path: Path,
) -> list[str]:
current_columns = [
column for column in frame.columns if column != "원본파일"
]
if expected_columns is None:
return current_columns
if current_columns != expected_columns:
raise ValueError(
"열 구조가 다릅니다.\n"
f"- 파일: {path.name}\n"
f"- 기준: {expected_columns}\n"
f"- 현재: {current_columns}"
)
return expected_columns
def merge_excel_files() -> Path:
files = find_excel_files(INPUT_DIR)
if not files:
raise FileNotFoundError(
f"합칠 .xlsx 파일이 없습니다: {INPUT_DIR.resolve()}"
)
frames: list[pd.DataFrame] = []
expected_columns: list[str] | None = None
for path in files:
print(f"[READ] {path.name}")
frame = read_excel_file(path)
if frame.empty:
continue
expected_columns = validate_columns(
frame,
expected_columns,
path,
)
frames.append(frame)
if not frames:
raise ValueError("읽을 수 있는 데이터가 없습니다.")
merged = pd.concat(frames, ignore_index=True)
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
merged.to_excel(
OUTPUT_FILE,
index=False,
sheet_name="통합",
engine="openpyxl",
)
print(f"[DONE] {len(merged):,}행 저장")
print(f"[OUTPUT] {OUTPUT_FILE.resolve()}")
return OUTPUT_FILE
if __name__ == "__main__":
try:
merge_excel_files()
except Exception as exc:
print(f"[ERROR] {exc}", file=sys.stderr)
raise SystemExit(1)
4. 코드 실행
프로젝트 폴더에서 실행합니다.
python merge_excel.py
정상 출력 예:
[READ] sales-2026-01.xlsx
[READ] sales-2026-02.xlsx
[READ] sales-2026-03.xlsx
[DONE] 300행 저장
[OUTPUT] .../output/merged.xlsx
5. 열 순서가 달라도 합치고 싶을 때
위 예제는 열 이름뿐 아니라 순서도 같아야 통과합니다. 열 순서는 달라도 되고 이름만 같으면 되는 경우에는 검증 함수를 집합 비교 방식으로 바꿀 수 있습니다.
if set(current_columns) != set(expected_columns):
raise ValueError("열 이름 구성이 다릅니다.")
frame = frame[expected_columns + ["원본파일"]]
그러나 이름이 비슷한 다른 열을 자동으로 합치면 데이터가 잘못 정렬될 수 있으므로 초기에는 엄격한 검증이 안전합니다.
6. 여러 시트를 합치는 경우
모든 시트를 읽으려면 다음처럼 sheet_name=None을 사용합니다.
sheets = pd.read_excel(
path,
sheet_name=None,
engine="openpyxl",
)
반환값은 시트명을 키로 하는 딕셔너리입니다.
for sheet_name, frame in sheets.items():
frame["원본파일"] = path.name
frame["원본시트"] = sheet_name
7. 자주 발생하는 문제
ModuleNotFoundError: No module named 'openpyxl'
python -m pip install openpyxl
실행 중인 Python과 pip가 다른 환경을 가리킬 수 있으므로 python -m pip 형식을 권장합니다.
결과 파일까지 다시 합쳐지는 문제
결과를 입력 폴더 밖의 output 폴더에 저장하면 가장 단순하게 방지할 수 있습니다.
Excel에서 열어 둔 파일
Excel에서 파일을 열면 ~$파일명.xlsx 임시 파일이 생길 수 있습니다. 예제 코드는 이 파일을 제외합니다.
서식과 수식이 유지되지 않는 문제
pandas 방식은 표 형태의 값을 통합하는 용도입니다. 원본 파일의 복잡한 셀 서식, 차트, 매크로와 수식을 그대로 복제하는 작업에는 적합하지 않습니다.
매크로 파일 .xlsm이나 기존 서식을 보존해야 한다면 openpyxl의 keep_vba 옵션과 워크북 단위 처리를 별도로 설계해야 합니다.
8. 실무에서 추가할 기능
- 처리 파일 수와 행 수 로그
- 오류 파일 별도 목록
- 중복 행 제거
- 필수 열과 데이터 형식 검사
- 날짜 형식 통일
- 대용량 파일의 분할 처리
- 실행 결과 이메일 또는 Teams 알림
공식 참고 자료
검증일: 2026-07-11