4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

pathlibのよく使う処理まとめ

Last updated at Posted at 2023-02-11

<windows><python>

はじめに

pathlibはパスを表すクラスを提供してくれます。このクラスには様々なメソッドが備わっており、os.pathのように文字でパスを扱うよりも簡単で便利に処理を記述することができます。
使い方を知るためには公式ドキュメントを読むのが良いのですが、(私が)使わないメソッドも多く記載されているので本記事ではよく使うメソッドに絞って紹介します。

環境

ディレクトリ構造

紹介の前に環境のお話です。今回は以下のような構造を対象として処理結果を例示します。
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で行っています。各処理で使用しているメソッドの解説は下の方で行います。

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()

以降のコードについてはtest.pyに記述すると同じ結果が得られます。

インストール

python3.4以降では標準ライブラリなのでインストールは不要です。
以下のようにメインクラスをインポートします。

from pathlib import Path

バージョンによって挙動が異なるので環境には注意しましょう。

コードの紹介

実行中のプログラムのファイルパスを取得する

__file__を使うことでどこから呼び出しても自分の位置を知ることができます。

current_file_path = Path(__file__)
print(current_file_path)
> c:\pathlib_demo\pathlib_demo\test.py

実行中のプログラムのフォルダパスを取得する

os.pathのような文字列で絶対パスを指定すると作業環境を移動させたときに不具合が出ますが、これなら安心です。

current_folder_path = Path(__file__).parent
print(current_folder_path)
> c:/pathlib_demo/pathlib_demo

jupyterの場合は__file__が使えませんが別の手段で同じことができます。

jupyterの場合
import os
current_folder_path = Path(os.path.abspath(" ")).parent
print(current_folder_path)
> c:/pathlib_demo/pathlib_demo

上の階層にアクセスする

フォルダパスを取得したときに使用したparentはパスを一つ上の階層に移動することができます。

project_path = current_folder_path.parent
print(project_path)
> c:\pathlib_demo

parentは繰り返し使用することで、さらに上の階層に移動することができます。
しかし、可読性が下がるのでおすすめはしません。

print(current_folder_path.parent) 
print(current_folder_path.parent.parent) 
> c:\pathlib_demo
> c:\

parentを繰り返し使いたい場合はparentsを使うとすっきりと書けます。
インデックスに指定した値+1個上の階層に移動します。

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:\

パスを指定する

/と文字列で指定することが出来ます。直感的で分かりやすいですね。

data_path = project_path / "data"
print(data_path)
> c:\pathlib_demo\data

osの違いを吸収してくれるのも嬉しいポイントです。
文字列でパスを扱う場合は区切りとしてwindowsでは\、Linuxでは/を使うため厄介ですがpathlibを使うことでユーザーはその違いを考える必要がなくなります。
これによりコードも共通化することができます。

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'>
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を使用することができます。
"*"で指定するとディレクトリ配下の全てのパスを返してくれます。

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()をつけて下さい。

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("*")は全てのパスを取得するパターンとなります。

曖昧な検索
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が対象になります。
[!]を使うとその文字以外が指定されます。

厳密な検索
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')]

"[]"を使った指定方法はreモジュールによる正規表現に似ていますが挙動が異なります。
globのパターン指定についてはfnmatchのドキュメントを参照してください。

パターン検索についてまとめたものがこちらです。

パターン 文字数 対象
? 一文字 任意
* 複数文字 任意
[文字] 一文字 指定した文字
[!文字] 一文字 指定した文字以外

再帰的に取得する

ディレクトリを指定した上でさらに下層のパスを取得することができます。

print(list(data_path.glob("230201/*")))  # 2月1日の下層
> [WindowsPath('c:/pathlib_demo/data/230201/demo1.csv'),
   WindowsPath('c:/pathlib_demo/data/230201/demo1.tsv')]

Linuxについても同様の指定方法で取得できます。

ファイル名がある層まで到達しました。
指定した拡張子のみを検索する場合は、さきほど紹介した*を使ってファイル名の部分を曖昧に検索してあげれば良いです。

pprint(list(data_path.glob("230201/*.tsv")))
> [WindowsPath('c:/pathlib_demo/data/230201/demo1.tsv')]

ディレクトリ生成

mkdirメソッドを使用することでディレクトリを生成できます。

tests_path = project_path / "tests"
tests_path.mkdir()
# c:\pathlib_demo\testsが生成されます

(project_path / "tests").mkdir()  # こちらも同じ挙動をします。

mkdirメソッドは引数を指定することで便利に使うことができます。

exits_ok

mkdirの対象ディレクトリが存在する場合エラーが発生しますが、exist_ok引数を使用することでエラーを発生させないようにすることができます。

tests_path.mkdir()
> FileExistsError: [WinError 183] 既に存在するファイルを作成することはできません

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

parents

もうひとつの引数はparents引数です。これにより生成するパスの中間にあるディレクトリが存在しない場合、そのディレクトリを自動的に生成してくれるようになります。

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メソッドを使用することで空のディレクトリであれば削除することができます。
しかし、削除対象のディレクトリ配下に何かデータがある場合はエラーが発生します。

(data_path / "230201").rmdir()  # 230201の配下にはファイルが2つ格納されている
# OSError: [WinError 145] ディレクトリが空ではありません。

空ではないディレクトリを削除する場合はshutil.rmtree()を使用します。

import shutil
shutil.rmtree(data_path / "230201")
# 230201が配下のファイルごと削除されます

:warning:使用できないライブラリがある

すべてのライブラリでpathlib.Pathオブジェクトを使用できるわけではありません。
例えばpillowではファイルを指定するときにpathlib.Pathオブジェクトを渡すことができますが、openCVではエラーが発生します。

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を読めば渡すことができる、できないの判断はできます。

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

img = cv2.imread(str(img_path))

IDEでパスの候補が表示されない

私はIDEとして普段VSCodeを使用しています。文字列であればパスを入力するときに拡張機能Path Autocompleteを使うことで簡単に入力ができますが、pathlibの記法の場合はそれが効かなくなります。効率やタイポ対策の面を考えると少し不便です。
tempsnip.png

おわりに

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

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?