Python 3のpathlibモジュール:ファイルシステムを使いこなす

Python 3のpathlibモジュール:ファイルシステムを使いこなす

Pythonでのファイルパス処理に苦労しましたか? Python 3.4以降では、闘争は終わりました! 次のようなコードに頭を悩ます必要はもうありません。

>>>

>>> path.rsplit('\\', maxsplit=1)[0]

または:

>>>

>>> os.path.isfile(os.path.join(os.path.expanduser('~'), 'realpython.txt'))

このチュートリアルでは、Pythonでファイルパス(ディレクトリとファイルの名前)を操作する方法を説明します。 ファイルを読み書きする新しい方法、パスと基になるファイルシステムを操作する方法、およびファイルを一覧表示してそれらを反復処理する方法の例を参照します。 `+ pathlib +`モジュールを使用すると、上記の2つの例は、次のようなエレガントで読みやすいPythonコードを使用して書き換えることができます。

>>>

>>> path.parent
>>> (pathlib.Path.home()/'realpython.txt').is_file()

無料のPDFダウンロード: Python 3 Cheat Sheet

Pythonファイルパス処理の問題

ファイルの操作とファイルシステムの操作は、さまざまな理由で重要です。 最も単純なケースは、ファイルの読み取りまたは書き込みのみを伴う場合がありますが、時にはより複雑なタスクが手元にあります。 特定のタイプのディレクトリ内のすべてのファイルをリストするか、特定のファイルの親ディレクトリを見つけるか、まだ存在しない一意のファイル名を作成する必要がある場合があります。

従来、Pythonは通常のテキスト文字列を使用してファイルパスを表現していました。 https://docs.python.org/3/library/os.path.html [+ os.path +]標準ライブラリのサポートにより、これは適切でしたが、少し面倒ですが(導入の2番目の例として)ショー)。 ただし、https://snarky.ca/why-pathlib-path-doesn-t-inherit-from-str/[パスは文字列ではない]であるため、https:/などのライブラリを含む標準ライブラリ全体に重要な機能が広がっています。/docs.python.org/3/library/os.html [+ os +]、https://docs.python.org/3/library/glob.html [+ glob +]、およびhttps://docs.python.org/3/library/shutil.html [+ shutil +]。 次の例では、すべてのテキストファイルをアーカイブディレクトリに移動するために、3つの「+ import +」ステートメントが必要です。

import glob
import os
import shutil

for file_name in glob.glob('*.txt'):
    new_path = os.path.join('archive', file_name)
    shutil.move(file_name, new_path)

文字列で表されるパスでは、通常の文字列メソッドを使用することは可能ですが、通常は悪い考えです。 たとえば、通常の文字列のように2つのパスを `+`で結合する代わりに、オペレーティングシステムの正しいパス区切り文字を使用してパスを結合する `+ os.path.join()`を使用する必要があります。 Windowsは ` \ `を使用し、MacおよびLinuxは `/+`を区切り文字として使用することを思い出してください。 この違いは、Windowsパスのみで機能する導入部の最初の例のように、見つけにくいエラーにつながる可能性があります。

