第134回 実務で使えるPython基礎:ファイル入出力と表データフォーマット(CSV/Excel/JSONL/圧縮)で作る堅牢な入出力ワークフロー

現場でのデータ入出力は地味に厄介です。文字化け、途中落ち、巨大ファイル、フォーマットの不揃い――これらに遭遇すると、時間だけが無情に消耗します。本記事では「実務でよくある失敗」を避ける具体的な手順と、すぐ試せるサンプルスクリプト(Excel→JSONL(分割・gzip・原本退避))を示します。前回(第133回:contextmanager)や第119回のpathlibの知識を活かして進めてください。

1) ファイル入出力の基本と安全化の原則

まずは基本的な用語整理と、現場で守るべき原則を示します。

項目 要点
モード(open) テキスト(’r’,’w’,’a’)とバイナリ(’rb’,’wb’)を用途に応じて使い分ける。圧縮やバイナリ形式は必ずバイナリモード。
改行 テキスト読み込み時はnewline=”(csv用)やnewline=Noneの違いに注意。CRLF/CRの混在に備える。
エンコーディング 明示指定を原則(UTF-8)。Shift-JISやBOM付きUTF-8は注意が必要。自動検出は補助手段。
BOM CSVやTSVの先頭BOMは読み取り時に除去する。utf-8-sigが便利。

実務ルール(簡潔)

  • 読み取りは明示的なエンコーディングか検出を使う。
  • 書き込みは一時ファイル→atomic replaceで原子性を担保する。
  • 巨大ファイルはストリーミング処理(chunked)で扱う。

2) 原子書き込み・一時ファイル・ロールバックパターン

途中で失敗して中途ファイルが残ると自動処理は停止します。安定運用には原子更新パターンが必須です。

パターン 説明
一時ファイル + os.replace 処理完了後にos.replaceで上書き。途中で落ちても元ファイルは残る。
.inprogress拡張子 処理中は拡張子を付ける。完成後にリネームして通知・転送。
チェックサム/タイムスタンプ 受け渡しの整合性確認に使う(受信側が完全性を検証できる)。

3) CSV実務:csvモジュール vs pandas

CSVをどう扱うかはデータサイズと処理内容で決めます。以下は選択の目安です。

用途 推奨 理由
大規模ストリーミング(メモリに乗らない) csvモジュール 逐次読みでメモリ効率が良い。chunksizeは自作のジェネレータで実現。
集計・列操作・高速開発 pandas 便利で高速。ただしメモリ使用量に注意。
フォーマット自動判定 csv.Sniffer 区切り文字の推定に有用。ただし不完全なサンプルだと失敗する。

CSVの実務ヒント

  • 読み取りはnewline=”、エンコーディングは可能ならutf-8-sigでBOMを無視。
  • 不正行が混ざる可能性がある場合はtry/exceptでログに倒す(行をスキップして続行するポリシーを可視化)。
  • パイプ区切りなど非標準区切りにも対応できるようSnifferを併用する。

4) Excel実務:openpyxl/xlrdの使い分け

Excelはバイナリ寄りで型推定が厄介です。現場ルールを決めておくとぶれにくくなります。

目的 ライブラリ 備考
.xlsxの読み書き openpyxl 読み取り専用モード(read_only=True)でメモリを節約。
.xlsの読み取り xlrd(古い) 最近はxlsが減少。互換性を確認。
大量行の読み取り openpyxlのiter_rows ストリーミングで低メモリ読み取り可能。

Excelで注意すべき点

  • 空セルの型(数値→空→文字列)で列型が不安定になる。必要なら明示的にキャストする。
  • 複数シートやヘッダが不規則なファイルは事前に簡易ルール(ヘッダ行を固定)を設ける。

5) JSONLとLLM向け行指向フォーマットの扱い

LLM用などで行指向(JSONL)を使う場合、各行が独立したJSONであることと文字列のエスケープに注意します。

観点 対策
1行が大きすぎる 行サイズをチェックして分割或いはパート化する。
不正なJSON 書き込み前にjson.dumpsで検証してから出力。
スキーマ検証 軽量にキー存在チェックや型チェックを行う。JSON Schemaは重めなので、最初は簡易チェックから。

6) 圧縮・アーカイブとストリーミング処理

gzip/zip/zstdなど圧縮はI/Oとストレージのトレードオフです。ストリーミング対応の書き方でメモリを保ちます。

  • gzipはpython標準で簡単。gzip.openを使ってバイナリで書く。
  • zipfileは複数ファイルのアーカイブに便利だがランダムアクセスに注意。
  • zstdは高速・高圧縮だが追加ライブラリが必要。

7) Parquet/バイナリフォーマットの導入判断

Parquetは分析向けに優れるが、導入コストと運用面(ライブラリ互換、クラウドとの親和性)を考慮します。

用途 Parquet向き? 理由
繰り返し読み取り・列選択が多い はい 列指向で読み取りが高速、サイズも小さくなる。
単一CSVの単発変換 いいえ 変換コストと運用負荷が割に合わない場合がある。

8) 実践レシピ:Excel→JSONL(分割+gzip)を安全に出力する完全スクリプト

以下は現場で使える最小限のワークフロー例です。ポイントはストリーミング読み、分割(行数ごと)、gzip圧縮、一時ファイル+atomic replace、原本退避です。openpyxlを想定しています。

