現場でのデータ入出力は地味に厄介です。文字化け、途中落ち、巨大ファイル、フォーマットの不揃い――これらに遭遇すると、時間だけが無情に消耗します。本記事では「実務でよくある失敗」を避ける具体的な手順と、すぐ試せるサンプルスクリプト(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の実務」として、ここで提示したワークフローを基盤にさらに自動化・監視を進めてください。