【Python標準ライブラリ入門 第6回】ファイル操作を極める!globで賢く検索、shutilで楽々コピー&移動・削除

「フォルダの中にある特定の種類のファイルだけをまとめて処理したいな…」
「手作業でのファイルのコピーや移動、もうウンザリ!」
「Pythonスクリプトで、もっと高度なファイル管理を自動化できないかな?」

こんにちは! Python標準ライブラリ探検隊、隊長のPythonistaです! 前回はcsvモジュールを使って、表形式データをスマートに扱う方法を学びましたね。データの入出力がかなりスムーズになったことと思います。

シリーズ第6回の今回は、日々のコンピュータ作業で避けては通れないファイルやディレクトリの操作を、さらに強力に、そして効率的に行うための2つの標準ライブラリ、glob(グロブ)モジュールshutil(シェユーティル)モジュールを特集します! globモジュールを使えば、ワイルドカード(*?など)を使って目的のファイルを賢く見つけ出すことができ、shutilモジュールを使えば、ファイルのコピー、移動、ディレクトリツリーの削除といった高水準な操作を簡単に行えます。この2つを組み合わせることで、面倒なファイル管理作業の多くを自動化できるようになります。さあ、Pythonでファイル操作の達人を目指しましょう!

その他の投稿はこちら


1. globモジュール:パターンマッチでファイルを見つけ出す魔法の杖

globモジュールは、Unixシェルで使われるルール(ワイルドカード)に従って、指定したパターンに一致するファイルやディレクトリのパス名を検索し、そのリスト(またはイテレータ)を取得するためのモジュールです。

1.1. globモジュールのインポートと基本的な使い方 (glob.glob())

import glob
import os # 動作確認のためにosモジュールも使うことがあります

# 事前にテスト用のファイルやディレクトリを作成しておくと試しやすいです。
# 例:
# my_project/
#   |- data1.txt
#   |- data2.csv
#   |- image01.jpg
#   |- image02.png
#   |- script.py
#   |- sub_folder/
#       |- notes.txt
#       |- data_old.txt

# カレントディレクトリの全てのアイテムを取得
all_items = glob.glob('*')
print(f"カレントディレクトリの全アイテム: {all_items}")

# 特定の拡張子のファイルを取得 (.txtファイル)
txt_files = glob.glob('*.txt')
print(f"\n.txt ファイル: {txt_files}")

# 特定の文字列で始まるファイルを取得 (imageで始まるファイル)
image_files = glob.glob('image*')
print(f"\nimageで始まるファイル: {image_files}")

# 一文字のワイルドカード (?)
# 例えば 'data?.txt' は 'data1.txt' にはマッチするが 'data10.txt' にはマッチしない
single_char_files = glob.glob('data?.txt')
print(f"\ndata?.txt にマッチするファイル: {single_char_files}")

# 角括弧で文字集合を指定 ([0-9]で数字一文字)
files_with_numbers = glob.glob('image[0-9][0-9].*') # image00.jpg ~ image99.pngなど
print(f"\nimage[0-9][0-9].* にマッチするファイル: {files_with_numbers}")

glob.glob()は、パターンに一致したパスのリストを返します。一致するものがなければ空のリストが返ります。

1.2. イテレータを返す glob.iglob()

もし大量のファイルがマッチする可能性がある場合、glob.glob()は全てのパスをメモリに読み込むため効率が悪いことがあります。そのような場合は、結果を一つずつ返すイテレータを生成するglob.iglob()を使うとメモリ効率が良くなります。

import glob

print("\n--- iglobのテスト (大量ファイルがある場合に有効) ---")
# ここではカレントディレクトリの全アイテムをイテレータで取得
for item_path in glob.iglob('*'):
    print(item_path)

1.3. サブディレクトリの再帰的な検索 (Python 3.5+)

特定のディレクトリだけでなく、その中のサブディレクトリまで含めて再帰的にファイルを検索したい場合は、**ワイルドカードとrecursive=Trueオプションを使います。

import glob
import os

# 事前に 'my_project' や 'my_project/sub_folder' のような構造を作っておく
# os.makedirs("my_project/sub_folder", exist_ok=True)
# open("my_project/main.txt", "w").close()
# open("my_project/sub_folder/sub.txt", "w").close()

