Python標準ライブラリshutilによるファイルシステム操作の実践

Pythonの標準モジュール群において、osはパス解析やディレクトリの作成・削除といった基本的なファイルシステム操作を提供します。ただし、ファイルのコンテンツ複製、ディレクトリツリー全体の移動、非空ディレクトリの削除、あるいはアーカイブの作成・展開といった高水準なタスクに対しては機能不足に陥ります。こうした要件をカバーするのがshutil(Shell Utilities)モジュールです。本セクションでは、shutilが提供する主要なインターフェースと、実環境での適用パターンを技術的に解説します。

ファイルとディレクトリツリーの複製

単一ファイルの複製にはshutil.copy()が利用されます。宛先引数にディレクトリを指定した場合、元のファイル名が維持されたままコピーされます。ファイル名を明示的に指定した場合は、複製と同時にリネームが実行されます。注意点として、ソースパスがディレクトリである場合、IsADirectoryErrorが発生するため事前の型判定が推奨されます。

import shutil
import os

def replicate_source(file_origin, destination_base):
    if not os.path.isfile(file_origin):
        raise ValueError("コピー元はファイルである必要があります。")

    # 宛先がディレクトリなら同名で配置、ファイルパス指定ならリネーム配置
    if os.path.isdir(destination_base):
        target_path = os.path.join(destination_base, os.path.basename(file_origin))
    else:
        target_path = destination_base
        
    shutil.copy(file_origin, target_path)
    return target_path

ディレクトリ構造ごと複製する場合はshutil.copytree()を使用します。この関数は再帰的にサブディレクトリとファイルのコピーを実行しますが、宛先パスは既に存在していてはなりません。存在するパスを指定するとFileExistsErrorがスローされます。コピー元の判定は厳密で、ディレクトリ以外が渡された場合はNotADirectoryErrorとなります。

import shutil

def clone_directory_tree(source_root, new_root):
    if os.path.exists(new_root):
        raise FileExistsError(f"宛先ディレクトリ '{new_root}' は既に存在します。")
    
    shutil.copytree(source_root, new_root)
    return new_root

アイテムの移動とリネーム

shutil.move()はファイルやディレクトリの場所を変更する際に用いられます。内部的には、コピー元とコピー先が同一ファイルシステムにある場合はOSレベルのrename()が呼び出され、異なるシステム間ではコピー後に元の削除が行われます。このため、os.rename()では発生するクロスデバイスエラーを回避でき、リネーム操作も同時に処理可能です。ソースが存在しない場合はFileNotFoundErrorが発生します。

import shutil
import os

def transfer_item(item_path, target_location):
    if not os.path.exists(item_path):
        raise FileNotFoundError(f"対象 '{item_path}' が見つかりません。")
        
    # 宛先ディレクトリが存在しない場合は作成
    dest_dir = target_location if os.path.isdir(target_location) else os.path.dirname(target_location)
    if dest_dir and not os.path.exists(dest_dir):
        os.makedirs(dest_dir, exist_ok=True)
        
    final_path = shutil.move(item_path, target_location)
    return final_path

ディレクトリの完全削除

osモジュールの削除関数は対象が空の場合にのみ動作しますが、実務では内部にファイルが残っているディレクトリを消去するケースが頻発します。この場合、shutil.rmtree()が有効です。該函数は指定されたパス配下のすべてのサブディレクトリとファイルを再帰的に削除します。存在しないパスを渡すとFileNotFoundErrorを返すため、実行前のバリデーションが重要です。

import shutil
import os

def purge_directory(target_dir):
    if not os.path.isdir(target_dir):
        raise NotADirectoryError(f"'{target_dir}' はディレクトリではありません。")
    shutil.rmtree(target_dir)

アーカイブ形式の確認・作成・展開

shutilは標準で複数のアーカイブ形式をサポートしており、shutil.get_archive_formats()で利用可能な形式の一覧を取得できます。圧縮にはmake_archive()、解凍にはunpack_archive()が対応しています。

import shutil
import os

def list_supported_formats():
    formats = shutil.get_archive_formats()
    for fmt_name, description in formats:
        print(f"形式: {fmt_name} | 説明: {description}")

def generate_archive(target_dir, output_base, fmt='zip'):
    # output_baseには拡張子を含めないのが仕様
    archive_path = shutil.make_archive(output_base, fmt, target_dir)
    return archive_path

def deploy_archive(archive_file, extract_destination):
    shutil.unpack_archive(archive_file, extract_destination)
    return extract_destination

実務環境での応用設計

以下の実装例は、上記の機能を組み合わせ、データバックアップやファイル整理のワークフローに組み込むための設計パターンです。エラーハンドリングとパス検証を統合し、運用時の安定性を高めています。

日時スタンプ付きディレクトリバックアップ:

import shutil
import os
from datetime import datetime

def create_timestamp_backup(src_path, backup_root, base_name="data_snapshot"):
    if not os.path.isdir(src_path):
        raise ValueError(f"バックアップ対象 '{src_path}' は有効なディレクトリではありません。")

    os.makedirs(backup_root, exist_ok=True)
    
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    archive_filename = f"{base_name}_{timestamp}"
    archive_fullpath = os.path.join(backup_root, archive_filename)
    
    created_archive = shutil.make_archive(archive_fullpath, 'zip', src_path)
    print(f"バックアップ完了: {created_archive}")
    return created_archive

安全なファイル移動とリネーム統合関数:

import shutil
import os

def safe_relocate_and_rename(source_file, dest_directory, new_name):
    if not os.path.isfile(source_file):
        raise FileNotFoundError(f"ソースファイル '{source_file}' が存在しません。")

    if not os.path.isdir(dest_directory):
        os.makedirs(dest_directory, exist_ok=True)

    final_path = os.path.join(dest_directory, new_name)
    
    if os.path.exists(final_path):
        raise FileExistsError(f"宛先 '{final_path}' に既に同名のファイルが存在します。")

    moved_path = shutil.move(source_file, final_path)
    return moved_path

タグ: shutilモジュール Pythonファイル操作 アーカイブ処理 再帰ディレクトリ管理 標準ライブラリ

8月4日 18:45 投稿