Python におけるファイルおよびディレクトリパスの操作

PythonBeginner
オンラインで実践に進む

はじめに

この実験(Lab)では、最新の pathlib モジュールを使用して、Python でファイルおよびディレクトリのパスを扱う方法を学習します。古い os.path モジュールも依然として一般的ですが、pathlib はパス操作をより直感的かつ読みやすくするオブジェクト指向のアプローチを提供します。Windows、macOS、Linux などの異なるオペレーティングシステムで機能する方法で、パス、ファイル、ディレクトリを作成、検査、検証、および操作する方法を学びます。

この実験(Lab)の終わりまでに、以下のスキルを習得します。

  • パスコンポーネントを正しく結合する方法。
  • ディレクトリの作成と管理。
  • 絶対パスと相対パスの区別。
  • パスの存在と種類の検証。
  • コピー、移動、削除などの一般的なファイル操作の実行。
  • ディレクトリツリーを走査してファイルやフォルダを見つける方法。

基本的なパス操作

このステップでは、pathlib モジュールを使用したパス操作の基本を学習します。これには、パスコンポーネントの結合、現在の作業ディレクトリの取得、および新しいディレクトリの作成が含まれます。

pathlib モジュールは、ファイルシステムパスを扱うためのエレガントでクロスプラットフォームな方法を提供します。Windows ではパスはバックスラッシュ (\) を使用しますが、Unix 系システム(Linux、macOS)ではフォワードスラッシュ (/) を使用します。pathlib を使用することで、手動で調整することなく、どのプラットフォームでもコードが動作することが保証されます。

注記: WebIDE(VS Code のようなもの)の使用に慣れていない場合は、まずこちらの実験(Lab)で学習してください。

まず、パスコンポーネントを結合する方法を見てみましょう。/ 演算子は joinpath() の直感的なショートカットです。

  1. 左側のファイルエクスプローラーから main.py ファイルを開きます。
  2. main.py の内容を以下のコードに置き換えて、パスコンポーネントを結合し、結果を出力します。
from pathlib import Path

## / 演算子を使用してパスコンポーネントを結合
my_path = Path('usr') / 'bin' / 'spam'
print(my_path)
  1. ターミナルからスクリプトを実行します。
python3 main.py

pathlib がどのように正しく Unix スタイルのパスを形成するかを示す、以下の出力が表示されます。

usr/bin/spam

次に、Path.cwd() を使用して現在の作業ディレクトリを取得します。

  1. main.py の内容を以下のコードに置き換えます(これは以前のコードを置き換えます)。
from pathlib import Path

## 現在の作業ディレクトリを取得
cwd = Path.cwd()
print(cwd)
  1. スクリプトを再度実行します。
python3 main.py

出力には現在の作業ディレクトリが表示されます。この環境では /home/labex/project となります。

/home/labex/project

最後に、新しいディレクトリを作成してみましょう。親ディレクトリが存在しない場所にディレクトリを作成しようとすると、FileNotFoundError が発生します。これを避けるために、mkdir(parents=True) を使用します。これにより、必要なすべての親ディレクトリが作成されます。

  1. ネストされたディレクトリ構造を作成するために、main.py の内容を以下のコードに置き換えます。
from pathlib import Path

## 現在の作業ディレクトリを取得
cwd = Path.cwd()

## 新しいネストされたディレクトリのパスを定義
new_dirs = cwd / 'delicious' / 'walnut' / 'waffles'

## 欠落している親ディレクトリも含めてディレクトリを作成
new_dirs.mkdir(parents=True, exist_ok=True)

print(f"Successfully created: {new_dirs}")

exist_ok=True 引数は、ディレクトリが既に存在する場合にエラーが発生するのを防ぎます。これにより、スクリプトを複数回実行してもエラーになりません。

  1. スクリプトを実行します。
python3 main.py

スクリプトはネストされたディレクトリを作成し、確認メッセージを出力します。

Successfully created: /home/labex/project/delicious/walnut/waffles

ターミナルで ls -R delicious を実行することで、ディレクトリの作成を確認できます。

絶対パスと相対パス

このステップでは、絶対パスと相対パスを区別し、それらを操作する方法を学習します。

  • 絶対パス (absolute path) は、ファイルシステムのルート(Linux では /、Windows では C:\ など)から始まります。
  • 相対パス (relative path) は、現在の作業ディレクトリから定義されます。

pathlib は、パスが絶対であるかを確認したり、パスを絶対形式に解決したり、開始点からの相対パスを見つけたりするためのメソッドを提供します。

まず、is_absolute() を使用してパスが絶対であるかどうかを確認します。

  1. main.py の内容を以下のコードに置き換えます。
from pathlib import Path

## パスが絶対であるかを確認
print(f"Path('/') is absolute: {Path('/').is_absolute()}")
print(f"Path('..') is absolute: {Path('..').is_absolute()}")
  1. スクリプトを実行します。
