pathlibのよく使う処理まとめ

![<windows>](https://img.shields.io/badge/windows-10-<COLOR>)![<python>](https://img.shields.io/badge/python-3.8.3-<COLOR>)
#  はじめに
`pathlib`はパスを表すクラスを提供してくれます。このクラスには様々なメソッドが備わっており、os.pathのように文字でパスを扱うよりも簡単で便利に処理を記述することができます。  
使い方を知るためには[公式ドキュメント](https://docs.python.org/ja/3/library/pathlib.html)を読むのが良いのですが、(私が)使わないメソッドも多く記載されているので本記事ではよく使うメソッドに絞って紹介します。

# 環境
### ディレクトリ構造
紹介の前に環境のお話です。今回は以下のような構造を対象として処理結果を例示します。
`data`には年月日を`yymmdd`形式で表現したデータが格納されているとイメージしてください。また、一部`datetime`を使ったほうが良い表現もありますが、今回は`pathlib`の説明のため使いません。
```
C:\pathlib_demo>tree /f
C:.
├─data
│  ├─230101
│  ├─230201
│  │      demo1.csv
│  │      demo1.tsv
│  │
│  ├─230202
│  │      demo1.csv
│  │      demo2.csv
│  │
│  ├─230203
│  └─230211
└─pathlib_demo
        test.py
```

ちなみにこのディレクトリ構造は以下のコードを実行することで生成できます。
せっかくなのでpathlibで行っています。各処理で使用しているメソッドの解説は下の方で行います。

```python
from pathlib import Path

# パスオブジェクトを生成
project_path = Path("C:\pathlib_demo")
data_path = project_path / "data"
program_path = project_path / "pathlib_demo"

# フォルダ生成
for i in ["101", "201", "202", "203", "210", "211"]:
    (data_path / f"230{i}").mkdir(parents=True)
program_path.mkdir(parents=True)

# ファイル生成
(data_path / "230201" / "demo1.csv").touch()
(data_path / "230201" / "demo1.tsv").touch()
(data_path / "230202" / "demo1.csv").touch()
(data_path / "230202" / "demo2.csv").touch()
(program_path / "test.py").touch()
```
:::note info
以降のコードについては`test.py`に記述すると同じ結果が得られます。
:::
### インストール
python3.4以降では標準ライブラリなのでインストールは不要です。
以下のようにメインクラスをインポートします。
```python
from pathlib import Path
```
:::note warn
バージョンによって挙動が異なるので環境には注意しましょう。
:::

# コードの紹介
### 実行中のプログラムのファイルパスを取得する
`__file__`を使うことでどこから呼び出しても自分の位置を知ることができます。
```python
current_file_path = Path(__file__)
print(current_file_path)
> c:\pathlib_demo\pathlib_demo\test.py
```

### 実行中のプログラムのフォルダパスを取得する
os.pathのような文字列で絶対パスを指定すると作業環境を移動させたときに不具合が出ますが、これなら安心です。
```python
current_folder_path = Path(__file__).parent
print(current_folder_path)
> c:/pathlib_demo/pathlib_demo
```
:::note info
jupyterの場合は`__file__`が使えませんが別の手段で同じことができます。
```python:jupyterの場合
import os
current_folder_path = Path(os.path.abspath(" ")).parent
print(current_folder_path)
> c:/pathlib_demo/pathlib_demo
```
:::

### 上の階層にアクセスする
フォルダパスを取得したときに使用した`parent`はパスを一つ上の階層に移動することができます。
```python
project_path = current_folder_path.parent
print(project_path)
> c:\pathlib_demo
```
`parent`は繰り返し使用することで、さらに上の階層に移動することができます。
しかし、可読性が下がるのでおすすめはしません。
```python
print(current_folder_path.parent) 
print(current_folder_path.parent.parent) 
> c:\pathlib_demo
> c:\
```

`parent`を繰り返し使いたい場合は`parents`を使うとすっきりと書けます。
インデックスに指定した値+1個上の階層に移動します。
```python
print(current_folder_path)
print(current_folder_path.parents[0])
print(current_folder_path.parents[1])
> c:\pathlib_demo\pathlib_demo
> c:\pathlib_demo
> c:\
```

### パスを指定する
`/`と文字列で指定することが出来ます。直感的で分かりやすいですね。
```python
data_path = project_path / "data"
print(data_path)
> c:\pathlib_demo\data
```
osの違いを吸収してくれるのも嬉しいポイントです。
文字列でパスを扱う場合は区切りとしてwindowsでは`\`、Linuxでは`/`を使うため厄介ですがpathlibを使うことでユーザーはその違いを考える必要がなくなります。
これによりコードも共通化することができます。
```python: windows
current_path = Path(__file__).parent
data_path = current_path / "data" 
print(data_path)
print(type(data_path))
> c:\pathlib_demo\data
> <class 'pathlib.WindowsPath'>
```
```python: Linux
current_path = Path(__file__).parent
data_path = current_path / "data" 
print(data_path)
print(type(data_path))
> /root/data
> <class 'pathlib.PosixPath'>  # Linuxの場合は自動的に非windowsパスを表すサブクラスになります
```

### ディレクトリ配下のパスを取得する
pathlibオブジェクトでは`glob`を使用することができます。
`"*"`で指定するとディレクトリ配下の全てのパスを返してくれます。
```python:
for i in data_path.glob("*"):
    print(i)
> c:\pathlib_demo\data\230201
> c:\pathlib_demo\data\230202
> c:\pathlib_demo\data\230203
> c:\pathlib_demo\data\230211
```
`glob`の戻り値は`generator`です。`list`として扱いたい場合は`list()`をつけて下さい。
```python:
from pprint import pprint  # listの出力は横に広がって見にくいためpprintを使用
l = list(data_path.glob("*"))  
pprint(l)  # WindowsPathはWindows用のパスであることを示します。使用の際は深く考えなくて良いです。
> [WindowsPath('c:/pathlib_demo/data/230201'),
   WindowsPath('c:/pathlib_demo/data/230202'),
   WindowsPath('c:/pathlib_demo/data/230203'),
   WindowsPath('c:/pathlib_demo/data/230211')]
```
`glob("*")`を使わなくても`data_path.iterdir()`でも同じ結果を得ることができます。
処理目的を明示する場合は後者を使った方が良いかもしれませんが、覚えることを減らす目的から他の用途にも使える`glob`で紹介をしました。

### パターン検索でパスを取得する
`glob`は与える引数によって取得するパスを指定することができます。
といっても覚えるのは`?`, `*`,  `[]`の三種です。

#### 曖昧な検索ができる"?"と"*"
`?`は **任意の文字(1文字分)** の代わりになります。一文字分曖昧に検索ができると考えると良いです。 ちなみにさきほどから使用している`*`は **任意の文字列(適当な文字数)** の代わりになります。そういった理由で`glob("*")`は全てのパスを取得するパターンとなります。
```python:曖昧な検索
pprint(list(data_path.glob("23020?")))  # 2月の上旬
> [WindowsPath('c:/pathlib_demo/data/230201'),
   WindowsPath('c:/pathlib_demo/data/230202'),
   WindowsPath('c:/pathlib_demo/data/230203')]

pprint(list(data_path.glob("2302?1")))  # 2月の1のつく日
> [WindowsPath('c:/pathlib_demo/data/230201'),
   WindowsPath('c:/pathlib_demo/data/230211')]

pprint(list(data_path.glob("2302*")))  # 2月
> [WindowsPath('c:/pathlib_demo/data/230201'),
   WindowsPath('c:/pathlib_demo/data/230202'),
   WindowsPath('c:/pathlib_demo/data/230203'),
   WindowsPath('c:/pathlib_demo/data/230211')]
```

#### 厳密な検索ができる"[]"
`?`は任意の一文字でしたが、`[]`は指定した一文字を表します。
`[12]`とすれば1か2が対象になります。連番の場合は`[1-2]`とすると可読性が上がります。
アルファベットの場合も同様に連番を指定することができます。例えば`[a-c]`と指定するとa,b,cが対象になります。
`[!]`を使うとその文字以外が指定されます。
```python:厳密な検索
pprint(list(data_path.glob("23020[1-2]")))  # 1日と2日
> [WindowsPath('c:/pathlib_demo/data/230201'),
   WindowsPath('c:/pathlib_demo/data/230202')]

pprint(list(data_path.glob("23020[!1-2]")))  # 2月上旬の1日と2日以外
> [WindowsPath('c:/pathlib_demo/data/230203')]

# *と組み合わせることもできます
pprint(list(data_path.glob("23*[1-2]")))  # 2023年の任意の月の1日と2日
> [WindowsPath('c:/pathlib_demo/data/230203')]
```
:::note warn
"[]"を使った指定方法は`re`モジュールによる正規表現に似ていますが挙動が異なります。
globのパターン指定については[fnmatchのドキュメント](https://docs.python.org/ja/3/library/fnmatch.html)を参照してください。
:::

パターン検索についてまとめたものがこちらです。
| パターン | 文字数 | 対象 |
|:-:|:-:|:-:|
| ? | 一文字 | 任意 |
| * | 複数文字  | 任意 |
| [文字] | 一文字 | 指定した文字 |
| [!文字] | 一文字 | 指定した文字以外 |

### 再帰的に取得する
ディレクトリを指定した上でさらに下層のパスを取得することができます。
```python
print(list(data_path.glob("230201/*")))  # 2月1日の下層
> [WindowsPath('c:/pathlib_demo/data/230201/demo1.csv'),
   WindowsPath('c:/pathlib_demo/data/230201/demo1.tsv')]
```
:::note info
Linuxについても同様の指定方法で取得できます。
:::

ファイル名がある層まで到達しました。
指定した拡張子のみを検索する場合は、さきほど紹介した`*`を使ってファイル名の部分を曖昧に検索してあげれば良いです。
```python:
pprint(list(data_path.glob("230201/*.tsv")))
> [WindowsPath('c:/pathlib_demo/data/230201/demo1.tsv')]
```

### ディレクトリ生成
`mkdir`メソッドを使用することでディレクトリを生成できます。
```python
tests_path = project_path / "tests"
tests_path.mkdir()
# c:\pathlib_demo\testsが生成されます

(project_path / "tests").mkdir()  # こちらも同じ挙動をします。
```
`mkdir`メソッドは引数を指定することで便利に使うことができます。

#### exits_ok
`mkdir`の対象ディレクトリが存在する場合エラーが発生しますが、`exist_ok`引数を使用することでエラーを発生させないようにすることができます。
```python:
tests_path.mkdir()
> FileExistsError: [WinError 183] 既に存在するファイルを作成することはできません。

tests_path.mkdir(exist_ok=True)
# 既に存在しているディレクトリを生成しようとしても何も起こりません。
```

#### parents
もうひとつの引数は`parents`引数です。これにより生成するパスの中間にあるディレクトリが存在しない場合、そのディレクトリを自動的に生成してくれるようになります。
```python:
new_path = project_path / "new" / "dir"
new_path.mkdir()
# FileNotFoundError: [WinError 3] 指定されたパスが見つかりません。  # newというディレクトリが存在しないためエラーが発生します。

new_path.mkdir(parents=True)
# c:\pathlib_demo\new\dirが生成されます。
```

# pathlibのいまいちなところ
ここまで紹介したように非常に便利なpathlibですが、いまいちなところもあります。
とはいえ、pathlibを使わないレベルの問題はないです。

### \:warning:削除できるのは空のディレクトリのみ
`rmdir`メソッドを使用することで空のディレクトリであれば削除することができます。
しかし、削除対象のディレクトリ配下に何かデータがある場合はエラーが発生します。
```python:
(data_path / "230201").rmdir()  # 230201の配下にはファイルが2つ格納されている
# OSError: [WinError 145] ディレクトリが空ではありません。
```
:::note info
空ではないディレクトリを削除する場合は`shutil.rmtree()`を使用します。
```python:
import shutil
shutil.rmtree(data_path / "230201")
# 230201が配下のファイルごと削除されます
```
:::

### \:warning:使用できないライブラリがある
すべてのライブラリで`pathlib.Path`オブジェクトを使用できるわけではありません。
例えば`pillow`ではファイルを指定するときに`pathlib.Path`オブジェクトを渡すことができますが、`openCV`ではエラーが発生します。
```python
from PIL import Image
img_path = project_path / "img.jpg"
img = Image.open(img_path)
# エラー発生無し

import cv2
img = cv2.imread(img_path)
# TypeError: Can't convert object to 'str' for 'filename'
```
公式ドキュメントや`docstring`を読めば渡すことができる、できないの判断はできます。

:::note info
`pathlib.Path`オブジェクトを渡すことができないライブラリでも、組み込み関数の`str()`で文字列に変換することで渡すことができます。
```python
img = cv2.imread(str(img_path))
```
:::

### IDEでパスの候補が表示されない
私はIDEとして普段VSCodeを使用しています。文字列であればパスを入力するときに拡張機能`Path Autocomplete`を使うことで簡単に入力ができますが、`pathlib`の記法の場合はそれが効かなくなります。効率やタイポ対策の面を考えると少し不便です。
![tempsnip.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314095/ef016a68-f6dd-1b22-2ceb-280afca85f6f.png)

# おわりに
今回紹介できませんでしたが、`pathlib`には他にも便利なメソッドが多くあります。慣れればメリットが盛り沢山なのでぜひお使い下さい。