スクリプト・ファイル自動化

ヘッダーを失わずに大きなCSVを小さなファイルへ分割する

CSVをデータレコード数で分割し、すべての出力ファイルに同じヘッダーを繰り返して、元のファイルは変更しません。7レコードの合成例を使って境界を確認し、すべてのレコードが元の順序のまま保持されることを検証します。

目次を表示

この翻訳はAIで作成しました。コード、単位、数値は原文と併せて確認してください。各言語のネイティブ話者による校閲は、まだ完了していません。 English

対象読者このガイドは、CSVエクスポートの内容を手作業で編集せずに小さなファイルへ分割する必要がある人向けです。

準備するもの
  • Python 3.12と、そのバージョンを起動するターミナルコマンド。
  • UTF-8ファイルを保存でき、引用符付きの複数行テキストを保持できるテキストエディター。
  • 入力を読み取る権限と、出力に十分なディスク容量がある作業フォルダー。
  • 必要なのはPython標準ライブラリのcsvとpathlibだけです。

01何を1行として数えるかを定義する

このスクリプトは、選択した最大データレコード数までを含む番号付きCSVファイルを作成します。各ファイルの先頭には同じヘッダーが入ります。ヘッダーは上限数に含めません。上限が3でデータレコードが7件の場合、各ファイルには3件、3件、1件のデータレコードが入ります。

CSVレコードは、物理的なテキスト1行と一致するとは限りません。引用符で囲まれたフィールドには改行を含めることができます。csvモジュールはCSVの引用規則に従ってレコードを読み取るため、複数行のメモも別の行になるのではなく、元のレコードに含まれたままになります。

02合成入力データを作成する

このデータセットは合成データで、この記事のために作成したものです。作業フォルダーにsample.csvとして保存してください。引用符で囲まれたnote内の改行はそのままコピーしてください。これは、通常の行単位の分割では正しく処理できないケースを意図的に含めています。

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内のカンマは1つのフィールド内に含まれます。R003のnoteは物理的には2行にまたがりますが、1つのフィールドのままです。unitsの値の合計は60です: 12 + 7 + 9 + 5 + 11 + 6 + 10。

03作業フォルダーを準備する

  1. sample.csvとsplit_large_csv.pyという新しいスクリプトを同じ作業フォルダーに置きます。
  2. 次のセクションにある完全なPythonコードをスクリプトへ貼り付けます。
  3. この例ではROWS_PER_FILEを3のままにします。この値は正の整数である必要があります。
  4. 作業フォルダーでターミナルを開き、以下のコマンドを実行します。
text
python split_large_csv.py

相対パスはスクリプトの場所から自動的に解決されるのではなく、ターミナルの作業ディレクトリを基準に解決されます。親フォルダーoutputsはすでに存在していても構いませんが、この実行を開始するときにoutputs/sample_partsが存在していてはいけません。

04レコードを分割し、書き込まれたファイルを検証する

分割処理ではレコードを順次読み込み、各パートを排他的作成モードで開きます。書き込み後に元ファイルと各パートを再度読み取り、ヘッダー、フィールド値、レコード順序、件数を比較します。成功メッセージは、その比較が完了してから表示されます。

python
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です。newline引数により、csvモジュールがレコード終端と埋め込み改行を処理できます。

05期待結果を手作業で確認する

期待されるファイルを以下に示します。unitsの合計は分割スクリプトが計算する値ではなく、手作業によるクロスチェックです。すべてのファイルでrecord_id,team,units,noteをヘッダーとして繰り返す必要があります。

outputs/sample_parts内のファイルレコードIDデータレコード数Units合計
part_0001.csvR001, R002, R003328
part_0002.csvR004, R005, R006322
part_0003.csvR007110

期待されるコンソール表示を以下に示します。これは手作業で導出したもので、実際の実行ログを取得したものではありません。

text
Verified 7 data records in 3 files.
Output folder: outputs/sample_parts

28 + 22 + 10が元の合計60と一致することを確認してください。最初のパートをテキストエディターで開き、引用符で囲まれたカンマと複数行のnoteを確認します。見た目の行数だけを数えると、データレコード数を誤って数えることになります。

