Pythonで条件を満たすパスの一覧を再帰的に取得するglobの使い方

Modified: | Tags: Python, ファイル処理

Pythonのglobモジュールを使うと、ワイルドカード*などの特殊文字を使って条件を満たすファイル・ディレクトリ(フォルダ)などのパスの一覧をリストやイテレータで取得できる。

パスをオブジェクトとして操作するpathlibモジュールでglob処理を行うこともできる。条件で抽出したあとで削除などの処理を行う場合はpathlibのほうが便利。

本記事のサンプルコードでは以下のようにglobモジュールとosモジュールをインポートしている。どちらも標準ライブラリに含まれているので追加のインストールは不要。

import glob
import os

以下のファイル・ディレクトリ構造を例とする。

temp/
├── 1.txt
├── 12.jpg
├── 123.txt
├── [x].txt
├── aaa.jpg
└── dir/
    ├── 987.jpg
    ├── bbb.txt
    ├── sub_dir1/
    │   ├── 98.txt
    │   └── ccc.jpg
    └── sub_dir2/
        └── ddd.jpg

glob()の基本的な使い方

glob()の第一引数にパスの文字列を指定する。ワイルドカード*などの特殊文字が使用可能。

条件を満たすパスの文字列を要素とするリスト(list)が取得できる。

l = glob.glob('temp/*.txt')
print(l)
# ['temp/[x].txt', 'temp/1.txt', 'temp/123.txt']

print(type(l))
# <class 'list'>

glob()で使用できるワイルドカード(特殊文字)

glob()では、Unixシェルで使われる*?などのワイルドカードが使用できる。正規表現を使う方法は後述。

長さ0文字以上の任意の文字列: *

*は長さ0文字以上の任意の文字列にマッチする。

print(glob.glob('temp/*'))
# ['temp/[x].txt', 'temp/12.jpg', 'temp/aaa.jpg', 'temp/dir', 'temp/1.txt', 'temp/123.txt']

print(glob.glob('temp/*.jpg'))
# ['temp/12.jpg', 'temp/aaa.jpg']

print(glob.glob('temp/dir/*/*.jpg'))
# ['temp/dir/sub_dir1/ccc.jpg', 'temp/dir/sub_dir2/ddd.jpg']

任意の一文字: ?

?は任意の一文字にマッチする。

例えば、拡張子を除くファイル名が3文字のパスを抽出したい場合は???.*とする。

print(glob.glob('temp/???.*'))
# ['temp/[x].txt', 'temp/aaa.jpg', 'temp/123.txt']

特定の一文字: []

[]で囲むと括弧内の文字列のどれか一文字にマッチする。例えば、[aZ1]aZ1のどれか一文字にマッチする。

ハイフン-を使って連続する文字を指定することも可能。例えば、[0-9]0から9までの数字のどれか一文字、[a-z]aからzまでの小文字のアルファベットのどれか一文字にマッチする。

print(glob.glob('temp/[0-9].*'))
# ['temp/1.txt']

print(glob.glob('temp/[0-9][0-9].*'))
# ['temp/12.jpg']

print(glob.glob('temp/[a-z][a-z][a-z].*'))
# ['temp/aaa.jpg']

!をつけると括弧内にない文字にマッチする。例えば[!a-z]は小文字のアルファベット以外の一文字にマッチする。

print(glob.glob('temp/[!a-z].*'))
# ['temp/1.txt']

特殊文字のエスケープ

