pypdf로 PDF를 합치고 페이지 범위를 추출하기
여러 PDF를 하나로 합치고, 지정한 페이지 범위를 두 번째 PDF로 추출한 뒤 페이지 수와 순서를 검증합니다. 페이지마다 크기가 다른 합성 PDF를 사용해 예상 결과를 손으로 확인할 수 있습니다.
같은 열 구조를 사용하는 여러 Excel 시트를 하나로 합치면서 각 행이 어느 시트에서 왔는지 기록합니다. 작은 합성 통합문서를 이용해 행 순서, 개수, 합계를 손으로 확인할 수 있습니다.
이런 분께 맞아요구조가 같은 여러 Excel 시트를 하나의 데이터셋으로 합치되 원본 통합문서는 변경하지 않으려는 사람을 위한 안내입니다.
이 작업은 하나의 통합문서에서 지정한 여러 워크시트를 읽어 새로운 통합문서 하나로 합칩니다. 모든 원본 시트의 헤더는 열 순서와 대소문자까지 정확히 같아야 합니다. 출력에서는 첫 번째 열에 source_sheet를 추가해 각 레코드가 어느 시트에서 왔는지 유지합니다.
완전히 빈 행은 무시합니다. 비어 있지 않은 행은 지정한 워크시트 순서대로 합치고, 각 시트 안에서는 원래의 위에서 아래 순서를 유지합니다. 원본 통합문서는 읽기 전용으로 열고 결과는 outputs 아래의 별도 폴더에 저장합니다.
아래 통합문서는 이 글을 위해 작성한 합성 데이터입니다. 준비용 스크립트를 create_excel_merge_demo.py로 저장하세요. East, West, Online이라는 시트 3개를 만들며, 모두 order_id, customer, amount라는 동일한 열을 사용합니다.
from pathlib import Path
from openpyxl import Workbook
SOURCE_DIR = Path("outputs") / "excel_merge_demo"
SOURCE = SOURCE_DIR / "regional_orders.xlsx"
HEADERS = ["order_id", "customer", "amount"]
DATA = {
"East": [
["E001", "Ana", 120],
["E002", "Ben", 80],
],
"West": [
["W001", "Cara", 150],
["W002", "Dan", 70],
],
"Online": [
["O001", "Emi", 90],
[None, None, None],
["O002", "Finn", 110],
],
}
SOURCE_DIR.parent.mkdir(parents=True, exist_ok=True)
SOURCE_DIR.mkdir() # Stop if the synthetic source folder already exists.
workbook = Workbook()
for index, (sheet_name, rows) in enumerate(DATA.items()):
if index == 0:
sheet = workbook.active
sheet.title = sheet_name
else:
sheet = workbook.create_sheet(sheet_name)
sheet.append(HEADERS)
for row in rows:
sheet.append(row)
workbook.save(SOURCE)
python create_excel_merge_demo.py| 시트 | 비어 있지 않은 데이터 행 | amount 합계 |
|---|---|---|
| East | 2 | 200 |
| West | 2 | 220 |
| Online | 2 | 200 |
비어 있지 않은 데이터 레코드는 총 6개입니다. amount의 전체 합계는 620입니다. 계산식은 120 + 80 + 150 + 70 + 90 + 110입니다. Online에는 두 레코드 사이에 완전히 빈 행 하나를 의도적으로 넣어, 합칠 때 빈 행이 제외되는지 확인할 수 있게 했습니다.
스크립트에서는 시트 순서를 East, West, Online으로 명시합니다. 이 순서가 결합된 레코드의 순서를 결정합니다. 각 시트 안에서는 원래의 위에서 아래 행 순서를 그대로 유지합니다.
| source_sheet | order_id | customer | amount |
|---|---|---|---|
| East | E001 | Ana | 120 |
| East | E002 | Ben | 80 |
| West | W001 | Cara | 150 |
| West | W002 | Dan | 70 |
| Online | O001 | Emi | 90 |
| Online | O002 | Finn | 110 |
따라서 출력은 4개 열을 가지며 헤더까지 포함하면 워크시트 행은 총 7개입니다. 헤더 1행과 데이터 6행입니다. source_sheet 값만 세어도 수동 검증이 가능합니다. East가 2번, West가 2번, Online이 2번 나와야 합니다.
다음 스크립트를 excel_merge_sheets.py로 저장하세요. 원본 통합문서를 읽기 전용으로 열고, 선택한 시트 이름과 헤더를 검증하며, 완전히 빈 행은 건너뜁니다. 또한 수식이 원래 통합문서의 문맥에서 분리된 채 복사되는 일을 피하기 위해 수식 셀이 발견되면 중단합니다.
from pathlib import Path
from openpyxl import Workbook, load_workbook
SOURCE = Path("outputs") / "excel_merge_demo" / "regional_orders.xlsx"
OUTPUT_DIR = Path("outputs") / "excel_merge_result"
OUTPUT = OUTPUT_DIR / "merged_orders.xlsx"
SHEETS = ("East", "West", "Online")
OUTPUT_SHEET = "Combined"
SOURCE_COLUMN = "source_sheet"
def read_sheet_rows(sheet, expected_header=None):
first_row = next(
sheet.iter_rows(min_row=1, max_row=1, values_only=True),
None,
)
if first_row is None:
raise ValueError(f"Sheet is empty: {sheet.title}")
header = tuple(first_row)
if any(not isinstance(name, str) or not name.strip() for name in header):
raise ValueError(f"Invalid header in sheet: {sheet.title}")
if len(set(header)) != len(header):
raise ValueError(f"Duplicate header name in sheet: {sheet.title}")
if SOURCE_COLUMN in header:
raise ValueError(f"Reserved column already exists: {SOURCE_COLUMN}")
if expected_header is not None and header != expected_header:
raise ValueError(f"Header mismatch in sheet: {sheet.title}")
rows = []
for excel_row, values in enumerate(
sheet.iter_rows(min_row=2, max_col=len(header), values_only=True),
start=2,
):
if all(value is None for value in values):
continue
if any(isinstance(value, str) and value.startswith("=") for value in values):
raise ValueError(
f"Formula found in {sheet.title} row {excel_row}; "
"this example merges literal values only."
)
rows.append(tuple(values))
return header, rows
def main() -> None:
source_path = SOURCE.resolve(strict=True)
if not source_path.is_file():
raise ValueError("SOURCE must be an Excel file.")
if len(set(SHEETS)) != len(SHEETS):
raise ValueError("SHEETS contains a duplicate sheet name.")
source_workbook = load_workbook(
source_path,
read_only=True,
data_only=False,
)
try:
missing = [name for name in SHEETS if name not in source_workbook.sheetnames]
if missing:
raise ValueError(f"Missing sheets: {missing}")
expected_header = None
combined_rows = []
for sheet_name in SHEETS:
sheet = source_workbook[sheet_name]
header, rows = read_sheet_rows(sheet, expected_header)
if expected_header is None:
expected_header = header
for row in rows:
combined_rows.append((sheet_name, *row))
finally:
source_workbook.close()
OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_DIR.mkdir() # Refuse to reuse an existing output folder.
output_workbook = Workbook()
output_sheet = output_workbook.active
output_sheet.title = OUTPUT_SHEET
output_sheet.append([SOURCE_COLUMN, *expected_header])
for row in combined_rows:
output_sheet.append(row)
output_workbook.save(OUTPUT)
check_workbook = load_workbook(OUTPUT, read_only=True, data_only=False)
try:
if check_workbook.sheetnames != [OUTPUT_SHEET]:
raise RuntimeError("Unexpected output worksheet structure.")
check_sheet = check_workbook[OUTPUT_SHEET]
written = list(check_sheet.iter_rows(values_only=True))
finally:
check_workbook.close()
expected = [(SOURCE_COLUMN, *expected_header), *combined_rows]
if written != expected:
raise RuntimeError("Saved workbook does not match the expected rows.")
print(f"Merged {len(SHEETS)} sheets into {len(combined_rows)} data rows.")
print(f"Verified {len(expected_header) + 1} columns and {len(combined_rows)} data rows.")
print(f"Output: {OUTPUT.as_posix()}")
if __name__ == "__main__":
main()
python excel_merge_sheets.py합성 통합문서의 출력에는 Combined라는 워크시트 하나만 있어야 합니다. 첫 번째 행은 source_sheet, order_id, customer, amount이고 그 아래에는 앞에서 계산한 데이터 6개가 들어가야 합니다.
아래 예상 콘솔 출력은 코드와 합성 데이터를 바탕으로 손으로 도출한 결과입니다. 실제 실행에서 가져온 로그가 아닙니다.
Merged 3 sheets into 6 data rows.
Verified 4 columns and 6 data rows.
Output: outputs/excel_merge_result/merged_orders.xlsx| 확인 항목 | 예상 결과 |
|---|---|
| 선택한 원본 시트 | 3 |
| 결합된 데이터 행 | 6 |
| 출력 열 | 4 |
| East 행 | 2 |
| West 행 | 2 |
| Online 행 | 2 |
| amount 합계 | 620 |
실제 통합문서에서는 모든 시트를 자동으로 합치기보다 SHEETS를 명시적으로 지정하는 편이 안전합니다. 같은 파일 안에 있는 조회표, 메모, 숨겨진 시트, 요약 시트 등을 실수로 포함할 가능성을 줄일 수 있습니다.
| 증상 | 확인할 사항 |
|---|---|
| ModuleNotFoundError: No module named openpyxl | 같은 Python 환경에 python -m pip install openpyxl 명령으로 openpyxl을 설치하세요. |
| FileNotFoundError | SOURCE와 터미널의 작업 디렉터리를 확인하세요. 이 예제에서는 합성 통합문서 준비용 스크립트를 먼저 실행해야 합니다. |
| Missing sheets | SHEETS의 이름을 공백과 대소문자까지 정확히 확인하세요. |
| Header mismatch | 선택한 모든 시트가 동일한 열 이름을 동일한 순서로 사용하는지 확인하세요. |
| Reserved column already exists | 원본의 source_sheet 열 이름을 바꾸거나 SOURCE_COLUMN을 충돌하지 않는 이름으로 변경하세요. |
| Formula found | 합치기 전에 수식을 어떻게 처리할지 결정하세요. 이 예제는 문맥이 없는 수식 표현식을 그대로 복사하지 않고 중단하도록 작성했습니다. |
| FileExistsError | 출력 폴더가 이미 존재합니다. 이전 결과를 확인하고 덮어쓰지 말고 새 대상 위치를 사용하세요. |
OUTPUT_DIR을 만든 뒤 저장이나 검증에서 실패하면 폴더 또는 통합문서가 일부 결과로 남을 수 있습니다. 검증되지 않은 결과로 취급하고 문제를 수정한 뒤에는 새 대상 위치를 사용하세요.
이 스크립트는 헤더가 한 줄인 직사각형 데이터 시트를 대상으로 합니다. 여러 줄 헤더, 병합된 헤더 셀, 피벗 테이블, 차트, 이미지, 하나의 시트에 서로 관련 없는 표가 여러 개 있는 구조는 처리 대상으로 삼지 않습니다.
통합되는 것은 셀 값뿐입니다. 숫자 표시 형식, 글꼴, 채우기, 조건부 서식, 데이터 유효성 검사, 하이퍼링크, 메모, 수식, 행 높이, 열 너비, 워크시트 설정은 복제하지 않습니다. 날짜와 숫자 자체는 값으로 전달될 수 있지만 원래의 표시 형식은 복사하지 않습니다.
또한 이 스크립트는 출력 통합문서를 만들기 전에 결합할 모든 행 튜플을 Python 메모리에 저장합니다. 이 작은 검증 예제에는 편리하지만 매우 큰 통합문서에는 적합하지 않을 수 있습니다. 큰 파일이라면 write-only 출력을 사용하는 스트리밍 방식과 별도의 증분 검증을 적용하면 메모리 사용량을 줄일 수 있습니다.
2026-09-20 · 예제 수동 검토 · 대상: Python 3.12 · openpyxl 필요 · 실행하지 않음
설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.