事前準備: pip install openpyxl

説明: このスクリプトは入力.xlsxの指定シートをiter_rowsで逐次読みし、指定行数ごとにJSONL(各行は1つのJSON)ファイルを作成、gzip圧縮して出力します。出力は一時ファイルに書いてからfinalに置換します。

from pathlib import Path
import json
import gzip
import tempfile
import os
from openpyxl import load_workbook

INPUT = Path('inputs/data.xlsx')
SHEET_NAME = 'Sheet1'
ROWS_PER_FILE = 10000
OUT_DIR = Path('out')
OUT_DIR.mkdir(parents=True, exist_ok=True)
BACKUP_DIR = Path('backup')
BACKUP_DIR.mkdir(parents=True, exist_ok=True)

def iter_rows_from_excel(path, sheet_name):
    wb = load_workbook(path, read_only=True, data_only=True)
    ws = wb[sheet_name]
    it = ws.iter_rows(values_only=True)
    headers = next(it)
    for row in it:
        yield dict(zip(headers, row))
    wb.close()

def atomic_write_gzip(json_lines, out_path: Path):
    # 一時ファイルに書いてから原子置換
    with tempfile.NamedTemporaryFile(dir=out_path.parent, delete=False) as tf:
        tmp_path = Path(tf.name)
    try:
        with gzip.open(tmp_path, 'wt', encoding='utf-8') as gz:
            for obj in json_lines:
                gz.write(json.dumps(obj, ensure_ascii=False) + '\n')
        os.replace(tmp_path, out_path)
    finally:
        if tmp_path.exists():
            try:
                tmp_path.unlink()
            except Exception:
                pass

def excel_to_jsonl_gzip(input_path):
    base = OUT_DIR / input_path.stem
    part = 0
    buffer = []
    for i, obj in enumerate(iter_rows_from_excel(input_path, SHEET_NAME), start=1):
        buffer.append(obj)
        if i % ROWS_PER_FILE == 0:
            part += 1
            out_path = base.with_suffix(f'.part{part}.jsonl.gz')
            atomic_write_gzip(buffer, out_path)
            buffer = []
    if buffer:
        part += 1
        out_path = base.with_suffix(f'.part{part}.jsonl.gz')
        atomic_write_gzip(buffer, out_path)

if __name__ == '__main__':
    # 原本退避
    backup_path = BACKUP_DIR / INPUT.name
    if not backup_path.exists():
        INPUT.replace(backup_path)
        # 作業用に原本を戻す(運用では移動/コピーのルールを選択)
        backup_path.replace(INPUT)
    excel_to_jsonl_gzip(INPUT)

ポイント補足:

  • openpyxlのread_only=Trueとvalues_only=Trueでメモリを節約。
  • json.dumps(…, ensure_ascii=False)で日本語を維持。
  • atomic_write_gzipで一時ファイル→os.replaceの原子性を確保。
  • 分割サイズはROWS_PER_FILEで調整。クラウド転送の上限やLLMの入力制約を踏まえて決める。

9) デバッグ・運用チェックリストとトラブルシューティング

チェック項目 推奨対応
ファイルサイズ メモリに収まらない場合はストリーミング。分割出力を規定する。
エンコーディングの不一致 utf-8-sigやchardetで検出、ログを残し変換ルールを定める。
処理途中で落ちる .inprogress拡張子・ロギング・リトライ設計で再実行可能にする。
圧縮済みをテキストで開く 必ずバイナリモードで扱う。gzip.openやzipfileを使用。
Excelの型変化 明示的にキャスト、必要ならスキーマ変換レイヤを用意。

運用観測ポイント(ログ/メトリクス)

  • 処理した行数、出力ファイル数、失敗行数
  • 処理時間とスループット(行/秒)
  • ストレージ使用量と圧縮比

よくある失敗と対応(簡易まとめ)

失敗例 原因 対応
文字化け エンコーディング誤指定 utf-8-sigを試す・検出して変換
行分割のズレ CRLF/改行の混在 newline=”でcsv処理、改行正規化
中途ファイル残存 原子性未担保の書き込み 一時ファイル→os.replaceパターンを導入
圧縮ファイルをテキストで開く バイナリモードの無視 gzip.open等でバイナリ/テキスト適切に扱う

パフォーマンス/コスト判断ガイド

ざっくりした選び方:

  • 少量データで素早く処理:pandasで楽に実装。
  • 大量データで低メモリ:標準csv/openpyxlのストリーミング。
  • 頻繁に分析・列選択するならParquetを検討。

まとめ(この記事の要点)

実務でのファイル入出力は「小さい工夫」の積み重ねで安定します。主なポイントを再掲します:

  • エンコーディングと改行を明示的に扱う(utf-8-sig、newline=”など)。
  • 書き込みは一時ファイル+atomic replaceで原子性を担保する。
  • 巨大ファイルはストリーミング/分割処理を採用する。
  • JSONLは1行1レコードを守り、書き出し前の検証を行う。
  • 圧縮とバイナリフォーマット導入は目的とコストを比較して判断する。

次回はこの記事の運用面をさらに深め、入出力アーティファクトのメタデータ管理(マニフェスト、チェックサム、保持ポリシー)やS3等外部ストレージとの安全連携を取り上げます。シリーズ「AIとPythonの実務」として、ここで提示したワークフローを基盤にさらに自動化・監視を進めてください。