これらの課題に対処するために、 `+ pathlib `モジュールはPython 3.4(https://www.python.org/dev/peps/pep-0428/[PEP 428])で導入されました。 必要な機能を1か所に集めて、使いやすい ` Path +`オブジェクトのメソッドとプロパティから利用できるようにします。

早い段階で、他のパッケージはまだファイルパスに文字列を使用していましたが、Python 3.6の時点では、一部のhttps://www.python.org/dev/の追加により、標準ライブラリ全体で `+ pathlib +`モジュールがサポートされていますpeps/pep-0519/[ファイルシステムパスプロトコル]。 レガシーPythonにこだわっている場合は、https://github.com/mcmtroffaes/pathlib2 [Python 2のバックポートが利用可能]もあります。

アクションの時間:実際に `+ pathlib +`がどのように機能するかを見てみましょう。

パスを作成する

本当に知っておく必要があるのは、 + pathlib.Path +`クラスだけです。 パスを作成する方法はいくつかあります。 まず、https://realpython.com/instance-class-and-static-methods-demystified/[classmethods like] `+ .cwd()+(現在の作業ディレクトリ)と `+ .home()があります。 + `(ユーザーのホームディレクトリ):

>>>

>>> import pathlib
>>> pathlib.Path.cwd()
PosixPath('/home/gahjelle/realpython/')

_ *注意:*このチュートリアルでは、上記のように `+ import pathlib `のスペルを記述せずに、 ` pathlib `がインポートされていると仮定します。 主に ` Path `クラスを使用するため、 ` from pathlib import Path `を実行し、 ` pathlib.Path `の代わりに ` Path +`を記述することもできます。 _

パスは、文字列表現から明示的に作成することもできます。

>>>

>>> pathlib.Path(r'C:\Users\gahjelle\realpython\file.txt')
WindowsPath('C:/Users/gahjelle/realpython/file.txt')

Windowsパスを扱うための小さなヒント:Windowsでは、パス区切り文字はバックスラッシュ、「+ \ 」です。 ただし、多くのコンテキストでは、バックスラッシュは印刷不能文字を表すために_エスケープ文字_としても使用されます。 問題を回避するには、_raw文字列リテラル_を使用してWindowsパスを表します。 これらは、先頭に「 r 」が付加された文字列リテラルです。 生の文字列リテラルでは、 ` \ `はリテラルバックスラッシュを表します: ` r’C:\ Users '+`。

パスを作成する3番目の方法は、特別な演算子「/」を使用してパスの部分を結合することです。 スラッシュ演算子は、プラットフォーム上の実際のパス区切り文字とは無関係に使用されます。

>>>

>>> pathlib.Path.home()/'python'/'scripts'/'test.py'
PosixPath('/home/gahjelle/python/scripts/test.py')

`/`は、少なくとも1つの `+ Path `オブジェクトがある限り、複数のパスまたはパスと文字列の組み合わせ(上記のように)を結合できます。 特別な `/`表記が気に入らない場合は、 ` .joinpath()+`メソッドで同じことを行うことができます。

>>>

>>> pathlib.Path.home().joinpath('python', 'scripts', 'test.py')
PosixPath('/home/gahjelle/python/scripts/test.py')

上記の例では、 `+ pathlib.Path `は ` WindowsPath `または ` PosixPath `で表されていることに注意してください。 パスを表す実際のオブジェクトは、基盤となるオペレーティングシステムによって異なります。 (つまり、 ` WindowsPath `の例はWindowsで実行され、 ` PosixPath +`の例はMacまたはLinuxで実行されています。)詳細については、リンク:#operating-system-differences [Operating System Differences]を参照してください。 。

ファイルの読み書き

従来、Pythonでファイルを読み書きする方法は、組み込みの `+ open()`関数を使用することでした。 ` open()`関数は ` Path +`オブジェクトを直接使用できるため、これはまだ当てはまります。 次の例では、Markdownファイル内のすべてのヘッダーを検索して出力します。

path = pathlib.Path.cwd()/'test.md'
with open(path, mode='r') as fid:
    headers = [line.strip() for line in fid if line.startswith('#')]
print('\n'.join(headers))

同等の代替手段は、 `+ Path `オブジェクトで ` .open()+`を呼び出すことです:

with path.open(mode='r') as fid:
    ...

実際、 `+ Path.open()`は組み込みの ` open()+`を舞台裏で呼び出しています。 どのオプションを使用するかは、主に好みの問題です。

ファイルの読み書きを簡単にするために、 `+ pathlib +`ライブラリにはいくつかの便利なメソッドがあります:

  • + .read_text()+:テキストモードでパスを開き、内容を文字列として返します。

  • + .read_bytes()+:パスをバイナリ/バイトモードで開き、内容をバイト文字列として返します。

  • + .write_text()+:パスを開き、それに文字列データを書き込みます。

  • + .write_bytes()+:パスをバイナリ/バイトモードで開き、データを書き込みます。

これらの各メソッドは、ファイルのオープンとクローズを処理するため、使用が簡単になります。たとえば、

>>>

>>> path = pathlib.Path.cwd()/'test.md'
>>> path.read_text()
<the contents of the test.md-file>

パスは、単純なファイル名として指定することもできます。この場合、パスは現在の作業ディレクトリを基準にして解釈されます。 次の例は、前の例と同等です。

>>>

>>> pathlib.Path('test.md').read_text()
<the contents of the test.md-file>

`+ .resolve()+`メソッドは完全なパスを見つけます。 以下では、現在の作業ディレクトリが単純なファイル名に使用されていることを確認します。

>>>

>>> path = pathlib.Path('test.md')
>>> path.resolve()
PosixPath('/home/gahjelle/realpython/test.md')
>>> path.resolve().parent == pathlib.Path.cwd()
False

パスが比較されるとき、比較されるのはそれらの表現であることに注意してください。 上記の例では、「+ path.parent 」は「 pathlib.Path 」で表されるため、「 path.parent 」は「 pathlib.Path.cwd()」と等しくありません。 .cwd() + '/home/gahjelle/realpython/' + `で表されます。

パスのコンポーネントを選択する

パスのさまざまな部分は、プロパティとして便利に利用できます。 基本的な例は次のとおりです。

  • + .name +:ディレクトリなしのファイル名

  • + .parent +:ファイルを含むディレクトリ、またはパスがディレクトリの場合は親ディレクトリ

  • + .stem +:接尾辞なしのファイル名

  • + .suffix +:ファイル拡張子

  • + .anchor +:パスのディレクトリの前の部分

動作中のこれらのプロパティは次のとおりです。

>>>

>>> path
PosixPath('/home/gahjelle/realpython/test.md')
>>> path.name
'test.md'
>>> path.stem
'test'
>>> path.suffix
'.md'
>>> path.parent
PosixPath('/home/gahjelle/realpython')
>>> path.parent.parent
PosixPath('/home/gahjelle')
>>> path.anchor
'/'

`+ .parent `は新しい ` Path `オブジェクトを返しますが、他のプロパティは文字列を返します。 これは、例えば、最後の例のように ` .parent `を連鎖させたり、 `/+`と組み合わせて完全に新しいパスを作成したりできることを意味します。

>>>

>>> path.parent.parent/('new' + path.suffix)
PosixPath('/home/gahjelle/new.md')

優れたhttps://github.com/chris1610/pbpython/blob/master/extras/Pathlib-Cheatsheet.pdf[Pathlib Cheatsheet]は、これらおよびその他のプロパティとメソッドの視覚的表現を提供します。

ファイルの移動と削除

`+ pathlib +`を通じて、ファイルの移動、更新、削除などの基本的なファイルシステムレベルの操作にもアクセスできます。 ほとんどの場合、これらのメソッドは、情報やファイルが失われる前に警告を表示したり、確認を待ったりしません。 これらの方法を使用するときは注意してください。

ファイルを移動するには、 `+ .replace()`を使用します。 宛先がすでに存在する場合、 ` .replace()`がそれを上書きすることに注意してください。 残念ながら、 ` pathlib +`はファイルの安全な移動を明示的にサポートしていません。 宛先パスの上書きを避けるために、最も簡単なのは、置換する前に宛先が存在するかどうかをテストすることです。

if not destination.exists():
    source.replace(destination)

ただし、競合状態が発生する可能性があるため、ドアは開いたままになります。 別のプロセスは、 `+ if `ステートメントの実行と ` .replace()`メソッドの間の ` destination +`パスにファイルを追加する場合があります。 それが懸念される場合、より安全な方法はhttps://docs.python.org/3/library/functions.html#open[exclusive creation]の宛先パスを開き、ソースデータを明示的にコピーすることです。

with destination.open(mode='xb') as fid:
    fid.write(source.read_bytes())

上記のコードは、 `+ destination `が既に存在する場合、 ` FileExistsError `を発生させます。 技術的には、これによりファイルがコピーされます。 移動を実行するには、コピーが完了した後に「 source +」を削除するだけです(以下を参照)。 ただし、例外が発生していないことを確認してください。

ファイルの名前を変更する場合、便利なメソッドは `+ .with_name()`と ` .with_suffix()+`です。 どちらも元のパスを返しますが、それぞれ名前またはサフィックスが置き換えられます。

例えば:

>>>

>>> path
PosixPath('/home/gahjelle/realpython/test001.txt')
>>> path.with_suffix('.py')
PosixPath('/home/gahjelle/realpython/test001.py')
>>> path.replace(path.with_suffix('.py'))

ディレクトリとファイルは、それぞれ `+ .rmdir()`と ` .unlink()+`を使用して削除できます。 (再度、注意してください!)

このセクションでは、 `+ pathlib +`を使用して簡単な課題に対処する方法の例をいくつか示します。

ファイルのカウント

多くのファイルをリストするには、いくつかの異なる方法があります。 最も単純なのは `+ .iterdir()`メソッドで、指定されたディレクトリ内のすべてのファイルを反復処理します。 次の例では、 ` .iterdir()`と ` collections.Counter +`クラスを組み合わせて、現在のディレクトリに各ファイルタイプのファイル数をカウントします。

>>>

>>> import collections
>>> collections.Counter(p.suffix for p in pathlib.Path.cwd().iterdir())
Counter({'.md': 2, '.txt': 4, '.pdf': 2, '.py': 1})

メソッド + .glob()+`および `+ .rglob()+(再帰的glob)を使用すると、より柔軟なファイルリストを作成できます。 たとえば、 `+ pathlib.Path.cwd()。glob( '*。txt')`は、現在のディレクトリにある接尾辞が ` .txt `のすべてのファイルを返します。 以下は、 ` p +`で始まるファイルタイプのみをカウントします:

>>>

>>> import collections
>>> collections.Counter(p.suffix for p in pathlib.Path.cwd().glob('*.p*'))
Counter({'.pdf': 2, '.py': 1})

ディレクトリツリーを表示する

次の例では、 `+ tree()`という関数を定義します。この関数は、指定されたディレクトリをルートとするファイル階層を表す視覚的なツリーを出力します。 ここでは、サブディレクトリもリストしたいので、 ` .rglob()+`メソッドを使用します。

def tree(directory):
    print(f'+ {directory}')
    for path in sorted(directory.rglob('*')):
        depth = len(path.relative_to(directory).parts)
        spacer = '    ' * depth
        print(f'{spacer}+ {path.name}')

ファイルがルートディレクトリからどれだけ離れているかを知る必要があることに注意してください。 これを行うには、まず `+ .relative_to()`を使用して、ルートディレクトリからの相対パスを表します。 次に、表現内のディレクトリの数をカウントします( ` .parts +`プロパティを使用)。 この関数を実行すると、次のようなビジュアルツリーが作成されます。

>>>

>>> tree(pathlib.Path.cwd())
+/home/gahjelle/realpython
    + directory_1
        + file_a.md
    + directory_2
        + file_a.md
        + file_b.pdf
        + file_c.py
    + file_1.txt
    + file_2.txt

_ 注意: f-stringsはPython 3.6以降でのみ機能します。 古いPythonでは、式「+ f」{spacer} + {path.name} '+ は、「+' {0} + {1} '。format(spacer、path.name)+」と記述できます。 _

最終変更ファイルを見つける

+ .iterdir()++ .glob()+、および `+ .rglob()`メソッドは、ジェネレーター式およびリスト内包表記に最適です。 最後に変更されたディレクトリでファイルを見つけるには、 ` .stat()`メソッドを使用して、基礎となるファイルに関する情報を取得できます。 たとえば、 ` .stat()。st_mtime +`はファイルの最終変更時刻を提供します:

>>>

>>> from datetime import datetime
>>> time, file_path = max((f.stat().st_mtime, f) for f in directory.iterdir())
>>> print(datetime.fromtimestamp(time), file_path)
2018-03-23 19:23:56.977817/home/gahjelle/realpython/test001.txt

同様の式で最後に変更されたファイルの内容を取得することもできます。

>>>

>>> max((f.stat().st_mtime, f) for f in directory.iterdir())[1].read_text()
<the contents of the last modified file in directory>

さまざまな `+ .stat()。st_ `プロパティから返されるタイムスタンプは、1970年1月1日からの秒数を表します。 「 datetime.fromtimestamp 」に加えて、「 time.localtime 」または「 time.ctime +」を使用して、タイムスタンプをより使いやすいものに変換できます。

一意のファイル名を作成する

最後の例では、テンプレートに基づいて一意の番号付きファイル名を作成する方法を示します。 まず、ファイル名のパターンを指定し、カウンター用のスペースを確保します。 次に、ディレクトリとファイル名を結合して作成されたファイルパスの存在を確認します(カウンターの値を使用)。 既に存在する場合は、カウンターを増やして再試行してください。

def unique_path(directory, name_pattern):
    counter = 0
    while True:
        counter += 1
        path = directory/name_pattern.format(counter)
        if not path.exists():
            return path

path = unique_path(pathlib.Path.cwd(), 'test{:03d}.txt')

ディレクトリにファイル「+ test001.txt 」と「 test002.txt 」がすでに含まれている場合、上記のコードは「 path 」を「 test003.txt +」に設定します。

オペレーティングシステムの違い

以前、 `+ pathlib.Path `をインスタンス化したときに、 ` WindowsPath `または ` PosixPath `オブジェクトが返されたことに注意しました。 オブジェクトの種類は、使用しているオペレーティングシステムによって異なります。 この機能により、クロスプラットフォーム互換のコードを簡単に作成できます。 ` WindowsPath `または ` PosixPath +`を明示的に要求することは可能ですが、そのシステムにコードを制限するだけで、メリットはありません。 このような具体的なパスは、別のシステムでは使用できません。

>>>

>>> pathlib.WindowsPath('test.md')
NotImplementedError: cannot instantiate 'WindowsPath' on your system

基礎となるファイルシステムにアクセスせずにパスの表現が必要な場合があります(この場合、Windows以外のシステムでWindowsパスを表すことも、その逆も意味があります)。 これは、 `+ PurePath +`オブジェクトを使用して実行できます。 これらのオブジェクトは、link:#picking-out-components-of-a-path [Path Components]セクションで説明されている操作をサポートしていますが、ファイルシステムにアクセスするメソッドはサポートしていません。

>>>

>>> path = pathlib.PureWindowsPath(r'C:\Users\gahjelle\realpython\file.txt')
>>> path.name
'file.txt'
>>> path.parent
PureWindowsPath('C:/Users/gahjelle/realpython')
>>> path.exists()
AttributeError: 'PureWindowsPath' object has no attribute 'exists'

すべてのシステムで `+ PureWindowsPath `または ` PurePosixPath `を直接インスタンス化できます。 ` PurePath +`をインスタンス化すると、使用しているオペレーティングシステムに応じてこれらのオブジェクトのいずれかが返されます。

適切なオブジェクトとしてのパス

link:#the-problem-with-python-file-path-handling [introduction]で、パスは文字列ではないことを簡単に説明しました。 実際、https://docs.python.org/3/library/pathlib.html [+ pathlib +`の公式ドキュメント]は_ + pathlib + `—オブジェクト指向ファイルシステムパス_というタイトルです。 Object-oriented approachは、上記の例ですでによく見られます(特に、古い `+ os.path +`のやり方と対比する場合) )。 ただし、他にもいくつかのヒントを残しておきます。

使用しているオペレーティングシステムとは関係なく、パスはPosixスタイルで表され、パスセパレーターとしてスラッシュが使用されます。 Windowsでは、次のようなものが表示されます。

>>>

>>> pathlib.Path(r'C:\Users\gahjelle\realpython\file.txt')
WindowsPath('C:/Users/gahjelle/realpython/file.txt')

それでも、パスが文字列に変換されると、たとえばWindowsのバックスラッシュを含むネイティブ形式が使用されます。

>>>

>>> str(pathlib.Path(r'C:\Users\gahjelle\realpython\file.txt'))
'C:\\Users\\gahjelle\\realpython\\file.txt'

これは、 + pathlib.Path +`オブジェクトの処理方法がわからないライブラリを使用している場合に特に便利です。 これは、3.6より前のPythonバージョンではより大きな問題です。 たとえば、Python 3.5では、https://docs.python.org/3/library/configparser.html [+ configparser +`標準ライブラリ]は文字列パスのみを使用してファイルを読み取ります。 そのような場合を処理する方法は、文字列への変換を明示的に行うことです。

