파일 크기와 SHA-256으로 중복 파일을 찾고 삭제 전에 검토하기
파일 크기로 비교 대상을 추리고 SHA-256 해시값을 비교한 뒤, 원본을 변경하지 않고 검토용 CSV를 작성합니다. 7개 파일로 구성된 합성 예제를 이용해 보고서에 포함될 경로와 제외될 경로를 확인합니다.
CSV를 데이터 레코드 수에 따라 나누고, 각 출력 파일에 헤더를 넣으면서 원본은 그대로 보존합니다. 7개 레코드로 구성된 합성 예제를 이용해 분할 경계를 확인하고, 모든 레코드가 원래 순서대로 유지되는지 검증합니다.
이런 분께 맞아요CSV 내용을 직접 편집하지 않고 내보낸 파일을 여러 개의 작은 파일로 나누려는 사람을 위한 안내입니다.
이 스크립트는 지정한 최대 데이터 레코드 수에 맞춰 번호가 붙은 CSV 파일을 만듭니다. 각 파일은 동일한 헤더로 시작합니다. 헤더는 분할 기준 개수에 포함하지 않습니다. 기준이 3이고 데이터 레코드가 7개라면, 각 파일에는 데이터 레코드가 3개, 3개, 1개씩 들어갑니다.
CSV 레코드가 반드시 텍스트의 물리적인 한 줄과 일치하는 것은 아닙니다. 따옴표로 감싼 필드에는 줄바꿈이 들어갈 수 있습니다. csv 모듈은 CSV의 따옴표 처리 규칙에 따라 레코드를 읽으므로, 여러 줄로 된 메모가 별도 행으로 분리되지 않고 해당 레코드에 유지됩니다.
아래 데이터는 이 글을 위해 작성한 합성 데이터입니다. 작업 폴더에 sample.csv라는 이름으로 저장하세요. 따옴표 안의 메모에 있는 줄바꿈도 그대로 복사해야 합니다. 단순히 텍스트를 줄 단위로 나누면 잘못 처리되는 상황을 의도적으로 포함했습니다.
record_id,team,units,note
R001,Support,12,"starter, pack"
R002,Ops,7,repeat
R003,Support,9,"two
lines"
R004,Sales,5,normal
R005,Ops,11,normal
R006,Sales,6,normal
R007,Support,10,normal
열은 4개이고 데이터 레코드는 7개입니다. starter, pack의 쉼표는 하나의 필드 안에 포함됩니다. R003의 메모는 물리적으로 2줄이지만 하나의 필드입니다. units 값의 합계는 60입니다. 계산식은 12 + 7 + 9 + 5 + 11 + 6 + 10입니다.
python split_large_csv.py상대 경로의 기준은 터미널의 현재 작업 디렉터리이며, 자동으로 스크립트 위치를 기준으로 삼는 것은 아닙니다. 상위 outputs 폴더는 이미 있어도 되지만, 실행을 시작할 때 outputs/sample_parts는 없어야 합니다.
분할 과정에서는 레코드를 차례로 읽고, 각 분할 파일을 기존 파일이 있으면 실패하는 배타적 생성 모드로 엽니다. 저장 후에는 원본과 분할 파일을 다시 읽어 헤더, 필드 값, 레코드 순서, 개수를 비교합니다. 이 비교가 끝나야 성공 메시지가 표시됩니다.
import csv
from pathlib import Path
SOURCE = Path("sample.csv")
OUTPUT_DIR = Path("outputs") / "sample_parts"
ROWS_PER_FILE = 3
def part_path(number: int) -> Path:
return OUTPUT_DIR / f"part_{number:04d}.csv"
def split_csv() -> tuple[int, int]:
if type(ROWS_PER_FILE) is not int or ROWS_PER_FILE < 1:
raise ValueError("ROWS_PER_FILE must be a positive integer.")
with SOURCE.open("r", encoding="utf-8-sig", newline="") as source:
reader = csv.reader(source, strict=True)
header = next(reader, None)
if not header or any(not name.strip() for name in header):
raise ValueError("Missing header or blank column name.")
if len(set(header)) != len(header):
raise ValueError("Duplicate column names.")
OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_DIR.mkdir() # Refuse to reuse an existing output path.
total = 0
parts = 0
target = None
try:
for record_no, row in enumerate(reader, start=1):
if len(row) != len(header):
raise ValueError(
f"Data record {record_no}: wrong number of fields."
)
if total % ROWS_PER_FILE == 0:
if target is not None:
target.close()
parts += 1
target = part_path(parts).open(
"x", encoding="utf-8", newline=""
)
writer = csv.writer(target)
writer.writerow(header)
writer.writerow(row)
total += 1
finally:
if target is not None:
target.close()
return total, parts
def verify_csv(expected_rows: int, part_count: int) -> None:
with SOURCE.open("r", encoding="utf-8-sig", newline="") as source:
original = csv.reader(source, strict=True)
header = next(original, None)
seen = 0
for number in range(1, part_count + 1):
with part_path(number).open(
"r", encoding="utf-8", newline=""
) as part:
rows = csv.reader(part, strict=True)
if next(rows, None) != header:
raise ValueError(f"Header mismatch in part {number}.")
count = 0
for row in rows:
if row != next(original, None):
raise ValueError(f"Data mismatch in part {number}.")
count += 1
required = min(
ROWS_PER_FILE,
expected_rows - (number - 1) * ROWS_PER_FILE,
)
if count != required:
raise ValueError(f"Row count mismatch in part {number}.")
seen += count
if seen != expected_rows or next(original, None) is not None:
raise ValueError("Overall row count mismatch.")
if __name__ == "__main__":
total, parts = split_csv()
verify_csv(total, parts)
print(f"Verified {total} data records in {parts} files.")
print(f"Output folder: {OUTPUT_DIR.as_posix()}")
입력은 파일 앞에 바이트 순서 표식이 있는 UTF-8과 없는 UTF-8을 모두 읽을 수 있습니다. 출력은 해당 표식 없이 UTF-8로 저장합니다. newline 인수를 설정하면 csv 모듈이 레코드 끝과 필드 안의 줄바꿈을 처리합니다.
생성될 것으로 예상되는 파일은 아래와 같습니다. units 합계는 수동 교차 확인용이며, 분할 스크립트가 계산하는 값은 아닙니다. 모든 파일의 헤더는 record_id,team,units,note여야 합니다.
| outputs/sample_parts 안의 파일 | 레코드 ID | 데이터 레코드 수 | units 합계 |
|---|---|---|---|
| part_0001.csv | R001, R002, R003 | 3 | 28 |
| part_0002.csv | R004, R005, R006 | 3 | 22 |
| part_0003.csv | R007 | 1 | 10 |
예상 콘솔 출력은 아래와 같습니다. 예제와 코드를 바탕으로 손으로 도출한 내용이며, 실제 실행에서 수집한 로그가 아닙니다.
Verified 7 data records in 3 files.
Output folder: outputs/sample_parts28 + 22 + 10이 원본 합계인 60과 같은지 확인하세요. 첫 번째 분할 파일을 텍스트 편집기로 열어 따옴표 안의 쉼표와 여러 줄 메모를 살펴보세요. 화면에 보이는 줄 수만 세면 데이터 레코드 수를 잘못 계산하게 됩니다.
경계 조건은 별도 작업 폴더에서 확인하세요. 레코드가 6개라면 3개씩 담긴 분할 파일 2개만 생성되고, 비어 있는 세 번째 파일은 생기지 않아야 합니다. 헤더만 있는 입력은 빈 출력 폴더를 남겨야 합니다. 완전히 빈 입력은 거부되어야 합니다. 이러한 예상 동작은 코드를 검토한 것이며, 여기서 실행한 결과는 아닙니다.
| 증상 | 확인할 사항 |
|---|---|
| FileNotFoundError | SOURCE와 터미널의 작업 디렉터리를 확인하세요. 파일명이 sample.csv.txt가 아니라 sample.csv인지 확인하세요. |
| FileExistsError | 기존 대상 폴더를 검토한 뒤 outputs 아래의 새 폴더를 선택하세요. 이미 존재하면 빈 폴더라도 사용할 수 없습니다. |
| UnicodeDecodeError | 원본 인코딩을 확인하세요. 디코딩 오류를 무시하지 말고, 올바르게 디코딩되는 사본을 확보하거나 만든 뒤 분할하세요. |
| 헤더 누락 또는 중복 | 비어 있지 않은 열 이름을 사용하세요. 정확히 같은 열 이름은 중복으로 거부하며, 그 외의 헤더 텍스트는 그대로 유지합니다. |
| 필드 수 불일치 또는 csv.Error | 구분자, 따옴표, 빈 레코드를 확인하세요. 이 스크립트는 쉼표 구분자와 표준 큰따옴표 처리 규칙을 사용하는 입력을 대상으로 합니다. |
| 저장 또는 검증 중 실패 | 해당 대상 폴더는 미완성 결과로 취급하세요. 오류를 검토하고, 수정 후에는 새 대상 폴더로 다시 실행하세요. |
처리 도중 뒤늦게 오류가 발생하면 일부 파일이 남을 수 있습니다. 스크립트는 현재 파일을 닫지만, 폴더를 실행 전 상태로 되돌리거나 파일을 자동 삭제하지 않습니다.
본인 컴퓨터에서 예제가 정상적으로 처리되면 SOURCE를 바꾸고, outputs 아래에 새로운 OUTPUT_DIR을 지정한 뒤 ROWS_PER_FILE을 늘리세요. 레코드 수 제한은 바이트 단위의 파일 크기 제한이 아닙니다. 필드가 길면 레코드 수가 같은 묶음이라도 디스크 사용량은 크게 달라질 수 있습니다.
이 스크립트는 파싱된 필드 문자열을 보존하지만 원본 바이트, 따옴표 표기 방식, 레코드 끝의 줄바꿈 형식까지 보존하지는 않습니다. 전체 데이터를 한꺼번에 불러오지는 않지만, 매우 큰 필드는 여전히 메모리가 필요하고 CSV 파서의 필드 크기 제한을 넘을 수 있습니다. 검증 과정에서는 원본과 출력을 추가로 읽습니다. 실행 내내 원본을 변경하지 마세요. 이 작업은 파일을 잠가 생성하는 스냅샷이나 트랜잭션 방식의 백업이 아닙니다.
2026-09-20 · 예제 수동 검토 · 대상: Python 3.12 · 표준 라이브러리: csv, pathlib · 실행하지 않음
설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.