Pythonにおけるファイル操作

Pythonにおけるファイル操作

この記事ではPythonにおけるファイル操作について説明します。

「安全性」「効率性」「可読性」を念頭に、基本から実践テクニックまで段階的に説明します。

YouTube Video

Pythonにおけるファイル操作

ファイル操作は小さいスクリプトから大規模アプリまで必須の基礎技術です。

ファイルを開く・閉じる

まず、テキストファイルを読み書きする例を見てみましょう。with 文(コンテキストマネージャ)を使うことでファイルの確実なクローズを保証できます。

次のコードは、テキストファイルを開いて中身を読み、行単位で処理します。

1# Open and read a text file safely using a context manager.
2with open("example.txt", "r", encoding="utf-8") as f:
3    # Read all lines lazily (as an iterator) and process each line.
4    for line in f:
5        print(line.rstrip())
  • encoding="utf-8" を明示することでプラットフォーム依存の問題を減らせます。
  • 大きなファイルでも for line in f はメモリ効率が良いです。

書き込み(上書き・追記)

ファイルに書き込む際はモードに注意します。w は上書き、a は追記です。書き込みも with 文を使います。

次のコードは、上書きと追記の基本的な例です。

1# Write (overwrite) to a file.
2with open("output.txt", "w", encoding="utf-8") as f:
3    f.write("First line\n")
4    f.write("Second line\n")
5
6# Append to the same file.
7with open("output.txt", "a", encoding="utf-8") as f:
8    f.write("Appended line\n")
  • このコードは、output.txt にテキストを書き込み、次に同じファイルへ追記する例です。"w" モードで上書きし、"a" モードで既存内容の末尾に新しい行を追加しています。
  • フラッシュが必要なら flush() を呼びますが、多くはコンテキスト終了で自動的にフラッシュされます。
  • 複数プロセスやスレッドが同時に書く可能性がある場合は排他制御(ファイルロックなど)を考慮する必要があります。

バイナリデータの読み書き

画像や圧縮ファイルなどはバイナリモード(rbwb)で扱います。テキストモードとは異なり encoding は無視されます。

次のコードは、バイナリファイルを読み込んで別ファイルにバイナリコピーする例です。

1# Read and write binary files.
2with open("image.png", "rb") as src:
3    data = src.read()
4
5with open("copy.png", "wb") as dst:
6    dst.write(data)
  • 大きなバイナリを扱うときは read()による一括読みは避け、チャンクで読み書きするとメモリ効率が良くなります。

大きなファイルをチャンクで扱う例

メモリに収まらない巨大ファイルは一定サイズずつ読み書きします。I/O バウンドなのでバッファサイズの調整が有用です。

次のコードは、ファイルを 64KB チャンクでコピーする例です。メモリ使用量を抑えつつ高速に動作します。

1# Copy a large file in chunks to avoid using too much memory.
2CHUNK_SIZE = 64 * 1024  # 64 KB
3
4with open("large_source.bin", "rb") as src,      open("large_dest.bin", "wb") as dst:
5    while True:
6        chunk = src.read(CHUNK_SIZE)
7        if not chunk:
8            break
9        dst.write(chunk)
  • ネットワークやディスクの特性に応じてチャンクサイズを調整できます。SSD ではやや大きめが良いことが多いです。

buffering 引数を使った例

open() 関数の buffering 引数を指定すると、Python が内部で使うバッファサイズを制御できます。これにより、入出力の効率をさらに最適化できます。

 1# Explicitly set the internal buffer size to 128 KB for faster I/O.
 2CHUNK_SIZE = 64 * 1024
 3BUFFER_SIZE = 128 * 1024  # 128 KB internal buffer
 4
 5with open("large_source.bin", "rb", buffering=BUFFER_SIZE) as src,      open("large_dest.bin", "wb", buffering=BUFFER_SIZE) as dst:
 6    while True:
 7        chunk = src.read(CHUNK_SIZE)
 8        if not chunk:
 9            break
10        dst.write(chunk)
  • buffering の値を 0 にするとバッファリングなしのI/Oになり、1 は行バッファリング、2 以上の値はバイト単位の指定サイズのバッファを使用します。
  • 一般的には OS が効率的にキャッシュするため、デフォルト値で十分ですが、非常に大きなファイルや特殊なデバイスでは調整が効果的です。

Pathlib を使ったモダンなファイル操作

標準 pathlib はパス操作を直感的にします。文字列パスより可読性・安全性が向上します。

 1from pathlib import Path
 2
 3path = Path("data") / "notes.txt"
 4
 5# Ensure parent directory exists.
 6path.parent.mkdir(parents=True, exist_ok=True)
 7
 8# Write and read using pathlib.
 9path.write_text("Example content\n", encoding="utf-8")