print("\n--- 'my_project' 以下の全ての.txtファイルを再帰的に検索 ---")
# Python 3.5以降
try:
    recursive_txt_files = glob.glob('my_project/**/*.txt', recursive=True)
    # 注意: recursive=True を使う場合、'**' はディレクトリの区切りを含めてマッチします。
    # Windows環境でパス区切りが'\'の場合、パターン中の'/'は自動的に調整されることがあります。
    # 確実に動かすためには os.path.join や pathlib を使うのがより堅牢です。
    # ここでは簡易的な例として示します。
    for f in recursive_txt_files:
        print(f)
except Exception as e:
    print(f"再帰検索エラー: {e} (Python 3.5以降で正しく動作します)")

# より推奨される再帰検索の方法 (カレントディレクトリからの例)
# current_path = os.getcwd()
# for root, dirs, files in os.walk(current_path):
#     for file in files:
#         if file.endswith(".txt"):
#             print(os.path.join(root, file))

**は0個以上のディレクトリとサブディレクトリにマッチします。


2. shutilモジュール:高水準なファイル・ディレクトリ操作

shutil(シェルユーティリティーズの略)モジュールは、osモジュールが提供する基本的なファイル操作よりも、より高水準で便利な機能を提供します。特にファイルのコピーや移動、ディレクトリツリーの操作などが簡単に行えます。

2.1. shutilモジュールのインポート

import shutil
import os # 準備や確認のために併用

2.2. ファイルのコピー

  • shutil.copy(src, dst): ファイルsrcdstにコピーします。dstがディレクトリの場合、そのディレクトリ内にsrcと同じ名前でコピーされます。dstにファイル名を指定すれば、その名前でコピーされます。パーミッションはコピーされません。
  • shutil.copy2(src, dst): copy()と似ていますが、パーミッションや最終アクセス時刻、最終更新時刻といったメタデータも可能な限りコピーしようとします。
import shutil
import os

# テスト用ファイルとディレクトリの準備
os.makedirs("shutil_test/original_dir", exist_ok=True)
os.makedirs("shutil_test/backup_dir", exist_ok=True)
with open("shutil_test/original_dir/source.txt", "w") as f:
    f.write("これはコピー元ファイルです。")

source_file = "shutil_test/original_dir/source.txt"
destination_dir = "shutil_test/backup_dir/"
destination_file = os.path.join(destination_dir, "copied_source.txt")

# copy2を使ってコピー (メタデータも保持)
if os.path.exists(source_file):
    shutil.copy2(source_file, destination_dir) # ディレクトリにコピー
    print(f"\n'{source_file}' を '{destination_dir}' にコピーしました。")
    
    shutil.copy2(source_file, destination_file) # 別名でファイルにコピー
    print(f"'{source_file}' を '{destination_file}' にコピーしました。")

2.3. ファイルやディレクトリの移動/名前変更

  • shutil.move(src, dst): ファイルまたはディレクトリsrcdstに移動します。もしdstが既存のディレクトリであれば、srcはそのディレクトリ内に移動されます。dstに新しいパス(ファイル名やディレクトリ名を含む)を指定すれば、名前の変更を伴う移動になります。
import shutil
import os

os.makedirs("shutil_test/move_source", exist_ok=True)
os.makedirs("shutil_test/move_target_dir", exist_ok=True)
with open("shutil_test/move_source/movable_file.txt", "w") as f:
    f.write("移動されるファイルです。")

source_to_move = "shutil_test/move_source/movable_file.txt"
target_dir_for_move = "shutil_test/move_target_dir/"
renamed_file_path = os.path.join(target_dir_for_move, "moved_and_renamed.txt")

if os.path.exists(source_to_move):
    shutil.move(source_to_move, target_dir_for_move)
    print(f"\n'{source_to_move}' を '{target_dir_for_move}' に移動しました。")
    # 移動後のパスは target_dir_for_move + "movable_file.txt" になる

    # 再度ファイルを作成して、次は名前変更を伴う移動
    with open(source_to_move, "w") as f: f.write("再作成")
    shutil.move(source_to_move, renamed_file_path)
    print(f"'{source_to_move}' を '{renamed_file_path}' に移動・改名しました。")