>>>

>>> from configparser import ConfigParser
>>> path = pathlib.Path('config.txt')
>>> cfg = ConfigParser()
>>> cfg.read(path)                     # Error on Python < 3.6
TypeError: 'PosixPath' object is not iterable
>>> cfg.read(str(path))                # Works on Python >= 3.4
['config.txt']

Python 3.6以降では、明示的な変換を行う必要がある場合は、「+ str()」ではなく「 os.fspath()+」を使用することをお勧めします。 pathlike以外のオブジェクトを誤って変換しようとするとエラーが発生するため、これは少し安全です。

おそらく `+ pathlib `ライブラリの最も珍しい部分は `/`演算子の使用です。 内部を少し覗いてみましょう、それがどのように実装されているか見てみましょう。 これは演算子のオーバーロードの例です。演算子の動作はコンテキストに応じて変更されます。 あなたはこれを見たことがあります。 「++」が文字列と数値に対して異なる意味を持つことを考えてください。 Pythonは、_double underscore_メソッド(別名、。 _dunder_メソッド)。

+/+`演算子は `+ . truediv ()+`メソッドによって定義されます。 実際、https://github.com/python/cpython/blob/master/Lib/pathlib.py [+ pathlib +`のソースコード]を見ると、次のようなものが表示されます。

class PurePath(object):

    def __truediv__(self, key):
        return self._make_child((key,))

結論

Python 3.4以降、標準ライブラリで `+ pathlib `が利用可能になりました。 ` pathlib `を使用すると、以前のようにプレーンな文字列ではなく、適切な ` Path +`オブジェクトでファイルパスを表すことができます。 これらのオブジェクトは、ファイルパスを処理するコードを作成します。

  • 特に、パスを結合するために「/」が使用されるため、読みやすい

  • より強力で、ほとんどの必要なメソッドとプロパティがオブジェクトで直接利用可能

  • さまざまなシステムの特性が `+ Path +`オブジェクトによって隠されているため、オペレーティングシステム全体でより一貫性があります。

このチュートリアルでは、 `+ Path +`オブジェクトの作成方法、ファイルの読み取りと書き込み、パスと基になるファイルシステムの操作方法、および多くのファイルパスを反復処理する方法の例を見てきました。

無料のPDFダウンロード: Python 3 Cheat Sheet