10content = path.read_text(encoding="utf-8")
11print(content)
  • このコードは、pathlib を使ってディレクトリを作成し、テキストファイルに文字列を書き込んでから内容を読み取る例です。Path オブジェクトによりパス操作を直感的かつ安全に行えます。
  • Path には iterdir(), glob() など便利な API があります。OS 間の区切り文字の違いを意識せずに書けます。

一時ファイルと一時ディレクトリ(tempfile)

一時ファイルは tempfile で安全に作成できます。セキュリティ上の競合や名前衝突を避けられます。

次のコードは、一時ファイルを使って一時的なデータを作成する例です。

 1import tempfile
 2
 3# Create a temporary file that is deleted on close.
 4with tempfile.NamedTemporaryFile(
 5    mode="w+",
 6    encoding="utf-8",
 7    delete=True
 8) as tmp:
 9    tmp.write("temporary data\n")
10    tmp.seek(0)
11    print(tmp.read())
12
13# tmp is deleted here
  • このコードは、一時ファイルを作成してデータを書き込み・読み込みし、with ブロック終了時に自動的に削除する例です。tempfile.NamedTemporaryFile を使うことで、安全かつ衝突のない一時ファイルを扱えます。delete=Trueを指定しているため、自動的に削除されます。
  • Windows ではすぐに別ハンドルから開けないケースがあるため、delete=False にして管理する選択肢もあります。

shutil: コピー・移動・削除の高レベル操作

ファイルやディレクトリの再帰的なコピーや移動、削除は shutil を使うと簡単です。

 1import shutil
 2
 3# Copy a file preserving metadata.
 4shutil.copy2("source.txt", "dest.txt")
 5
 6# Move a file or directory.
 7shutil.move("old_location", "new_location")
 8
 9# Remove an entire directory tree (use with caution).
10shutil.rmtree("some_directory")
  • shutil.copy2 はメタデータ(更新時刻等)もコピーします。move は rename が使えずともフォールバックして移動します。
  • rmtree は危険な操作なので削除前に必ず確認やバックアップを行ってください。

ファイルのメタデータ(os.stat)・パーミッション操作

ファイルサイズ、更新時刻、パーミッションの読み取り/変更は os / stat で行います。

次のコードは、os.stat でファイルの基本情報を取得し、os.chmod でパーミッションを変更する例です。

 1import os
 2import stat
 3from datetime import datetime
 4
 5st = os.stat("example.txt")
 6print("size:", st.st_size)
 7print("modified:", datetime.fromtimestamp(st.st_mtime))
 8
 9# Make file read-only for owner.
10os.chmod("example.txt", stat.S_IREAD)
  • パーミッションは POSIX と Windows で挙動が異なります。クロスプラットフォームを意識するなら高レベル API を使うか条件分岐します。

ファイルロック(排他制御) — Unix と Windows の違い

並列プロセスで同一ファイルにアクセスする場合、排他制御が必要です。UNIX 系は fcntl、Windows は msvcrt を使います。

次のコードは、UNIX 系で fcntl.flock を使い、書き込み時に排他ロックを取得する例です。

 1# Unix-style file locking example
 2import fcntl
 3
 4with open("output.txt", "a+", encoding="utf-8") as f:
 5    fcntl.flock(f, fcntl.LOCK_EX)
 6    try:
 7        f.write("Safe write\n")
 8        f.flush()
 9    finally:
10        fcntl.flock(f, fcntl.LOCK_UN)
  • このコードは、UNIX 系システムで fcntl.flock を使い、ファイルへの同時書き込みを防ぐために排他ロックを取得して安全に書き込む例です。処理後は必ずロックを解除して他のプロセスからのアクセスを許可します。
  • Windows では msvcrt.locking() を使います。さらに高レベルに使いたい場合は portalockerfilelock のような外部ライブラリを検討できます。

アトミックなファイル書き込みパターン

更新途中でファイルが壊れるのを防ぐため、書き込みは一時ファイルに行い、成功時にos.replaceで差し替えることができます。

一時ファイルへ書いてから置き換えることでクラッシュ時の破損を避けます。

 1import os
 2from pathlib import Path
 3import tempfile
 4
 5def atomic_write(path: Path, data: str, encoding="utf-8"):
 6    # Write to a temp file in the same directory and atomically replace.
 7    dirpath = path.parent
 8    with tempfile.NamedTemporaryFile(
 9        mode="w",
10        encoding=encoding,
11        dir=str(dirpath),
12        delete=False
13    ) as tmp:
14        tmp.write(data)
15        tmp.flush()
16        tempname = tmp.name
17
18    # Atomic replacement (on most OSes)
19    os.replace(tempname, str(path))
20
21# Usage
22atomic_write(Path("config.json"), '{"key":"value"}\n')

os.replace は同一ファイルシステム内で原子的に置換します。異なるマウント間だと原子性が保証されないことに注意してください。

mmap(メモリマップ)を使った高速アクセス(大規模データ向け)