2.4. ディレクトリツリーのコピー

  • shutil.copytree(src, dst, dirs_exist_ok=False): ディレクトリsrcとその中身(サブディレクトリやファイル)全てを、新しいディレクトリdstに再帰的にコピーします。dstは存在しない名前でなければなりません(Python 3.8以降ではdirs_exist_ok=Trueを指定すると、dstが存在しても中のファイルをマージするようにコピーできますが、注意が必要です)。
import shutil
import os

source_tree = "shutil_test/original_dir" # 上で作ったファイル入りディレクトリ
destination_tree = "shutil_test/copied_tree"

# 既存のコピー先があれば一旦削除 (テストのため)
if os.path.exists(destination_tree):
    shutil.rmtree(destination_tree) 

if os.path.exists(source_tree):
    shutil.copytree(source_tree, destination_tree)
    print(f"\nディレクトリツリー '{source_tree}' を '{destination_tree}' にコピーしました。")
    print(f"コピー先の内容: {os.listdir(destination_tree)}")

2.5. ディレクトリツリーの削除

  • shutil.rmtree(path, ignore_errors=False, onerror=None): ディレクトリpathとその中身(サブディレクトリやファイル)全てを再帰的に削除します。この操作は非常に強力で、一度削除したファイルやディレクトリは基本的に元に戻せません。使用には細心の注意が必要です!
  • ignore_errors=Trueにすると、削除中にエラーが発生しても処理を続行します。
import shutil
import os

dir_to_delete_completely = "shutil_test/copied_tree" # 上でコピーしたディレクトリ

if os.path.exists(dir_to_delete_completely):
    # input("本当に削除しますか? (yes/no): ") などで確認を入れるのが安全
    print(f"\n警告: ディレクトリツリー '{dir_to_delete_completely}' を中身ごと削除します。")
    # shutil.rmtree(dir_to_delete_completely)
    # print(f"'{dir_to_delete_completely}' を完全に削除しました。")
    print("(実際の削除はコメントアウトしています。試す際は注意してください)")
else:
    print(f"'{dir_to_delete_completely}' は存在しません。")

# このスクリプトの最後にテスト用ディレクトリ全体を削除するなどのクリーンアップ処理を入れると良い
# if os.path.exists("shutil_test"):
#     shutil.rmtree("shutil_test")
#     print("\nテスト用ディレクトリ 'shutil_test' をクリーンアップしました。")

重ねて強調しますが、shutil.rmtree()の使用は非常に慎重に行ってください。


3. globshutilの合わせ技!実践的なファイル操作の自動化

これら2つのモジュールを組み合わせることで、より強力なファイル操作の自動化が可能になります。

例1:特定の拡張子のファイルをまとめてバックアップフォルダにコピーする

import glob
import shutil
import os
from datetime import datetime

source_directory = "my_documents" # 対象のディレクトリ
backup_base_directory = "backups"
file_extension_to_backup = "*.txt" # バックアップしたい拡張子

# テスト用ディレクトリとファイルの準備
os.makedirs(source_directory, exist_ok=True)
os.makedirs(backup_base_directory, exist_ok=True)
with open(os.path.join(source_directory, "report1.txt"), "w") as f: f.write("レポート1")
with open(os.path.join(source_directory, "memo.md"), "w") as f: f.write("メモ")
with open(os.path.join(source_directory, "important_data.txt"), "w") as f: f.write("重要データ")

# 今日の日付でバックアップフォルダ名を作成
today_str = datetime.now().strftime("%Y%m%d")
backup_folder = os.path.join(backup_base_directory, today_str)
os.makedirs(backup_folder, exist_ok=True) # 日付ごとのバックアップフォルダを作成

# バックアップ対象のファイルパスを取得
# os.path.joinでsource_directoryとfile_extension_to_backupを結合
search_pattern = os.path.join(source_directory, file_extension_to_backup)
files_to_backup = glob.glob(search_pattern)