python3 main.py

出力は、/ が絶対パスであり、..(親ディレクトリ)が相対パスであることを確認します。

Path('/') is absolute: True
Path('..') is absolute: False

次に、resolve() を使用して相対パスの完全な絶対パスを取得できます。これは、.(カレントディレクトリ)や ..(親ディレクトリ)のような相対パスを、完全で曖昧さのない形式に変換する場合に役立ちます。

  1. main.py の内容を以下のコードに置き換えます。
from pathlib import Path

## 相対パスを絶対形式に解決
## '..' は現在の作業ディレクトリの親ディレクトリを指します
resolved_path = Path('..').resolve()

print(f"Current working directory: {Path.cwd()}")
print(f"Resolved path of '..': {resolved_path}")
  1. スクリプトを実行します。
python3 main.py

出力には、現在のディレクトリと、その親の解決された絶対パスが表示されます。

Current working directory: /home/labex/project
Resolved path of '..': /home/labex

最後に、relative_to() を使用して、あるパスから別のパスへの相対パスを計算できます。

  1. 相対パスを計算するために、main.py の内容を以下のコードに置き換えます。
from pathlib import Path

## 開始点からの相対パスを取得
## これは、ルートディレクトリ '/' に対する '/etc/passwd' へのパスを示します
relative_path = Path('/etc/passwd').relative_to('/')
print(f"Path to '/etc/passwd' relative to '/': {relative_path}")
  1. スクリプトを実行します。
python3 main.py

出力は、ルートディレクトリに対する相対パスを示します。

Path to '/etc/passwd' relative to '/': etc/passwd

パスの検証と検査

このステップでは、パスのプロパティ(存在するかどうか、ファイルかディレクトリかなど)を確認する方法を学習します。また、ファイルサイズなどのファイルメタデータ(metadata)を取得する方法についても学習します。

一般的なタスクは、ファイルやディレクトリにアクセスしようとする前に、それらが存在するかどうかを確認することです。これには exists() メソッドが最適です。

  1. main.py を開き、内容を以下のコードに置き換えます。これにより、カレントディレクトリ (.)、ファイル (setup.py)、および存在しないファイルの存在がチェックされます。
from pathlib import Path

## パスが存在するかどうかを確認
print(f"Path('.') exists: {Path('.').exists()}")
print(f"Path('setup.py') exists: {Path('setup.py').exists()}")
print(f"Path('nonexistentfile') exists: {Path('nonexistentfile').exists()}")
  1. スクリプトを実行します。
python3 main.py

出力は、どのパスが存在するかを確認します。

Path('.') exists: True
Path('setup.py') exists: True
Path('nonexistentfile') exists: False

また、is_file() でパスがファイルであるか、または is_dir() でディレクトリであるかを具体的にチェックすることもできます。

  1. これらのチェックを実行するために、main.py の内容を以下のコードに置き換えます。
from pathlib import Path

## パスがファイルであるかを確認
print(f"Path('setup.py').is_file(): {Path('setup.py').is_file()}")
print(f"Path('/home').is_file(): {Path('/home').is_file()}")

## パスがディレクトリであるかを確認
print(f"Path('/').is_dir(): {Path('/').is_dir()}")
print(f"Path('setup.py').is_dir(): {Path('setup.py').is_dir()}")
  1. スクリプトを実行します。
python3 main.py

出力は、各パスのタイプを正しく識別します。

Path('setup.py').is_file(): True
Path('/home').is_file(): False
Path('/').is_dir(): True
Path('setup.py').is_dir(): False

最後に、ファイルサイズなどの詳細なファイル情報を取得するには、stat() メソッドを使用できます。これは、バイト単位のファイルサイズである st_size を含む様々な属性を持つオブジェクトを返します。

  1. システムファイルのサイズを取得するために、main.py の内容を以下のコードに置き換えます。
from pathlib import Path
import os

## ファイルへのパスを定義
file_path = Path('/bin/python3')

## ファイルの統計情報を取得
stat_info = file_path.stat()

## サイズをバイト単位で出力
print(f"File: {file_path}")
print(f"Size: {stat_info.st_size} bytes")
  1. スクリプトを実行します。
python3 main.py

出力には、Python 実行可能ファイルのサイズがバイト単位で表示されます。正確なサイズは異なる場合があります。

File: /bin/python3
Size: 10024 bytes

ファイルとフォルダの管理

このステップでは、shutil モジュールと pathlib を組み合わせて、ファイルのコピー、移動、名前の変更、削除といった一般的なファイルシステム操作を学習します。