大きなファイルをランダムアクセスする場合、mmap は I/O 性能を改善します。バイナリ操作が中心です。

次のコードは、ファイルをメモリマップして特定のバイト範囲を読み書きする例です。ファイルのサイズ変更には注意が必要です。

1import mmap
2
3with open("data.bin", "r+b") as f:
4    mm = mmap.mmap(f.fileno(), 0)  # map entire file
5    try:
6        print(mm[:20])  # first 20 bytes
7        mm[0:4] = b"\x00\x01\x02\x03"  # modify bytes
8    finally:
9        mm.close()
  • このコードは、mmap を使ってバイナリファイルをメモリ上にマップし、直接バイト単位で読み書きする例です。メモリマップにより大規模データへの高速なランダムアクセスが可能になります。
  • mmap は効率的ですが、使い方を誤るとデータ一貫性の問題が発生します。必要に応じて同期(flush())を呼ぶ必要があります。

CSV / JSON / Pickle:形式別の読み書き

特定のデータ形式には専用モジュールがあります。CSV は csv、JSON は json、Python オブジェクト保存は pickle を使います。

次のコードは、CSV と JSON の読み書き、及び Pickle の基本的な例です。Pickle は任意コード実行の危険があるため外部ソースの読み込みは避けます。

 1import csv
 2import json
 3import pickle
 4
 5# CSV write
 6with open("rows.csv", "w", encoding="utf-8", newline="") as f:
 7    writer = csv.writer(f)
 8    writer.writerow(["name", "age"])
 9    writer.writerow(["Alice", 30])
10
11# JSON write
12data = {"items": [1, 2, 3]}
13with open("data.json", "w", encoding="utf-8") as f:
14    json.dump(data, f, ensure_ascii=False, indent=2)
15
16# Pickle write (only for trusted environments)
17obj = {"key": "value"}
18with open("obj.pkl", "wb") as f:
19    pickle.dump(obj, f)
  • CSV の newline="" は Windows で余分な空行を防ぐために推奨されます。JSON は ensure_ascii=False で UTF-8 の可読性を保てます。

圧縮ファイルの直接読み書き(gzip / bz2 / zipfile)

gzip や zip を直接扱えばディスクスペースを節約できます。標準ライブラリに対応モジュールがあります。

次のコードは、gzip ファイルをテキストとして読み書きする簡単な例です。

1import gzip
2
3# Write gzipped text.
4with gzip.open("text.gz", "wt", encoding="utf-8") as f:
5    f.write("Compressed content\n")
6
7# Read gzipped text.
8with gzip.open("text.gz", "rt", encoding="utf-8") as f:
9    print(f.read())
  • 圧縮レベルやフォーマットにより圧縮率と速度のトレードオフがあります。

セキュリティと脆弱性対策

セキュリティと脆弱性対策のため、次の点を考慮できます。

  • 信頼できない入力 をファイル名やパスに直に使わないようにします。
  • Pickle は信頼できるソースのみで使用します。
  • 実行権限 を最小化するようにし、ファイルを扱うプロセスには必要最小限の権限だけ与えます。
  • 一時ファイルtempfile を使い、公開ディレクトリに平文で置かないようにします。

ユーザ入力をファイルパスに使う場合は正規化と検証が必要です。たとえば Path.resolve() と親ディレクトリチェックなどを行います。

 1from pathlib import Path
 2
 3def safe_path(base_dir: Path, user_path: str) -> Path:
 4    candidate = (base_dir / user_path).resolve()
 5    if base_dir.resolve() not in candidate.parents and base_dir.resolve() != candidate:
 6        raise ValueError("Invalid path")
 7    return candidate
 8
 9# Usage
10base = Path("/srv/app/data")
11
12# Raises an exception: attempted path traversal outside `base`
13safe = safe_path(base, '../etc/passwd')
  • ウェブアプリや公開 API などでファイルパスを外部入力に使う場合は特に注意が必要です。

よくあるパターンまとめ

  • 常に with 文を使う(自動クローズ)。
  • 文字データは encoding を明示する。
  • 大きなファイルはチャンクで読み書き。
  • 共有リソースにはファイルロックを導入。
  • 重要な更新は「一時ファイル書き込み → os.replace」 の原子パターン。
  • 危険な操作(削除、上書き)は事前確認/バックアップを必ず。
  • 外部入力をファイルパスに使うときは正規化と検証を行う。

まとめ

ファイル操作では、with 文の利用やエンコーディングの明示、アトミックな書き込みなど、安全で再現性の高い手法を取ることが重要です。大規模処理や並列アクセスではロックやログ管理を導入し、データ破損や競合を防ぐ設計も必要になります。効率性と安全性の両立が、信頼できるファイル操作の鍵です。

YouTubeチャンネルでは、Visual Studio Codeを用いて上記の記事を見ながら確認できます。 ぜひYouTubeチャンネルもご覧ください。

YouTube Video