0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python文法講義~標準ライブラリ:ファイル操作~

Posted at

まえがき

今回は、os, io, sys, pathlob, shutil, pickle, csv, jsonについて扱う。これらは頻出にもかかわらず、テキストによっては扱われたり扱われなかったりであることが多いので、ここでまとめて紹介する。
標準モジュールは、調べながら知識整理∧実装の際の参照元としての記録も兼ねるため、テキストや公式ドキュメントの記述に頼る。

そもそもどのライブラリを用いるべきか

ファイル操作系のライブラリはいくつかあるが、具体的にどれをどの状況で用いるべきかをまとめる。

各ライブラリの状況は以下。

ライブラリ 主用途 強み 弱み/注意 代表API
os OS機能全般(CWD、環境変数、権限、プロセス、低レベルFD) 広範・低レベル。互換性高い パス操作が文字列中心。可読性低くなりがち getcwd, chdir, environ, chmod, walk, scandir
os.path 文字列パスの操作 依存が少なく軽い パスは単なる文字列。バグを生みやすい join, split, basename, dirname, exists, isfile
pathlib 高水準パス(オブジェクト指向) 直感的・安全・表現力。glob内包 学習コスト少し。超低レベル操作は別途 Path, .read_text(), .iterdir(), .glob(), .rglob()
shutil 高水準ファイル操作(コピー/移動/削除/圧縮/アーカイブ) ひとことで完結、メタ情報維持も可 細かい制御はos併用 copy, copy2, move, rmtree, make_archive
glob パターン展開(ワイルドカード) 簡潔・高速 高度なフィルタは別途 glob, iglob, escapePath.glob推奨)

タスク別の状況は以下。