06より大きな入力を使う前に確認する

  1. 3個のファイルすべてに同じ4個のヘッダーフィールドがあり、各ファイルでヘッダーが1回だけ現れることを確認します。
  2. ファイルを番号順に読みます。R001からR007までがそれぞれ1回ずつ現れ、欠落や重複がないことを確認します。
  3. スクリプトが検証成功を報告することを確認します。比較では件数だけでなくフィールド文字列も確認するため、件数が同じだけでは不十分です。
  4. 出力先を変更せずにスクリプトを再度実行します。別のパートを書き込む前にFileExistsErrorで停止する必要があります。

境界条件は別々の作業フォルダーでテストしてください。6レコードなら3件ずつの2パートになり、空の3番目のパートは作成されないはずです。ヘッダーだけの入力では空の出力フォルダーが残るはずです。完全に空の入力は拒否されるはずです。これらの期待動作はコードを確認して判断したもので、ここでは実行していません。

07よくあるエラーを把握する

症状確認すること
FileNotFoundErrorSOURCEとターミナルの作業ディレクトリを確認します。ファイル名がsample.csv.txtではなくsample.csvであることを確認してください。
FileExistsError既存の出力先を確認し、outputs配下の新しいフォルダーを選択します。既存の出力先が空でも拒否されます。
UnicodeDecodeError元ファイルのエンコーディングを確認します。デコードエラーを無視せず、正しくデコードされたコピーを取得または作成してから分割してください。
ヘッダーの欠落または重複空ではない列名を指定します。完全に同一の重複列名は拒否されますが、それ以外のヘッダーテキストは保持されます。
フィールド数の誤りまたはcsv.Error区切り文字、引用符、空レコードを確認します。このスクリプトは標準的な二重引用符による引用を使用したカンマ区切り入力を想定しています。
書き込み中または検証中の失敗その出力先は未完成として扱ってください。エラーを確認し、修正後の実行では新しい出力先を使用します。

処理の後半でエラーが発生すると、不完全なファイルが残る場合があります。スクリプトは現在のファイルを閉じますが、フォルダー全体をロールバックしたり自動削除したりはしません。

08規模を拡大する前に制限を理解する

この例が自分の環境で正常に動作したら、SOURCEを変更し、outputs配下に新しいOUTPUT_DIRを選び、ROWS_PER_FILEを増やします。レコード数の上限はバイトサイズの上限ではありません。長いフィールドがあると、同じレコード数のグループでもディスク使用量が大きく異なる場合があります。

このスクリプトが保持するのは解析されたフィールド文字列であり、元のバイト列、引用形式、レコード終端形式ではありません。データセット全体を一度に読み込むことはありませんが、異常に大きいフィールドは依然としてメモリを必要とし、CSVパーサーのフィールドサイズ上限を超える可能性があります。検証では元ファイルと出力をもう一度読み取ります。処理中は元ファイルを変更しないでください。これはロックされたスナップショットでもトランザクション型バックアップでもありません。

実行・検証の記録

2026-09-20 · 手作業で確認した例 · 対象: Python 3.12 · 標準ライブラリ: csv, pathlib · 未実行

  • 引用符付きの複数行noteを1つのフィールドとして扱い、4列と7データレコードを手作業で数えました。
  • レコードを3、3、1のグループへ手作業で割り当てました。
  • 各グループの合計28、22、10を手作業で計算し、全体が60になることを確認しました。
  • 出力ファイル名、排他的作成、ヘッダーの繰り返し、順次比較ロジックを確認しました。
  • 例とコードから期待されるコンソール表示を導出しました。
検証範囲の限界
  • この回答の作成者はコードを実行しておらず、Pythonランタイムやファイルシステムの動作はテストしていません。
  • 境界入力、不正なファイル、出力の衝突はコード確認のみで検討しました。
  • 大規模ファイルの性能、メモリ使用量、処理中の元ファイル変更、中断された書き込みからの復旧はテストしていません。

サイト全体の執筆・検証方針

参考資料

説明と例は独自に作成しました。関連する動作や概念は、以下の公式資料で確認できます。