헤더를 유지하면서 큰 CSV를 작은 파일로 나누기
CSV를 데이터 레코드 수에 따라 나누고, 각 출력 파일에 헤더를 넣으면서 원본은 그대로 보존합니다. 7개 레코드로 구성된 합성 예제를 이용해 분할 경계를 확인하고, 모든 레코드가 원래 순서대로 유지되는지 검증합니다.
원본 파일을 덮어쓰지 않고 여러 이미지를 새 폴더에 축소하며, 가로세로 비율도 유지합니다. 크기가 명확한 작은 합성 이미지 세트를 이용해 예상 출력 크기를 손으로 확인할 수 있습니다.
이런 분께 맞아요여러 이미지의 원본을 그대로 보존하면서 비율이 유지된 작은 사본을 만들려는 사람을 위한 안내입니다.
목표는 모든 이미지를 가로 300픽셀, 세로 300픽셀 상자 안에 들어가도록 만들면서 원래 가로세로 비율을 유지하는 것입니다. 이미 이 상자보다 작은 이미지는 확대하지 않습니다. 원본 이미지는 변경하지 않고, 리사이즈된 사본은 모두 별도의 출력 폴더에 저장합니다.
이미지의 가로 크기를 W, 세로 크기를 H라고 하면 300/W, 300/H, 1 가운데 가장 작은 값을 scale로 정합니다. 그 하나의 배율을 가로와 세로에 모두 적용합니다. 두 방향에 같은 배율을 사용하면 이미지가 늘어나거나 찌그러지는 것을 막을 수 있습니다.
scale = min(300 / width, 300 / height, 1)
new_width = int(width * scale)
new_height = int(height * scale)아래 파일들은 이 글을 위해 만든 합성 이미지입니다. 준비용 스크립트는 크기를 알고 있는 단순한 PNG 이미지 4개를 만듭니다. 이미지의 시각적 내용 자체는 중요하지 않으며, 예상되는 리사이즈 결과를 손으로 계산하기 쉽도록 크기를 정했습니다.
from pathlib import Path
from PIL import Image
SOURCE = Path("outputs") / "image_resize_demo"
IMAGES = {
"landscape.png": ((800, 600), (220, 80, 80)),
"portrait.png": ((600, 800), (80, 120, 220)),
"small.png": ((200, 100), (80, 180, 100)),
"wide.png": ((1200, 300), (180, 120, 60)),
}
SOURCE.parent.mkdir(parents=True, exist_ok=True)
SOURCE.mkdir() # Stop if this synthetic source already exists.
for filename, (size, color) in IMAGES.items():
image = Image.new("RGB", size, color)
target = SOURCE / filename
image.save(target, format="PNG")
python create_resize_demo.py| 파일 | 원본 가로 | 원본 세로 | 가로세로 비율 |
|---|---|---|---|
| landscape.png | 800 | 600 | 4:3 |
| portrait.png | 600 | 800 | 3:4 |
| small.png | 200 | 100 | 2:1 |
| wide.png | 1200 | 300 | 4:1 |
원본 4개의 전체 픽셀 수는 1,240,000개입니다. 계산식은 480,000 + 480,000 + 20,000 + 360,000입니다. 픽셀 수는 예제를 이해하는 데 유용하지만, PNG 압축률은 이미지 내용에 따라 달라지므로 파일의 바이트 크기와 같은 의미는 아닙니다.
landscape.png에서는 가로가 제한 조건입니다. 300/800 = 0.375입니다. 가로와 세로에 모두 0.375를 곱하면 300×225가 됩니다. portrait.png에서는 세로가 제한 조건이며, 300/800 = 0.375이므로 결과는 225×300입니다.
small.png는 두 방향 모두 이미 300 이하입니다. 배율의 최댓값을 1로 제한했으므로 200×100 그대로 유지됩니다. wide.png는 가로가 제한 조건입니다. 300/1200 = 0.25이므로 결과는 300×75입니다.
| 파일 | 배율 | 예상 출력 | 출력 픽셀 수 |
|---|---|---|---|
| landscape.png | 0.375 | 300 × 225 | 67,500 |
| portrait.png | 0.375 | 225 × 300 | 67,500 |
| small.png | 1 | 200 × 100 | 20,000 |
| wide.png | 0.25 | 300 × 75 | 22,500 |
따라서 리사이즈된 사본의 전체 픽셀 수는 177,500개입니다. 원본은 별도로 그대로 남아 있으므로 이 숫자를 디스크 공간 절약량으로 해석하면 안 됩니다.
다음 스크립트를 batch_image_resize.py로 저장하세요. SOURCE 바로 아래에 있는 PNG, JPEG, WebP 파일을 처리합니다. 하위 폴더까지 재귀적으로 검색하지 않습니다. 대상 폴더는 exist_ok 없이 mkdir로 생성하므로, 기존 출력 폴더가 있으면 이전 결과와 새 결과를 섞지 않고 실행을 중단합니다.
from pathlib import Path
from PIL import Image, ImageOps
SOURCE = Path("outputs") / "image_resize_demo"
OUTPUT_DIR = Path("outputs") / "image_resize_result"
MAX_WIDTH = 300
MAX_HEIGHT = 300
SUPPORTED = {".png", ".jpg", ".jpeg", ".webp"}
def target_size(width: int, height: int) -> tuple[int, int]:
scale = min(MAX_WIDTH / width, MAX_HEIGHT / height, 1)
new_width = max(1, int(width * scale))
new_height = max(1, int(height * scale))
return new_width, new_height
def main() -> None:
if not SOURCE.is_dir():
raise FileNotFoundError(f"Source folder not found: {SOURCE}")
if MAX_WIDTH < 1 or MAX_HEIGHT < 1:
raise ValueError("Maximum dimensions must be positive integers.")
source_resolved = SOURCE.resolve()
output_resolved = OUTPUT_DIR.resolve()
if source_resolved == output_resolved:
raise ValueError("SOURCE and OUTPUT_DIR must be different.")
OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_DIR.mkdir() # Refuse to reuse an existing destination.
candidates = sorted(
path for path in SOURCE.iterdir()
if path.is_file() and path.suffix.lower() in SUPPORTED
)
resized_count = 0
unchanged_size_count = 0
for source_path in candidates:
output_path = OUTPUT_DIR / source_path.name
if output_path.exists():
raise FileExistsError(f"Output already exists: {output_path}")
with Image.open(source_path) as opened:
image = ImageOps.exif_transpose(opened)
image.load()
old_size = image.size
new_size = target_size(*old_size)
if new_size == old_size:
result = image.copy()
unchanged_size_count += 1
else:
result = image.resize(new_size, Image.Resampling.LANCZOS)
resized_count += 1
save_options = {}
if source_path.suffix.lower() in {".jpg", ".jpeg"}:
save_options["quality"] = 90
result.save(output_path, **save_options)
result.close()
print(f"{source_path.name}: {old_size[0]}x{old_size[1]} -> {new_size[0]}x{new_size[1]}")
print(f"Processed {len(candidates)} images.")
print(f"Resized: {resized_count}; already within limit: {unchanged_size_count}.")
print(f"Output folder: {OUTPUT_DIR.as_posix()}")
if __name__ == "__main__":
main()
ImageOps.exif_transpose는 EXIF 방향 정보가 있는 경우 리사이즈 크기를 계산하기 전에 해당 방향을 실제 픽셀에 적용합니다. 이 예제의 합성 PNG에는 그러한 방향 정보가 필요하지 않으므로 표시 크기는 앞에서 적은 값과 같습니다.
파일명은 정렬된 순서로 처리하므로 예상 콘솔 순서는 landscape.png, portrait.png, small.png, wide.png입니다. 3개 이미지는 작아지고, 1개 이미지는 원래 크기를 유지합니다.
landscape.png: 800x600 -> 300x225
portrait.png: 600x800 -> 225x300
small.png: 200x100 -> 200x100
wide.png: 1200x300 -> 300x75
Processed 4 images.
Resized: 3; already within limit: 1.
Output folder: outputs/image_resize_result이 예상 출력은 실제 실행 로그를 가져온 것이 아니라 손으로 계산한 결과입니다. 중요한 확인 항목은 이미지 크기, 파일 수, 그리고 outputs/image_resize_demo 안의 원본이 그대로 남아 있는지 여부입니다.
| 확인 항목 | 예상값 |
|---|---|
| 원본 폴더의 파일 수 | 4 |
| 출력 폴더의 파일 수 | 4 |
| 축소된 파일 수 | 3 |
| 이미 제한 범위 안에 있던 파일 수 | 1 |
| 출력 중 가장 큰 가로 | 300 |
| 출력 중 가장 큰 세로 | 300 |
파일명이나 콘솔 메시지만으로 판단하지 마세요. 출력 파일을 직접 열어 크기를 확인해야 합니다. 아래의 선택적 확인 스크립트는 원본과 출력 폴더를 읽고 크기를 표시하기만 하며 파일에는 아무것도 쓰지 않습니다.
from pathlib import Path
from PIL import Image
SOURCE = Path("outputs") / "image_resize_demo"
OUTPUT_DIR = Path("outputs") / "image_resize_result"
for source_path in sorted(SOURCE.glob("*.png")):
output_path = OUTPUT_DIR / source_path.name
with Image.open(source_path) as original, Image.open(output_path) as resized:
print(
f"{source_path.name}: "
f"original={original.size}, output={resized.size}"
)
| 증상 | 확인할 사항 |
|---|---|
| ModuleNotFoundError: No module named PIL | 스크립트를 실행하는 동일한 Python 환경에 python -m pip install Pillow 명령으로 Pillow를 설치하세요. |
| FileNotFoundError | SOURCE와 터미널의 현재 작업 디렉터리를 확인하세요. 이 예제에서는 먼저 합성 데이터 준비용 스크립트를 실행해야 합니다. |
| FileExistsError | 출력 폴더가 이미 존재합니다. 자동으로 덮어쓰게 하지 말고 기존 결과를 검토한 뒤 새 대상 폴더를 선택하세요. |
| 이미지를 식별할 수 없음 | 확장자는 지원되는 형식처럼 보이지만 내용이 손상되었거나 실제 이미지가 아닐 수 있습니다. 실패한 배치는 미완성으로 취급하세요. |
| 예상과 다른 회전 | 방향 메타데이터와 실제 픽셀 방향은 다를 수 있습니다. 리사이즈 전에 ImageOps.exif_transpose를 사용하지만 중요한 이미지는 눈으로 확인하세요. |
| 예상과 다른 출력 바이트 크기 | 픽셀 크기만으로 압축된 파일 크기가 결정되는 것은 아닙니다. 형식, 이미지 내용, 메타데이터, JPEG 품질 등이 영향을 줍니다. |
일부 파일을 저장한 뒤 처리에 실패하면 스크립트는 이미 생성된 결과를 삭제하지 않습니다. 문제를 조사하기 위해 해당 대상 폴더를 보관하거나, 수정 후에는 새로운 출력 디렉터리를 사용하세요.
이 예제는 한 폴더 바로 아래에 있는 파일만 처리하며 PNG, JPEG, WebP 확장자를 지원합니다. 중첩된 폴더 구조를 그대로 복제하지 않고, 애니메이션의 각 프레임을 개별적으로 리사이즈하지 않으며, 모든 종류의 메타데이터나 색상 관리 정보를 동일하게 보존한다고 보장하지 않습니다.
리샘플링은 픽셀 데이터를 변경합니다. 크기가 그대로인 이미지도 열고 다시 저장하면 인코딩된 바이트나 메타데이터가 달라질 수 있습니다. JPEG 출력은 손실 압축이며, 이 스크립트는 JPEG 사본을 quality 90으로 저장합니다. 따라서 원래 JPEG의 바이트열이나 원래 압축 설정을 보존하는 작업이 아닙니다.
int 계산은 소수점이 있는 크기를 아래쪽 정수로 변환합니다. 따라서 임의의 원본 크기에서는 이미지 크기가 정수여야 하므로 결과의 픽셀 비율이 수학적인 비율과 아주 조금 달라질 수 있습니다. 이 글에서 사용한 합성 이미지 크기는 정확히 나누어지므로 예상 결과 4개에는 이러한 반올림 모호성이 없습니다.
2026-09-20 · 예제 수동 검토 · 대상: Python 3.12 · Pillow 필요 · 실행하지 않음
설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.