やりたいこと 推奨 補足
パス結合・拡張子取得・親子参照 pathlib Path(base) / "sub" / "file.txt", .suffix, .parent
再帰的にファイル列挙 pathlib.rglob() / os.walk 属性も欲しい・大量なら os.scandir or walk
ファイルのコピー/移動/削除 shutil コピーなら copy2(メタ保持)、大量削除は rmtree
単純なワイルドカード列挙 Path.glob / glob.glob Path.glob("*.csv") が自然
環境変数、一時的にCWD変更 os.environ, os.chdir(3.11+ は contextlib.chdir with文で一時変更が安全
権限・タイムスタンプ os.chmod, os.utime Windowsは挙動制限あり
低レベルFD(性能/特殊用途) os.open/read/write/close 原則は高水準open()を使う

os モジュール

os モジュールによって、OS依存の機能(ファイル/ディレクトリ操作、環境変数、プロセス情報など)へのインターフェースを持てる。また以下とは補完関係になる。
os.path:文字列パスの操作(Pathlib登場以前の定番)
pathlib:高水準パスAPI(推奨)。ただし既存コードや一部APIで os は依然重要
shutil:物理コピー/移動/削除の高水準ユーティリティ
subprocess:外部コマンド実行(os.system は基本的に非推奨)

よく使うAPI早見表

分類 主な関数/属性 用途
CWD getcwd, chdir カレントワーキングディレクトリ取得/変更
環境変数 environ, getenv, putenv* 環境変数の取得・設定(*は原則os.environ操作推奨)
パス os.path.join/split/ext/basename/dirname, abspath, relpath, exists, isfile, isdir, getsize 文字列パス処理
FS操作 listdir, scandir, mkdir, makedirs, rmdir, remove, rename, replace ファイル・ディレクトリ操作
走査 walk 再帰的ディレクトリ走査
メタ情報 stat, lstat, utime サイズ・時刻・パーミッション
権限 chmod, umask, chown* 権限変更(*Windowsでは無効 or 制限あり)
リンク link, symlink, readlink ハード/シンボリックリンク
システム uname, cpu_count, getpid, getppid, getlogin システム/プロセス情報
乱数 urandom 暗号学的に強い乱数
改行/セパレータ sep, pathsep, linesep OS固有の区切り文字
実行 system 外部コマンド(基本はsubprocess.run推奨

基本:作業ディレクトリ(CWD)

import os

print(os.getcwd())              # => 現在の作業ディレクトリ(例: 'C:\\Users\\you')
os.chdir("..")                  # 1つ上へ
print(os.getcwd())              # => 変更後のディレクトリ

# 一時的な chdir は Python 3.11+ の contextlib.chdir が便利
from contextlib import chdir
with chdir("/"):                # Windowsなら "C:\\"
    print(os.getcwd())          # => '/'
# ブロックを出ると元に戻る

ファイル・ディレクトリ操作(作成/削除/一覧/リネーム)

import os

# 一覧:直下のみ
print(os.listdir("."))          # => ['a.txt', 'data', ...]

# 高速・詳細:scandir(DirEntryで型/サイズ取得が軽い)
with os.scandir(".") as it:
    names = [e.name for e in it if e.is_file()]
print(names)                    # => ['a.txt', 'b.csv', ...]

# ディレクトリ作成
os.mkdir("work")                # work が無ければ作成
os.makedirs("nested/dir", exist_ok=True)  # 階層的に作成(既存ならOK)

# 削除(空ディレクトリのみ)
os.rmdir("work")                # 中身があると OSError
# 中身ごと削除なら shutil.rmtree を利用

# ファイル削除・リネーム
open("x.txt", "w").close()
os.rename("x.txt", "y.txt")     # 同一ボリューム内の改名
os.replace("y.txt", "z.txt")    # 置換セマンティクス(上書き)
os.remove("z.txt")              # ファイル削除

補足:物理コピー/移動/削除は shutil.copy2, shutil.move, shutil.rmtree が高水準・安全。

環境変数

import os

# 取得
print(os.environ.get("PATH")[:60])    # => 'C:\\Windows\\System32;...'
print(os.getenv("PYTHONPATH"))        # => None or 値

# 設定(現在のプロセスと子プロセスに影響)
os.environ["MODE"] = "dev"
print(os.environ["MODE"])             # => 'dev'

# 削除
os.environ.pop("MODE", None)
print(os.environ.get("MODE"))         # => None

実務では一時変更は contextlib でラップして元に戻せる設計が安全(with文の解説参照)。

プロセス/システム情報・外部コマンド

import os

print(os.getpid(), os.getppid())      # => 自プロセスID・親プロセスID
try:
    print(os.getlogin())              # => ログイン名(環境によっては失敗)
except OSError:
    print("getlogin not available")

print(os.cpu_count())                 # => 論理CPU数(例: 16)

# OS情報(Windowsでは platform モジュール併用が実務的)
if hasattr(os, "uname"):              # Windowsでは通常未定義
    print(os.uname())                 # => posix系

外部コマンド実行は os.system("...") ではなく、subprocess.run([...], check=True) 推奨。戻り値/標準出力/エラー取り回しが堅牢。

メタ情報(サイズ/時刻/属性)と権限

import os, time, stat

# stat情報
st = os.stat(__file__)                # このスクリプト自身
print(st.st_size)                     # => サイズ(bytes)
print(time.ctime(st.st_mtime))        # => 最終更新時刻(ローカル)

# タイムスタンプ変更
os.utime(__file__, (st.st_atime, st.st_mtime))  # そのまま上書き(例)

# 権限(posix中心の概念。Windowsは限定的)
# 読取専用にする例(posix)
try:
    os.chmod("data.txt", stat.S_IREAD)         # Windows でも最小限は効くことがある
except FileNotFoundError:
    pass

# umask(新規作成時のデフォ権限マスク)
old = os.umask(0o022)
os.umask(old)                          # 元に戻す

Windows注意:chmod / chown は制限されることが多い。ACLは icacls や外部ライブラリを検討。

os.path(文字列パス処理)

import os

p = r"C:\data\project\report.xlsx"
print(os.path.basename(p))       # => 'report.xlsx'
print(os.path.dirname(p))        # => 'C:\\data\\project'
print(os.path.split(p))          # => ('C:\\data\\project', 'report.xlsx')
print(os.path.splitext(p))       # => ('C:\\data\\project\\report', '.xlsx')

# 正規化・絶対化・相対化
print(os.path.normpath("C:/data//project/../report.xlsx"))
# => 'C:\\data\\report.xlsx'(Windowsの例)
print(os.path.abspath("."))      # => CWDの絶対パス
print(os.path.relpath(r"C:\data\report.xlsx", r"C:\data"))
# => 'report.xlsx'

# 結合(セパレータは OS 依存で自動)
print(os.path.join("C:\\data", "sub", "file.txt"))
# => 'C:\\data\\sub\\file.txt'

# 存在/種別
print(os.path.exists("README.md"))  # => True/False
print(os.path.isfile("README.md"))  # => True/False
print(os.path.isdir("C:\\Windows")) # => True/False

# サイズ
# (os.path.getsize は内部で os.stat を呼ぶ)
# print(os.path.getsize("README.md"))  # => bytes

Pathlib推奨:新規実装は from pathlib import Path を使うと直感的・安全(Path.read_text() など)。既存コードや一部APIでは os.path がまだ現役。

ディレクトリ走査:os.walk

import os

# ルート配下を再帰走査(topdown=True: 親→子、False: 子→親)
for dirpath, dirnames, filenames in os.walk(".", topdown=True):
    # 例:不要ディレクトリを除外(topdown時のみ有効)
    if ".git" in dirnames:
        dirnames.remove(".git")
    if filenames:
        print(dirpath, "=>", len(filenames))   # => ./src => 12 など
        break

walk は大量ファイルでもイテレータで逐次返す。
・ファイル属性が必要なら scandir のほうが速い( DirEntry.stat() は遅延取得)。

リンク関連:ハードリンク/シンボリックリンク

import os

# ハードリンク(同一ボリューム上のみ)
# os.link("src.txt", "dst_hardlink.txt")

# シンボリックリンク(Windowsは管理者権限/開発者モード等が必要)
# os.symlink("src.txt", "dst_symlink.txt")

# シンボリックリンク先の取得
# print(os.readlink("dst_symlink.txt"))   # => 'src.txt'

Windowsでの symlink は要件が厳しめ。代替としてジャンクションやショートカットは別物。

低レベルファイルディスクリプタ(FD)

ほとんどの高水準I/Oは使わずに済むが、パフォーマンス/特殊用途で使うことがある。

import os

fd = os.open("raw.bin", os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)
try:
    n = os.write(fd, b"\x00\x01\x02")   # => 書き込んだバイト数(例: 3)
    # os.read(fd, size) は O_RDONLY で開く
finally:
    os.close(fd)

原則は open()(組み込みの高水準)を用いる。FD操作はエラーハンドリングが難しく上級者向け。

OS依存の定数・区切り文字

import os
print(os.sep)       # => '\\' (Windows) or '/' (POSIX)
print(os.pathsep)   # => ';' (Windows PATH区切り) or ':' (POSIX)
print(os.linesep)   # => '\r\n' (Windows) or '\n' (POSIX)

セキュリティ/堅牢性の注意

・外部コマンド実行は os.system ではなく subprocess.run([...], check=True)。シェルインジェクション対策。
・パス結合は os.path.join/Path を使い、手書きの '\\' '/' を避ける。
・存在チェックと使用のTOCTOU(Time-of-check to time-of-use)に注意。exists 直後に削除され得る。例外処理で守る。
・Unicodeパス:Windowsでは特に長いパス(\?\プレフィクス)や全角文字の扱いに注意。pathlibが扱いやすい。
・権限:chmod はOSで挙動が異なる。Windows権限は別の概念(ACL)。

実務テンプレート

拡張子 .xlsx を深さ制限つきで集め、サイズ順にCSVへ

import os, csv

ROOT = "."
DEPTH_MAX = 3
rows = []

def depth_from_root(root: str, path: str) -> int:
    rel = os.path.relpath(path, root)
    return 0 if rel == "." else len(rel.split(os.sep))

for dirpath, dirnames, filenames in os.walk(ROOT):
    if depth_from_root(ROOT, dirpath) > DEPTH_MAX:
        dirnames[:] = []                   # ここより深く潜らない
        continue
    for name in filenames:
        if name.lower().endswith(".xlsx"):
            full = os.path.join(dirpath, name)
            st = os.stat(full)
            rows.append([full, st.st_size])

rows.sort(key=lambda r: r[1], reverse=True)
with open("xlsx_list.csv", "w", newline="", encoding="utf-8-sig") as f:
    w = csv.writer(f)
    w.writerow(["path", "size"])
    w.writerows(rows)

print(len(rows))                            # => 収集件数

環境変数を一時的に上書き

import os
from contextlib import contextmanager

@contextmanager
def setenv(key, value):
    old = os.environ.get(key)
    try:
        os.environ[key] = value
        yield
    finally:
        if old is None:
            os.environ.pop(key, None)
        else:
            os.environ[key] = old

print(os.environ.get("MODE"))              # => None など
with setenv("MODE", "test"):
    print(os.environ["MODE"])              # => 'test'
print(os.environ.get("MODE"))              # => None

scandir で拡張属性を併用した軽量集計

import os

total = 0
with os.scandir(".") as it:
    for e in it:
        if e.is_file():                    # os.DirEntry は is_file/is_dir が高速
            total += e.stat().st_size
print(total)                               # => 直下ファイルの合計サイズ

pathlib

pathlib:Pythonの pathlib はパスを文字列ではなく“オブジェクト”として扱うための標準ライブラリ

基本概念:Path オブジェクト

Path はファイル/ディレクトリへの道筋を表すオブジェクト。
・OSに応じて WindowsPathPosixPath の具象クラスになります(自動で選ばれる)。
・主要サブクラス:
PurePath 系:純粋パス操作のみ(ファイルシステムにアクセスしない)
Path 系:実ファイルシステム操作ができる(存在確認・作成・削除など)

from pathlib import Path

p = Path("data") / "input" / "sample.txt"
print(p)                    # -> data/input/sample.txt(OSに応じて区切りが変わる)
print(type(p))              # -> <class 'pathlib.PosixPath'> など
print(p.parent)             # -> data/input
print(p.name)               # -> sample.txt
print(p.stem, p.suffix)     # -> sample .txt

よく用いるプロパティ/メソッド

区分 メンバ 役割
構成 p.parent / p.parents 親ディレクトリ(ひとつ/複数段)
p.name, p.stem, p.suffix, p.suffixes ファイル名、拡張子(複数も可)
結合 p / "child" パス結合(演算子オーバーロード)
文字列化 str(p) / p.as_posix() 文字列に変換(POSIX表記へも可)
検査 p.exists() / p.is_file() / p.is_dir() 存在・種別チェック
走査 p.iterdir() / p.glob("*.csv") / p.rglob("*.csv") 直下・再帰の列挙
参照 p.read_text() / p.read_bytes() テキスト/バイト読み込み
書込 p.write_text(), p.write_bytes() テキスト/バイト書き込み
I/O p.open() withでのファイルオープン
変換 p.resolve() / p.absolute() 実体パス解決/絶対パス化
生成 p.mkdir(parents=True, exist_ok=True) ディレクトリ作成
削除 p.unlink() / p.rmdir() ファイル/空ディレクトリ削除
操作 p.rename(dst) / p.replace(dst) リネーム/置換(上書き寄り)
情報 p.stat() サイズ/時刻/モードなどのstat情報

パス結合と分解

ここで扱う内容は、可読性において非常に重要。

from pathlib import Path

root = Path("project")
p = root / "data" / "raw" / "2025" / "samples.csv"
print(p)                 # -> project/data/raw/2025/samples.csv

print(p.parent)          # -> project/data/raw/2025
print(p.parents[1])      # -> project/data/raw
print(p.name)            # -> samples.csv
print(p.stem)            # -> samples
print(p.suffix)          # -> .csv
print(p.suffixes)        # -> ['.csv']  # 例: .tar.gzなら['.tar', '.gz']

ファイル/ディレクトリ操作(作る・消す・移す)

from pathlib import Path

work = Path("work")
work.mkdir(parents=True, exist_ok=True)   # ディレクトリ作成(多段もOK)
# 実行結果: ディレクトリworkが無ければ作成される

f = work / "hello.txt"
f.write_text("こんにちは\n", encoding="utf-8")
# 実行結果: work/hello.txt が作成され、"こんにちは\n"が書き込まれる

print(f.exists())        # -> True
print(f.is_file())       # -> True
print(work.is_dir())     # -> True

f2 = work / "hello_renamed.txt"
f.rename(f2)             # リネーム
# 実行結果: work/hello.txt -> work/hello_renamed.txt に変更

f2.unlink()              # ファイル削除
# 実行結果: work/hello_renamed.txt が削除される

# 空ディレクトリを削除
# work.rmdir()           # 中身があると例外。中身ごと消すならshutil.rmtreeを使用

補足:中身のあるディレクトリを消す場合は標準 shutil.rmtree(work) を併用。

読み書き(read_text / write_text / open + with

from pathlib import Path

p = Path("example.txt")
p.write_text("1行目\n2行目\n", encoding="utf-8")
# 実行結果: example.txtに2行書かれる

txt = p.read_text(encoding="utf-8")
print(txt)               # -> "1行目\n2行目\n"

# with + open で制御を細かく(バイナリや追記など)
with p.open(mode="a", encoding="utf-8") as fp:
    fp.write("3行目\n")
# 実行結果: example.txt の末尾に"3行目\n"が追記される

print(p.read_text(encoding="utf-8"))
# -> "1行目\n2行目\n3行目\n"

走査と検索(iterdir / glob / rglob

from pathlib import Path

root = Path("data")
root.mkdir(exist_ok=True)

# 直下の子を列挙
for child in root.iterdir():
    print(child)
    # 実行結果例:
    # data/a.csv
    # data/b.txt
    # data/subdir

# パターン一致(直下)
for csv in root.glob("*.csv"):
    print(csv)
    # 実行結果例: data/a.csv

# 再帰的に検索
for csv in root.rglob("**/*.csv"):   # "**/*.csv" でもOK
    print(csv)
    # 実行結果例: data/subdir/c.csv など

Windowsの既定では glob は大文字小文字を区別しないが、POSIXでは区別することが多い点に注意。

パスの正規化と実体解決(resolve, absolute

from pathlib import Path

p = Path("./data/../data/raw/./file.txt")
print(p)                     # -> data/../data/raw/file.txt(表記はそのまま)

print(p.resolve())           # -> /abs/path/to/data/raw/file.txt(実体に解決)
# p.resolve(strict=True)     # 存在しないと例外

print(p.absolute())          # -> /abs/path/to/data/../data/raw/file.txt(表記上の絶対パス)

resolve():シンボリックリンクも含め実体に解決
absolute():単純に絶対パスへ変換(リンク解決はしない)

ファイル情報(stat)と時刻/サイズ

from pathlib import Path
import datetime as dt

p = Path("example.txt")
st = p.stat()
print(st.st_size)                 # -> バイトサイズ(例: 18)
print(dt.datetime.fromtimestamp(st.st_mtime))  # -> 最終更新時刻

os/os.pathpathlib の使い分け

観点 pathlib推し os/os.path推し
可読性 p = Path(...) ; p / "child"は直感的 os.path.join(a, b)は冗長
型安全 パスはPath型で分かりやすい 文字列ベースで型が曖昧
互換性 標準。多くのAPIはPathos.PathLikeを受け付ける 古いAPIでは文字列しか受けない場合がある
低レベル機能 os.chmod, os.symlink 等の細かい機能は依然有用
既存コード 新規/モダン レガシー互換や既存大量資産がある時

実務指針:新規は pathlib を第一選択、足りない所を os / shutil で補完。

例外処理と堅牢化の例

from pathlib import Path

def safe_read(path: Path, encoding="utf-8") -> str:
    try:
        return path.read_text(encoding=encoding)
    except FileNotFoundError:
        # ログ出力などを行い、用途によってはNone返しや再送出を選択
        raise
    except UnicodeDecodeError:
        # フォールバックエンコーディング例
        return path.read_text(encoding="cp932")

# 使い方
p = Path("maybe.txt")
try:
    content = safe_read(p)
    print(content)  # 実行結果: ファイルがあれば内容表示
except FileNotFoundError:
    print("ファイルが見つかりませんでした")  # 実行結果: 例外時のハンドリング

実務レシピ

拡張子別の最新ファイルを探す

from pathlib import Path

def latest_by_suffix(root: Path, suffix: str) -> Path | None:
    # 再帰走査してsuffix一致の中から最終更新が最大のもの
    candidates = list(root.rglob(f"*{suffix}"))
    # 実行結果: candidatesに該当ファイルがリストで入る
    if not candidates:
        return None
    return max(candidates, key=lambda p: p.stat().st_mtime)

root = Path("logs")
latest = latest_by_suffix(root, ".log")
if latest:
    print(latest)  # 実行結果例: logs/2025-09-30.log

ディレクトリツリーをCSV化(パス/サイズ/更新時刻)

from pathlib import Path
import csv
import datetime as dt

def dump_tree_to_csv(root: Path, out_csv: Path) -> None:
    rows = []
    for p in root.rglob("*"):
        if p.is_file():
            st = p.stat()
            rows.append({
                "path": str(p),
                "size": st.st_size,
                "mtime": dt.datetime.fromtimestamp(st.st_mtime).isoformat(timespec="seconds"),
                "suffix": "".join(p.suffixes),
            })
            # 実行結果: rowsに1ファイル1行の辞書が蓄積される

    out_csv.parent.mkdir(parents=True, exist_ok=True)
    with out_csv.open("w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=["path", "size", "mtime", "suffix"])
        writer.writeheader()
        writer.writerows(rows)
    # 実行結果: out_csvにCSVが出力される

dump_tree_to_csv(Path("project"), Path("out/files.csv"))

よく用いるユニット

# 1) 一括置換リネーム(.txt -> .log)
from pathlib import Path
for p in Path("logs").glob("*.txt"):
    p.rename(p.with_suffix(".log"))
# 実行結果: 拡張子が.txtのファイルが.logにリネームされる

# 2) 拡張子を二重に(.tar.gz)
p = Path("archive") / "data"
p2 = p.with_suffix(".tar.gz")
print(p2)   # -> archive/data.tar.gz

# 3) 相対→絶対→実体
p = Path("../shared/link")
print(p.absolute())    # 文字面の絶対パス
print(p.resolve())     # 実体に解決(リンク追跡)

# 4) フィルタ付き走査(サイズ>1MBのCSVだけ)
from pathlib import Path
big_csv = [
    f for f in Path("data").rglob("*.csv")
    if f.stat().st_size > 1_000_000
]
print(len(big_csv))    # 実行結果: 条件に合うCSVの件数

Windows/Unixの違いと注意点

・区切り:Path はOSに合わせて \(Windows)/ /(Unix)を内部で扱う。自分で区切り記号を文字列結合しない。
・ドライブ/ルート:Path("C:/work") のようにWindowsドライブも扱える。
・大文字小文字:ファイルシステムによっては大文字小文字非区別(Windows既定)。glob の振る舞いにも影響。

他ライブラリとの連携

多くのライブラリは Path をそのまま受け付ける。受け付けない場合でも str(p) でOK。

よくある落とし穴と回避策

・文字列結合してしまう
→ 悪例:root + "/" + name → 良例:Path(root) / name
・存在前提で stat() / open() する
p.exists()try-except で防御
・相対パスのまま比較
p.resolve() で正規化して比較
・エンコーディング未指定
read_text/write_text は、明示的に encoding を指定

shutil ライブラリ

shutil:ファイル/ディレクトリ操作の高水準ユーティリティをまとめた標準ライブラリ
os が低レベルAPI、pathlib がパス表現、shutil は「コピー・移動・削除・圧縮などを手早く安全に」行う道具、という住み分け

基本:ファイルコピー(メタデータ保持の違い)

from pathlib import Path
import shutil

src = Path("data") / "report.csv"
dst_dir = Path("backup")

dst_dir.mkdir(parents=True, exist_ok=True)  # 事前に作成
# 実行結果: backup/ が無ければ作成される

shutil.copy(src, dst_dir)    # 内容 + 権限
# 実行結果: backup/report.csv が作られる

shutil.copy2(src, dst_dir)   # copy に加え stat(更新時刻など) も保持
# 実行結果: 既存の backup/report.csv を更新(タイムスタンプ保持)

履歴や同一性を重視するバックアップは copy2、ただ移せればいいなら copy

大容量ファイルのストリーミングコピー

from pathlib import Path
import shutil

src = Path("big") / "movie.mp4"
dst = Path("mirror") / "movie.mp4"
dst.parent.mkdir(parents=True, exist_ok=True)

with src.open("rb") as fsrc, dst.open("wb") as fdst:
    shutil.copyfileobj(fsrc, fdst, length=1024*1024)  # 1MB チャンク
# 実行結果: movie.mp4 がチャンク転送でコピーされる(メモリ抑制)

ディレクトリごとにコピー/削除(ignoreパターン活用)

from pathlib import Path
import shutil

src = Path("project")
dst = Path("project_copy")

# 一部のファイル/フォルダを無視
ignore = shutil.ignore_patterns("*.tmp", "*.log", "__pycache__", ".DS_Store")

shutil.copytree(src, dst, dirs_exist_ok=False, ignore=ignore)
# 実行結果: project_copy/ が新規に作られ、*.tmpやログは除外される
from pathlib import Path
import shutil

target = Path("project_copy")
# 危険: これでツリーごと削除。バックアップ/確認の上で。
shutil.rmtree(target)
# 実行結果: project_copy/ 以下が全て削除される(取り消し不能)

安全策:rmtree 前に 対象件数を数える・ユーザー確認を挟む・ゴミ箱送信(send2trash等)も検討。

移動:ボリューム跨ぎを意識

from pathlib import Path
import shutil

src = Path("staging") / "dataset.parquet"
dst = Path("data") / "dataset.parquet"
dst.parent.mkdir(exist_ok=True)

shutil.move(src, dst)
# 実行結果: 同一ドライブなら rename に等しい高速移動。
#          異なるドライブ間は copy → src削除 になるので、容量と時間に注意。

アーカイブ作成・展開(zip/tar/gztar など)

from pathlib import Path
import shutil

root = Path("results")
archive_base = Path("archives") / "exp_2025-09-30"
archive_base.parent.mkdir(exist_ok=True)

# zipを作る(他に 'gztar' 'tar' 'bztar' 'xztar' など)
archive_path = shutil.make_archive(
    base_name=str(archive_base),  # 拡張子は自動で付く
    format="zip",
    root_dir=str(root),           # ここを基準に
    base_dir="."                  # root配下の全体を固める
)
# 実行結果: archives/exp_2025-09-30.zip が作成される

# 展開
extract_to = Path("extracted")
shutil.unpack_archive(archive_path, extract_dir=str(extract_to))
# 実行結果: zip内容が extracted/ に展開される

ディスク容量・which(実務の前提チェック)

import shutil
from pathlib import Path

total, used, free = shutil.disk_usage(Path("."))
print(total, used, free)  # 実行結果例: 512000000000 320000000000 192000000000

print(shutil.which("python"))  # 実行結果例: C:\Python311\python.exe や /usr/bin/python

メタデータのみコピー(権限・stat)

from pathlib import Path
import shutil

src = Path("bin") / "tool.sh"
dst = Path("bin") / "tool_copy.sh"

shutil.copyfile(src, dst)     # 内容のみ
shutil.copymode(src, dst)     # 権限だけ合わせる(実行権など)
shutil.copystat(src, dst)     # タイムスタンプ等のstat全体も合わせる
# 実行結果: tool_copy.sh の権限・時刻が src と同一になる

エラー処理:rmtree の onerror で頑固なファイルに対処

Windows で「使用中」ファイルや読み取り専用属性が原因で失敗することがある。onerror で再試行戦略を組み込み可能。

from pathlib import Path
import os, stat, shutil

def handle_remove_readonly(func, path, exc_info):
    # 読み取り専用を解除して再試行
    os.chmod(path, stat.S_IWRITE)
    func(path)

target = Path("to_delete")
# 読み取り専用などでも削除を試みる
shutil.rmtree(target, onerror=handle_remove_readonly)
# 実行結果: 読み取り専用ファイルも削除できる場合がある

“安全な上書き”の基本設計(バックアップ→置換)

from pathlib import Path
import shutil

src = Path("generated") / "model.pkl"
dst = Path("deploy") / "model.pkl"
bak = dst.with_suffix(".bak")

dst.parent.mkdir(parents=True, exist_ok=True)

# 既存をバックアップ
if dst.exists():
    shutil.copy2(dst, bak)    # メタデータ保持でバックアップ

# 一時ファイルに書き出してから、原子的に move(同ボリューム前提)
tmp = dst.with_suffix(".tmp")
shutil.copy2(src, tmp)
tmp.replace(dst)              # Path.replace は上書き寄りの rename
# 実行結果: deploy/model.pkl が一時ファイルから安全に差し替えられる

“差分ミラー”(簡易 rsync 風):更新されたものだけ上書き

from pathlib import Path
import shutil

def newer_than(a: Path, b: Path) -> bool:
    return not b.exists() or a.stat().st_mtime > b.stat().st_mtime

def mirror(src_root: Path, dst_root: Path) -> None:
    dst_root.mkdir(parents=True, exist_ok=True)
    for src in src_root.rglob("*"):
        rel = src.relative_to(src_root)
        dst = dst_root / rel
        if src.is_dir():
            dst.mkdir(exist_ok=True)
            continue
        if newer_than(src, dst):
            dst.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(src, dst)
            # 実行結果: 更新されていれば dst にコピーされる

mirror(Path("src_data"), Path("dst_data"))

進捗表示付きコピー(大容量の可視化)

shutil にはコールバックはありませんが、copyfileobj を使えば自前進捗を出せます。

from pathlib import Path
import shutil, sys

def copy_with_progress(src: Path, dst: Path, chunk=1024*1024):
    size = src.stat().st_size
    done = 0
    dst.parent.mkdir(parents=True, exist_ok=True)
    with src.open("rb") as r, dst.open("wb") as w:
        while True:
            buf = r.read(chunk)
            if not buf:
                break
            w.write(buf)
            done += len(buf)
            # 簡易プログレス
            sys.stdout.write(f"\r{done/size:.1%}")
            sys.stdout.flush()
    print("\nDone.")

copy_with_progress(Path("big/file.bin"), Path("backup/file.bin"))
# 実行結果: 進捗率が行内更新で表示され、完了すると "Done." と出る

スニペットをまとめる

# 1) 日次ログをzipでまとめる
from pathlib import Path
import shutil

def archive_logs(log_dir: Path, out_dir: Path, tag: str) -> Path:
    out_dir.mkdir(parents=True, exist_ok=True)
    base = out_dir / f"logs_{tag}"
    return Path(shutil.make_archive(str(base), "zip", root_dir=str(log_dir)))

# 2) 一時ディレクトリで作業→成果物だけ移動
from pathlib import Path
import shutil, tempfile

def build_artifact(builder) -> Path:
    with tempfile.TemporaryDirectory() as td:
        tdir = Path(td)
        artifact = builder(tdir)         # tdir内に成果物を作る関数
        dst = Path("dist") / artifact.name
        dst.parent.mkdir(parents=True, exist_ok=True)
        shutil.move(artifact, dst)       # 原子的に近い置き換え
        return dst

# 3) “ゴミ掃除”:サイズが0のファイルや~で終わるバックアップを削除
from pathlib import Path
import shutil

def cleanup(root: Path) -> int:
    count = 0
    for p in root.rglob("*"):
        if p.is_file() and (p.stat().st_size == 0 or p.name.endswith("~")):
            p.unlink()
            count += 1
    return count  # 削除数

よくある落とし穴と対策

rmtree を誤って実行
→ 対策:対象を表示・件数確認→確認プロンプト→実行。あるいはゴミ箱送信ライブラリを使用。
copytree の上書きで失敗
→ Py3.8+ の dirs_exist_ok=True を使う。古いPythonでは一度 rmtree が必要。
・更新時刻を失う
→ バックアップは copy2 を使う。copystat を保持しない。
・異ボリュームの move が遅い
→ 「大容量の予測→十分な空き容量確認(disk_usage)→時間見積もり→ copyfileobj」 でチューニング。
Windows のロック/属性
onerror で再試行ロジック、読み取り専用解除、プロセスのクローズ徹底。

参考文献

[1] 独習Python 第2版 (2025, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版(2024, Alex, O’Reilly Japan)
[3] Python実践レシピ (2022, 鈴木たかのり 他, 技術評論社)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?