shutil モジュールは、ファイル操作のための高水準な関数を提供します。まず、ファイルのコピーから始めましょう。shutil.copy() は単一のファイルを新しい場所にコピーします。

  1. 左側のファイルエクスプローラーから manage_files.py ファイルを開きます。
  2. manage_files.py の内容を以下のコードに置き換えます。これにより、spam.txt が /tmp/delicious ディレクトリにコピーされます。
import shutil
from pathlib import Path

## ソースと宛先を定義
source_file = Path('/tmp/spam.txt')
destination_dir = Path('/tmp/delicious')

## ファイルをコピー
shutil.copy(source_file, destination_dir)

print(f"'{source_file}' を '{destination_dir}' にコピーしました")

## コピー時にファイル名を変更することも可能です
shutil.copy('/tmp/eggs.txt', '/tmp/delicious/eggs2.txt')
print("'eggs.txt' を 'eggs2.txt' にコピーして名前を変更しました")
  1. スクリプトを実行します。
python3 manage_files.py

ディレクトリツリー全体をコピーするには、shutil.copytree() を使用します。

  1. manage_files.py の末尾に以下の行を追加します(既存のコードの後に追記します)。
## ディレクトリツリー全体をコピー
shutil.copytree('/tmp/bacon', '/tmp/bacon_backup')
print("ディレクトリ '/tmp/bacon' を '/tmp/bacon_backup' にコピーしました")

注: このスクリプトを複数回実行すると、宛先ディレクトリが既に存在するため、shutil.copytree() は失敗します。これは期待される動作です。今のところは一度だけ実行してください。

  1. スクリプトを再度実行して動作を確認します。

次に、shutil.move() を使用してファイルの移動と名前の変更を行います。この関数は、ファイルを別のディレクトリに移動するか、名前を変更するかのいずれかを行うことができます。

  1. manage_files.py の内容全体を、bacon.txt を移動するための以下のコードに置き換えます。
import shutil
from pathlib import Path

## 宛先ディレクトリが存在しない場合は作成
Path('/tmp/eggs').mkdir(parents=True, exist_ok=True)

## ファイルをディレクトリに移動(ソースが存在する場合のみ)
bacon_source = Path('/tmp/bacon.txt')
if bacon_source.exists():
    shutil.move('/tmp/bacon.txt', '/tmp/eggs')
    print("'/tmp/bacon.txt' を '/tmp/eggs' に移動しました")
else:
    print("'/tmp/bacon.txt' が見つかりません(既に移動済みかもしれません)")

## ファイルを移動して名前を変更
## まず、移動するファイルを再作成します
Path('/tmp/bacon.txt').touch()
shutil.move('/tmp/bacon.txt', '/tmp/eggs/new_bacon.txt')
print("'/tmp/bacon.txt' を '/tmp/eggs/new_bacon.txt' に移動して名前を変更しました")

注: このコードは移動前にソースファイルの存在を確認するため、スクリプトを複数回安全に実行できます。2 番目の部分は常に新しいファイルを作成し、それを移動します。

  1. スクリプトを実行します。

最後に、ファイルの削除とディレクトリの削除について見ていきましょう。

  • Path.unlink() は単一のファイルを削除します。
  • Path.rmdir() は空のディレクトリを削除します。
  • shutil.rmtree() はディレクトリとそのすべての内容を削除します。
  1. manage_files.py の内容全体を以下に置き換えます。
from pathlib import Path
import shutil

## 削除するファイルを作成
file_to_delete = Path('/tmp/deleteme.txt')
file_to_delete.touch()
print(f"'{file_to_delete}' を作成しました")

## ファイルを削除
file_to_delete.unlink()
print(f"'{file_to_delete}' を削除しました")

## 削除するディレクトリを作成
dir_to_delete = Path('/tmp/temp_dir')
dir_to_delete.mkdir(exist_ok=True)
## rmdir() は空のディレクトリでのみ機能します
dir_to_delete.rmdir()
print(f"空のディレクトリ '{dir_to_delete}' を削除しました")

## shutil.rmtree() を使用してディレクトリとその内容を削除します
## 注:ディレクトリが存在しない場合、これは失敗しますが、この例では問題ありません
if Path('/tmp/bacon_backup').exists():
    shutil.rmtree('/tmp/bacon_backup')
    print("ディレクトリ '/tmp/bacon_backup' とそのすべての中身を削除しました")
else:
    print("ディレクトリ '/tmp/bacon_backup' は存在しません(既に削除済みかもしれません)")

注: このスクリプトを複数回実行すると、ファイルやディレクトリが既に削除されているため、一部の操作が失敗する可能性があります。これは正常であり、これらの操作が冪等性(べきとうせい、idempotent:安全に繰り返し実行できる性質)を持たないことを示しています。

  1. スクリプトを実行して、削除操作を実行します。

ディレクトリトラバーサル

このステップでは、ディレクトリの内容を一覧表示する方法と、ディレクトリツリーを再帰的にトラバースする方法を学習します。