*, ?, [そのものを含む文字列を対象としたい場合は[]で囲む。

print(glob.glob('temp/[[]*'))
# ['temp/[x].txt']

再帰的に取得: 引数recursive

glob()の引数recursiveTrueとして**を使うと、あらゆるファイルや0個以上のディレクトリおよびサブディレクトリにマッチする。

*を使って任意のディレクトリ名にマッチさせると同一階層のファイルしか抽出できないが、**を使うとあらゆる中間ディレクトリに対応してマッチさせることができる。

print(glob.glob('temp/*/*.jpg'))
# ['temp/dir/987.jpg']

print(glob.glob('temp/**/*.jpg', recursive=True))
# ['temp/12.jpg', 'temp/aaa.jpg', 'temp/dir/987.jpg', 'temp/dir/sub_dir1/ccc.jpg', 'temp/dir/sub_dir2/ddd.jpg']

特定のディレクトリに含まれるすべてのファイル・ディレクトリの一覧をサブディレクトリ内まで再帰的に取得することも可能。

print(glob.glob('temp/**', recursive=True))
# ['temp/', 'temp/[x].txt', 'temp/12.jpg', 'temp/aaa.jpg', 'temp/dir', 'temp/dir/987.jpg', 'temp/dir/sub_dir1', 'temp/dir/sub_dir1/ccc.jpg', 'temp/dir/sub_dir1/98.txt', 'temp/dir/bbb.txt', 'temp/dir/sub_dir2', 'temp/dir/sub_dir2/ddd.jpg', 'temp/1.txt', 'temp/123.txt']

なお、ファイル・ディレクトリ数が多い場合に**を使うと時間がかかる可能性があるので、他の特殊文字で条件を絞れるのであればそちらを使ったほうがいい。

ルートディレクトリを指定: 引数root_dir

glob()の引数root_dirにルートディレクトリを指定できる。glob()の実行前にカレントディレクトリをroot_dirに変更したように動作する(実際のカレントディレクトリは変更されない)。

デフォルトはNoneでカレントディレクトリがそのまま基準となる。

print(glob.glob('temp/*.txt'))
# ['temp/[x].txt', 'temp/1.txt', 'temp/123.txt']

print(glob.glob('*.txt', root_dir='temp'))
# ['[x].txt', '1.txt', '123.txt']

root_dirを指定したときの結果はroot_dirからの相対パスなので、カレントディレクトリからの相対パスを想定しているos.pathの関数などに渡す際は注意。root_dirと結果を結合して渡す必要がある。

ファイル名のみ取得

ファイル名のみを取得するには、パスがファイルか判定するos.path.isfile()をリスト内包表記の条件分岐で使う。

print([p for p in glob.glob('temp/**', recursive=True) if os.path.isfile(p)])
# ['temp/[x].txt', 'temp/12.jpg', 'temp/aaa.jpg', 'temp/dir/987.jpg', 'temp/dir/sub_dir1/ccc.jpg', 'temp/dir/sub_dir1/98.txt', 'temp/dir/bbb.txt', 'temp/dir/sub_dir2/ddd.jpg', 'temp/1.txt', 'temp/123.txt']

親ディレクトリの情報が必要ない場合はos.path.basename()で末尾のファイル名のみを抽出できる。

print([os.path.basename(p) for p in glob.glob('temp/**', recursive=True)
       if os.path.isfile(p)])
# ['[x].txt', '12.jpg', 'aaa.jpg', '987.jpg', 'ccc.jpg', '98.txt', 'bbb.txt', 'ddd.jpg', '1.txt', '123.txt']

中間ディレクトリの情報を残したい場合は引数root_dirを指定する。上述のようにroot_dirを指定したときの結果はroot_dirからの相対パスなので、os.path.isfile()にはroot_dirと結合して渡す。

print([p for p in glob.glob('**', recursive=True, root_dir='temp')
       if os.path.isfile(os.path.join('temp', p))])
# ['[x].txt', '12.jpg', 'aaa.jpg', 'dir/987.jpg', 'dir/sub_dir1/ccc.jpg', 'dir/sub_dir1/98.txt', 'dir/bbb.txt', 'dir/sub_dir2/ddd.jpg', '1.txt', '123.txt']

ディレクトリ名のみ取得

ディレクトリ名のみを取得するには、os.path.isdir()を使ってもよいが、**の末尾にディレクトリの区切り文字をつけるほうがシンプルで簡単。

print(glob.glob('temp/**/', recursive=True))
# ['temp/', 'temp/dir/', 'temp/dir/sub_dir1/', 'temp/dir/sub_dir2/']

指定したディレクトリ自体を除外するには、以下のように*/**/を使うか、引数root_dirを指定する。

print(glob.glob('temp/*/**/', recursive=True))
# ['temp/dir/', 'temp/dir/sub_dir1/', 'temp/dir/sub_dir2/']

print(glob.glob('**/', recursive=True, root_dir='temp'))
# ['dir/', 'dir/sub_dir1/', 'dir/sub_dir2/']

結果の末尾に区切り文字が必要ない場合はrstrip()で削除する。各OSでの区切り文字はos.sepで取得できる。

print([p.rstrip(os.sep) for p in glob.glob('temp/**/', recursive=True)])
# ['temp', 'temp/dir', 'temp/dir/sub_dir1', 'temp/dir/sub_dir2']

親ディレクトリの情報が必要ない場合は、さらにos.path.basename()を使う。必要であれば結果の末尾に区切り文字を結合する。

print([os.path.basename(p.rstrip(os.sep)) for p
       in glob.glob(os.path.join('temp/**/'), recursive=True)])
# ['temp', 'dir', 'sub_dir1', 'sub_dir2']

print([os.path.basename(p.rstrip(os.sep)) + os.sep for p
       in glob.glob(os.path.join('temp/**/'), recursive=True)])
# ['temp/', 'dir/', 'sub_dir1/', 'sub_dir2/']

正規表現で条件指定

ワイルドカード*?などである程度の条件指定は可能だが、より複雑な条件で抽出したい場合は正規表現reモジュールを使う。

glob()でファイル・ディレクトリを再帰的にリストアップしてからre.search()などで正規表現による判定を行う。

ファイル名が数字のみで拡張子がtxtのファイル、および、ファイル名が数字以外の文字3文字で拡張子がtxtまたはjpgのファイルを抽出する例は以下の通り。

import re

print([p for p in glob.glob('temp/**', recursive=True)
       if re.search('\d+\.txt', p)])
# ['temp/dir/sub_dir1/98.txt', 'temp/1.txt', 'temp/123.txt']

print([p for p in glob.glob('temp/**', recursive=True)
       if re.search('\D{3}\.(txt|jpg)', p)])
# ['temp/[x].txt', 'temp/aaa.jpg', 'temp/dir/sub_dir1/ccc.jpg', 'temp/dir/bbb.txt', 'temp/dir/sub_dir2/ddd.jpg']

ここで、\dは数字、\Dは数字以外の文字、{n}はn回の繰り返し、+は1回以上の繰り返し、(a|b)a, bのいずれかにマッチする。.も特殊文字なので\.とエスケープする必要がある。詳細は以下の記事を参照。

glob()で取得できるのは文字列のリストなので、正規表現以外にもin演算子や文字列メソッドなどで要素を抽出することも可能。

イテレータで一覧を取得: iglob()

これまでの例のようにglob()はパスのリストを生成する。

ファイルやディレクトリが少なければ特に気にする必要はないが、抽出したパスをfor文などで処理する場合は、リストではなくイテレータを使ったほうがメモリ使用量が抑えられる。

iglob()を使うとイテレータが返される。引数はglob()と同じでrecursiveも使える。

print(type(glob.iglob('temp/*.txt')))
# <class 'generator'>

for p in glob.iglob('temp/*.txt'):
    print(p)
# temp/[x].txt
# temp/1.txt
# temp/123.txt

関連カテゴリー

関連記事