print(f"\n--- バックアップ開始 ({file_extension_to_backup} ファイル) ---")
if files_to_backup:
    for file_path in files_to_backup:
        try:
            print(f"コピー中: {file_path} -> {backup_folder}")
            shutil.copy2(file_path, backup_folder) # copy2でメタデータも保持
        except Exception as e:
            print(f"  エラー: {file_path} のコピーに失敗しました - {e}")
    print("バックアップが完了しました。")
else:
    print("バックアップ対象のファイルが見つかりませんでした。")

# 後片付け (テスト用)
# shutil.rmtree(source_directory)
# shutil.rmtree(backup_base_directory)

例2:ダウンロードフォルダ内の古いファイルを「古いファイル」フォルダに移動する

(この例はファイルの最終更新日時を使うため、os.path.getmtime()datetimeモジュールも活用します。少し複雑になるので、ここではコンセプトの紹介に留め、実際のコードは読者の演習課題としても良いかもしれません。)

考え方:

  1. 移動元フォルダ(例:ダウンロードフォルダ)と移動先フォルダ(例:古いファイルフォルダ)のパスを設定。
  2. globで移動元フォルダ内のファイル一覧を取得。
  3. 各ファイルについてos.path.getmtime()で最終更新日時(Unixタイムスタンプ)を取得し、datetime.fromtimestamp()datetimeオブジェクトに変換。
  4. 現在日時と比較し、一定期間(例: 30日前)より古いかどうかを判定。
  5. 古ければshutil.move()で移動先フォルダに移動。

4. ファイル操作を行う際の重要注意点

  • バックアップは最優先: 特にファイルの削除 (os.remove, shutil.rmtree) や上書きを伴う移動・コピーを行う前には、万が一に備えて重要なデータのバックアップを必ず作成してください。
  • パスの正確性の確認: 操作対象のファイルやディレクトリのパスが本当に正しいか、十分に確認しましょう。特に相対パスと絶対パスの扱いや、存在しないパスを指定していないか注意が必要です。
  • エラーハンドリングの徹底: try-exceptブロックを使って、ファイルが見つからない (FileNotFoundError)、アクセス権限がない (PermissionError) などの予期せぬエラーに備えましょう。
  • 少量データでのテスト: スクリプトが完成したら、まずは少量のテスト用ファイルや、コピーしても問題ないデータで十分にテストし、意図通りに動作することを確認してから、実際の大量データや重要なデータに適用してください。
  • shutil.rmtree() の危険性再確認: この関数はディレクトリとその中身を完全に削除し、通常は元に戻せません。使用する際は、対象パスを何度も確認し、本当に削除して良いか慎重に判断してください。コメントアウトしておき、実行直前に有効にするなどの工夫も有効です。

まとめ:globshutil でファイル管理を自動化しよう!

今回は、Pythonの標準ライブラリ解説シリーズ第6回として、ファイル検索の達人globモジュールと、高水準なファイル操作の便利屋shutilモジュールについて学びました。

  • globモジュールを使ったパターンマッチによるファイルパスの効率的な検索。
  • shutilモジュールを使った、ファイルのコピー、移動、ディレクトリツリーのコピーや完全削除といった高水準な操作。
  • これら2つのモジュールを組み合わせることで、日常的なファイル管理タスクを強力に自動化できること。

これらのモジュールを使いこなせば、例えば「毎週月曜日に、特定のフォルダにある先週分のログファイル(ファイル名に日付が含まれる)をアーカイブフォルダに移動し、zip圧縮する」といったような、少し複雑なファイル操作の自動化もPythonで実現できるようになります。

ただし、繰り返しになりますが、ファイルシステムを操作するプログラムは、意図しない結果を招くと影響が大きい場合があります。常に慎重に、テストを十分に行いながら活用してください。

次回もPythonの便利な標準ライブラリの世界を一緒に探検していきましょう!お楽しみに!

【Python標準ライブラリ入門 第7回】printデバッグ卒業!loggingモジュールで本格的なログ管理をはじめよう

その他の投稿はこちら

コメント

このブログの人気の投稿

タイトルまとめ

これで迷わない!WindowsでPython環境構築する一番やさしい方法 #0

【Python標準ライブラリ完結!】11の冒険をありがとう!君のPython力が飛躍する「次の一歩」とは? 🚀