The jonki

呼ばれて飛び出てじょじょじょじょーんき

pathlibをもっと使おう

皆さんはPythonpathlib使ってますか?私は非常によく使っています.例えば機械学習では学習の前に前処理を多く行うケースが非常に多いですが,このようなに知っておくと便利です.pathlibは意外とPython 3.4(2014年~)からとそれなりに新しいため,古くからのPythonユーザーは os.pathの方をよく使っているかもしれません.ただpathlibは文字列ではなくPathクラスとして扱ってくれることで,例えばLinux/Windowsのパス表記の違いを吸収してくれたりします.

pathlibos.pathの比較は公式のpathlibドキュメントに譲るとして,私がよく使うpathlibのクラスを紹介します.また今回改めてドキュメントを眺めて知った便利関数も多いので,公式ドキュメントに目を通すのもオススメします.

今回は下記のような複数の素性の異なるデータセットに対して処理するケースなどを考えてみます.

.
├── datasetA
│   ├── abc
│   │   ├── a.wav
│   │   └── b.wav
│   └── def
│       └── d.wav
└── datasetB
    ├── 123.wav
    └── sub
        └── 456.wav

便利関数26選

検索・走査系

所定ディレクトリ以下にある特定のファイルを再帰的に探したい

この時に使えるのが,globあるいはrglobです.個人的にはrglobの方がショートカット記法になっていて,正規表現のタイポが避けられるので好んで使っています.スペースの都合上 list化して表示しています.

from pathlib import Path

In [1]: list(Path(".").rglob("*.wav"))
Out[1]:
[PosixPath('datasetA/abc/a.wav'),
 PosixPath('datasetA/abc/b.wav'),
 PosixPath('datasetA/def/d.wav'),
 PosixPath('datasetB/123.wav'),
 PosixPath('datasetB/sub/456.wav')]

In [2]: list(Path(".").glob("**/*.wav"))
Out[2]:
[PosixPath('datasetA/abc/a.wav'),
 PosixPath('datasetA/abc/b.wav'),
 PosixPath('datasetA/def/d.wav'),
 PosixPath('datasetB/123.wav'),
 PosixPath('datasetB/sub/456.wav')]

サブディレクトリをリストアップしたい

os.listdir 相当の捜査です.

In [3]: list(Path(".").iterdir())
Out[3]: [PosixPath('datasetA'), PosixPath('datasetB')]

編集系

絶対パス内の相対パスを取得したい

例えばデータセット内のディレクトリ構造を維持したまま,新たに別のディレクトリにしてパスをすげ替えてファイルを書き出す場合などに有用です.

In [4]: Path("/home/jojonki/datasetA/abc.wav").relative_to("/home/jojonki")
Out[4]: PosixPath('datasetA/abc.wav')

ファイル名や拡張子を変更したい

これもよく使います.

# ファイル名を変更
In [5]: Path("datasetA/abc.wav").with_name("AAA.mp3")
Out[5]: PosixPath('datasetA/AAA.mp3')

# 拡張子を除くファイル名を変更
In [6]: Path("datasetA/abc.wav").with_stem("AAA")
Out[6]: PosixPath('datasetA/AAA.wav')

# 拡張子を変更
In [7]: Path("datasetA/abc.wav").with_suffix(".mp3")
Out[7]: PosixPath('datasetA/abc.mp3')

# os.rename相当
In [8]: Path("datasetA/abc/b.wav").rename("datasetA/abc/c.wav")
Out[8]: PosixPath('datasetA/abc/c.wav')

宣言系

Pathインスタンスを生成したい

非常に直感的にPathクラスを作れます.例えば何らかのデータのディレクトリに対して動的にパスを作る際などに便利です.

# スラッシュでつながる
In [9]: root_dir = Path("datasetA/abc")
In [9]: root_dir / "sub" / "def.wav"
Out[9]: PosixPath('datasetA/abc/sub/def.wav')

# os.path.joinと同様
In [10]: Path("datasetA").joinpath("abc", "def.wav")
Out[10]: PosixPath('datasetA/abc/def.wav')

生成系

ディレクトリを生成したい

In [11]: Path("datasetC/").mkdir()

# 必要に応じて親ディレクトリも作成.mkdir -p, os.makedirs相当
In [12]: Path("datasetD/1/2/3/4/5").mkdir(parents=True)

ファイルを読み書きしたい

これは知りませんでしたが便利そうです.

In [13]: Path("README.txt").write_text("# Title\nhello")
Out[13]: 13
    
In [14]: Path("README.txt").read_text()
Out[14]: '# Title\nhello'

アクセス系

これらもよく使うので頭の片隅に入れておくと良いと思います.

パスの存在を確認したり,絶対パスを取得したい

# 存在確認
In [15]: Path("datasetA").exists()
Out[15]: True
    
# ディレクトリ?
In [16]: Path("datasetA").is_dir()
Out[16]: True
    
# 絶対パス?
In [17]: Path("datasetA").is_absolute()
Out[17]: False
    
# 絶対パス取得
In [18]: Path("datasetA").absolute()
Out[18]: PosixPath('/Users/jonki/sandbox/datasetA')

ファイル名に関しての色々な情報を取りたい

In [19]: Path("datasetA/abc/a.wav")
Out[19]: PosixPath('datasetA/abc/a.wav')

# 拡張子も含めて
In [20]: Path("datasetA/abc/a.wav").name
Out[20]: 'a.wav'

# 拡張子除く
In [21]: Path("datasetA/abc/a.wav").stem
Out[21]: 'a'

# 拡張子を取得
In [22]: Path("datasetA/abc/a.wav").suffix
Out[22]: '.wav'

# 最後の.gzだけ
In [23]: Path("data.tar.gz").suffix
Out[23]: '.gz'

# まとめて拡張子を取る
In [24]: Path("data.tar.gz").suffixes
Out[24]: ['.tar', '.gz']

# そのファイル/ディレクトリの親ディレクトリ
In [25]: Path("datasetA/abc/a.wav").parent
Out[25]: PosixPath('datasetA/abc')

# パスを分解
In [26]: Path("datasetA/abc/a.wav").parts
Out[26]: ('datasetA', 'abc', 'a.wav')

まとめ

以上Pythonpathlibをまとめました. 昨今はChatGPTなどでやりたいことを伝えればChatGPTが生成してくれますが,今回紹介したようなコードは非常によく使うので覚えておいて損はないと思います.文字列同士を無理やり駆使してファイル名を変更していたりするコードがたまにありますが,pathlibを使えばそのようなこともなくなりますね.