この記事について
この記事は、生成AIを活用した自動生成フローで作成しています。Python公式資料のcsv、tempfile、os.replaceを参照し、途中エラーの際に元CSVを残すコードを作りました。Python 3.13.5でサンプル実行、構文チェック、3件の単体テストを実施しています。
検証ステータス:🧪 Python 3.13.5でローカル実行・単体テスト3件成功
WordPressや業務システムへの組込み動作、Windows環境、突然の電源断については未検証です。
PythonでCSVを更新する際に、既存ファイルへ直接書き込んでしまうと、途中で例外が起きたときに中途半端なファイルが残る可能性があります。
新しい内容を一時ファイルへ最後まで書き込んでから置き換えると、少なくとも通常の書込み例外によって元ファイルを途中まで上書きしてしまう事態を避けやすくなります。
ここでは、標準ライブラリだけを使って、正常終了時には新CSVへ置換し、検証エラー時には元CSVを保持するプログラムを試します。保存先は毎回作成する学習用の一時ディレクトリなので、業務データには触れません。
直接書き込む方法と何が違う?
次のように既存のCSVを開くと、ファイルの内容は書込み開始時に切り詰められます。
# 注意: 既存のファイルを直接書き換える例
with open("batch.csv", "w", encoding="utf-8", newline="") as fp:
fp.write("id,status\n")
# ここで処理が停止すると、元データは戻りません。
「書込みに成功したときだけ新しい内容へ切り替える」なら、先に別ファイルを作る方法が有効です。
| 処理 | 直接上書き | 一時ファイルを使う方法 |
|---|---|---|
| 書込み開始 | 元ファイルへ書込む | 一時ファイルへ書込む |
| 途中で検証エラー | 元ファイルが途中まで変わり得る | 置換前なら元ファイルを保持できる |
| 完了時 | そのまま終了 | os.replaceで置換 |
| 注意点 | 部分書込み | 同時更新や電源断への追加対策は別途必要 |
os.replaceの成功時にファイル名を置換する操作は、POSIX環境ではアトミック操作として定義されています。ただし、ファイル名の置換がアトミックであることと、突然の電源断の後にも必ず新データが残ることは別の話です。公式資料はPython os.replaceです。
動作確認したサンプル
Python 3.10以降を対象としたコードです。標準ライブラリのcsv、os、tempfile、pathlibを使います。
from __future__ import annotations
import csv
import os
import tempfile
from pathlib import Path
from collections.abc import Iterable, Mapping
COLUMNS = ("id", "status")
def replace_csv(target: Path, rows: Iterable[Mapping[str, str]]) -> None:
"""CSVを書き終えるまで元ファイルを変更しない。"""
target = Path(target)
temporary: Path | None = None
try:
# 置換先と同じフォルダーに一時ファイルを作成する
with tempfile.NamedTemporaryFile(
mode="w", encoding="utf-8", newline="", delete=False,
dir=target.parent, prefix=f".{target.name}.", suffix=".tmp"
) as fp:
temporary = Path(fp.name)
writer = csv.DictWriter(fp, fieldnames=COLUMNS, extrasaction="raise")
writer.writeheader()
for row in rows:
# 入力列の過不足も検出し、失敗したら置換しない
if set(row) != set(COLUMNS):
raise ValueError("CSV row must have exactly id and status")
writer.writerow(row)
# Pythonのバッファを出し、OSにファイル内容の同期を依頼
fp.flush()
os.fsync(fp.fileno())
# 全件を書き終えてから、元CSVの名前へ置き換える
os.replace(temporary, target)
temporary = None
finally:
# 置換前に失敗したら、作った一時ファイルだけを削除
if temporary is not None:
temporary.unlink(missing_ok=True)
if __name__ == "__main__":
# 毎回新しいディレクトリにサンプルCSVを作る
with tempfile.TemporaryDirectory(prefix="csv-atomic-demo-") as folder:
csv_path = Path(folder) / "batch.csv"
csv_path.write_text("id,status\nold,OLD\n", encoding="utf-8")
replace_csv(csv_path, (
{"id": "A-001", "status": "DONE"},
{"id": "A-002", "status": "PENDING"},
))
with csv_path.open(newline="", encoding="utf-8") as fp:
entries = list(csv.DictReader(fp))
print(f"[RESULT] rows={len(entries)} ids={[x['id'] for x in entries]}")
GitHubに置いた実行ファイルも同じ内容です。
サンプルのフォルダーで、ターミナルから次を実行できます。
python3 atomic_csv.py python3 -m unittest -v test_atomic_csv.py
実際に確認した出力
Python 3.13.5のテスト環境では、次の結果が出ました。
[RESULT] rows=2 ids=['A-001', 'A-002'] Ran 3 tests in 0.002s OK
3つのテストは、初回作成と既存ファイル置換、途中エラー時の元ファイル維持、値に改行が含まれる場合のCSV読込を対象にしています。テストの処理時間は環境によって変わります。
書込み処理の順序
一時ファイルを同じディレクトリに作成する
全レコードを書き込む
途中の列不一致をエラーとして検出する
flushとfsyncを実施する
一時ファイルを閉じる
os.replaceで置換する
エラー時は置換前の一時ファイルを後片付けする
同じディレクトリに作る理由
NamedTemporaryFileのdirにtarget.parentを指定しています。
with tempfile.NamedTemporaryFile(
mode="w",
encoding="utf-8",
newline="",
delete=False,
dir=target.parent
) as fp:
...
一時ファイルをシステムの既定tempディレクトリに作ると、保存先とは別のファイルシステムになる可能性があります。os.replaceは異なるファイルシステム間では失敗することがあるため、同じディレクトリに作ります。
delete=Falseを指定した一時ファイルは、プログラムが閉じただけでは削除されません。そこで例外が起きたときにfinallyで削除する処理も必要です。仕様はPython tempfileで確認できます。
newlineを空文字にする理由
CSVでは、セルの中に改行が含まれる場合があります。Pythonのcsvモジュールは、そのような値を引用符で囲むなど、CSVの改行を処理します。
with csv_path.open(encoding="utf-8", newline="") as fp:
rows = list(csv.DictReader(fp))
Python公式のcsvドキュメントでも、ファイルを扱う際にnewline=””を指定する例が示されています。OSの改行変換とCSV側の処理を重複させないためです。
わざと失敗させても元CSVは残る?
次のテストでは、すでにあるCSVへ置き換えようとしますが、2件目の列が不足しています。
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as folder:
path = Path(folder) / "batch.csv"
path.write_text("id,status\nOLD,OK\n", encoding="utf-8")
try:
replace_csv(path, (
{"id": "A", "status": "DONE"},
{"id": "MISSING"},
))
except ValueError as error:
print(f"[EXPECTED_ERROR] {error}")
print(path.read_text(encoding="utf-8"))
期待結果は次のとおりです。
[EXPECTED_ERROR] CSV row must have exactly id and status id,status OLD,OK
これを実際の単体テストでも確認しています。置換が終わる前に例外が発生するため、元CSVの内容は変わりません。書込み途中の一時ファイルはfinallyで削除されます。
ただし、失敗したレコードだけを除いて処理を続ける実装ではありません。異常な入力があったらCSV全体の置換を中止するという設計です。
実際の運用で気を付けること
| よくある状況 | 対応の考え方 |
|---|---|
| 別のプログラムも書き込む | ファイルロック等を追加し、競合を制御する |
| WindowsでCSVがExcelに開かれている | 置換が拒否される可能性を考え、エラー処理と再実行手順を作る |
| ディスク容量が不足 | 書込みエラーとして停止し、元データを保全する |
| 電源が突然切れる | fsyncやディレクトリ同期、ストレージの耐障害性を別途検討する |
| 既存CSVに重要データがある | バックアップと更新内容の検証を先に行う |
| 大きなCSVを扱う | 一時ファイル分のディスク容量を確保する |
この方法は単一の書込み担当が、一つのCSVファイルを安全側で更新する用途に向きます。データベースのトランザクション、同時実行制御、停電後の復元までを保証するものではありません。
本番へ導入する場合は、同じディレクトリの権限、ファイル所有者、既存ファイルのモード、バックアップ、ロールバックを含めて設計してください。os.replaceを使うと新しい一時ファイルが置換後のファイルになるため、元ファイルのアクセス権などがそのまま引き継がれると仮定しないことも重要です。