ディレクトリの内容を一覧表示するには、iterdir() メソッドを使用できます。これは、パス内のすべてのアイテム(ファイルとディレクトリ)のイテレータを返します。

  1. まず、サンプルとなるディレクトリ構造を設定しましょう。ターミナルで次のコマンドを実行します。
mkdir -p ~/project/data/level1 && touch ~/project/data/file0.txt ~/project/data/level1/file1.txt
  1. 次に、main.py を開き、その内容を次のコードに置き換えて、/usr/bin ディレクトリの内容を一覧表示します。
from pathlib import Path

## ディレクトリの内容を一覧表示
print("Contents of '/usr/bin':")
count = 0
for f in Path('/usr/bin').iterdir():
    if count < 5: ## 出力を短く保つため、最初の 5 つのアイテムのみ表示
        print(f)
        count += 1
    else:
        break
  1. スクリプトを実行します。
python3 main.py

出力には、/usr/bin 内の最初の数個のファイルとディレクトリが表示されます。正確なリストは異なる場合があります。

Contents of '/usr/bin':
/usr/bin/[
/usr/bin/addpart
/usr/bin/apt
/usr/bin/apt-cache
/usr/bin/apt-cdrom

iterdir() と stat() を組み合わせることで、ディレクトリ内のすべてのファイルの合計サイズを計算できます。

  1. main.py の内容を次のコードに置き換えます。このコードは直接の子のサイズのみを合計し、サブディレクトリには再帰しないことに注意してください。
from pathlib import Path

total_size = 0
directory = Path('/usr/bin')

for sub_path in directory.iterdir():
    if sub_path.is_file(): ## ファイルのサイズのみを加算するようにする
        total_size += sub_path.stat().st_size

print(f"Total size of files in '{directory}': {total_size} bytes")
  1. スクリプトを実行します。これにより、/usr/bin の直下にあるすべてのファイルの合計サイズが出力されます。

より複雑なタスクの場合、すべてのサブディレクトリとファイルを訪問するために、ディレクトリツリー全体を「ウォーク (walk)」する必要があるかもしれません。rglob() メソッドがこれに最適です。これは、特定のパターンに一致するすべてのパスを再帰的に検索します。パターンとして * を使用すると、すべてに一致します。

  1. まず、この例のために、より複雑なディレクトリ構造を作成します。ターミナルでこれを実行します。
mkdir -p /tmp/delicious/cats /tmp/delicious/walnut/waffles
touch /tmp/delicious/spam.txt /tmp/delicious/cats/catnames.txt /tmp/delicious/walnut/waffles/butter.txt

注記: これらのコマンドは何度実行しても安全です。mkdir -p コマンドはディレクトリが既に存在していても失敗しませんし、touch コマンドはファイルが既に存在していてもタイムスタンプを更新するだけです。

  1. main.py の内容を次のコードに置き換えて、/tmp/delicious ディレクトリをウォークします。
from pathlib import Path

p = Path('/tmp/delicious')
print(f"Walking directory tree for: {p}")

for i in p.rglob('*'):
    print(i)
  1. スクリプトを実行します。
python3 main.py

出力には、/tmp/delicious およびそのサブディレクトリ内のすべてのファイルとフォルダが一覧表示され、rglob() がツリー全体をどのようにトラバースするかを示します。

Walking directory tree for: /tmp/delicious
/tmp/delicious/spam.txt
/tmp/delicious/eggs2.txt
/tmp/delicious/cats
/tmp/delicious/walnut
/tmp/delicious/cats/catnames.txt
/tmp/delicious/walnut/waffles
/tmp/delicious/walnut/waffles/butter.txt

まとめ

この実験(Lab)の完了、おめでとうございます!Python でモダンな pathlib モジュールを使用して、ファイルパスとディレクトリパスを効果的に管理する方法を学びました。

この実験を通して、以下の項目を実践しました。

  • Path オブジェクトと / 演算子を使用して、クロスプラットフォームな方法でパスを作成および結合する方法。
  • 現在の作業ディレクトリを取得し、mkdir(parents=True) を使用してネストされたディレクトリを作成する方法。
  • 絶対パスと相対パスを区別し、is_absolute(), resolve(), relative_to() を使用してそれらの間で変換する方法。
  • exists(), is_file(), is_dir() でパスを検証し、stat() でファイルメタデータを検査する方法。
  • shutil モジュールと pathlib メソッドを使用して、コピー、移動、削除などの基本的なファイル管理タスクを実行する方法。
  • iterdir() で内容を一覧表示し、rglob() で再帰的にアイテムを検索することにより、ディレクトリ構造をトラバースする方法。

これらのスキルは、ファイルシステムと連携する堅牢で保守性の高い Python アプリケーションを作成するための基礎となります。