Python Path.rglob()とは?サブフォルダも再帰的に検索する方法

PythonのPath.rglob()は、サブフォルダも含めて再帰的にファイルを探すpathlibのメソッドです。

rはrecursive(再帰的)を意味します。


Path.rglob()の基本的な使い方

from pathlib import Path

for p in sorted(Path("data").rglob("*.txt")):
    print(p)

実行結果(例):

data/b.txt
data/sub/c.txt

glob()が直下しか見ないのに対し、rglob()はすべての階層を探します。

from pathlib import Path

print([str(p) for p in sorted(Path("data").glob("*.txt"))])
print([str(p) for p in sorted(Path("data").rglob("*.txt"))])

実行結果(例):

['data/b.txt']
['data/b.txt', 'data/sub/c.txt']

glob("**/x")と同じもの

from pathlib import Path

a = sorted(str(p) for p in Path("data").rglob("*.txt"))
b = sorted(str(p) for p in Path("data").glob("**/*.txt"))

print(a == b)

実行結果:

True

rglob(パターン)はglob("**/" + パターン)の短縮形です。書き方が短いぶん、意図も伝わりやすくなります。

glob()については「Python Path.glob()とは?パターンでファイルを探す方法」をご覧ください。


すべてのファイルを列挙する

from pathlib import Path

files = [p for p in Path("projects").rglob("*") if p.is_file()]

for f in sorted(files):
    print(f)

実行結果(例):

projects/app/main.py
projects/app/utils.py
projects/docs/readme.md

rglob("*")はフォルダも返すため、is_file()で絞り込みます。

フォルダの合計サイズを出す

from pathlib import Path

total = sum(p.stat().st_size for p in Path("projects").rglob("*") if p.is_file())
print(f"{total:,} バイト")

実行結果(例):

15,690 バイト

フォルダ自体のサイズは中身の合計ではないため、配下のファイルを数え上げる必要があります。


戻り値はジェネレータ

from pathlib import Path

g = Path("projects").rglob("*.py")

print(type(g))
print(len(list(g)))
print(len(list(g)))   # 2回目は空

実行結果(例):

<class 'generator'>
5
0

一度取り出すと再利用できません。何度も使うならsorted()やlist()でリスト化します。

ジェネレータであることには利点もあります。巨大なフォルダでも、見つかったものから順に処理できるためメモリを圧迫しません。

from pathlib import Path

# 最初に見つかった1件だけ欲しい
first = next(Path("projects").rglob("*.py"), None)
print(first)

next()を使えば、全部を探し終える前に処理を打ち切れます。


深いフォルダでは時間がかかる

rglob()はすべての階層を実際に走査します。対象が巨大だと、想定以上に時間がかかります。

from pathlib import Path

# 危険:ホームディレクトリ全体を走査してしまう
# list(Path.home().rglob("*.py"))

特に次のような場所は避けてください。

  • ホームディレクトリやルート(/)
  • node_modulesや.gitを含むプロジェクト
  • ネットワークドライブ

不要なフォルダを除外する

from pathlib import Path

SKIP = {".git", "node_modules", "__pycache__", ".venv"}

files = [
    p for p in Path("projects").rglob("*.py")
    if p.is_file() and not (SKIP & set(p.parts))
]

print([str(p) for p in sorted(files)])

実行結果(例):

['projects/app/main.py', 'projects/app/utils.py']

p.partsはパスを要素ごとに分解したタプルです。除外したいフォルダ名が含まれていれば弾きます。

from pathlib import Path

print(Path("projects/app/.git/config").parts)

実行結果:

('projects', 'app', '.git', 'config')

ただし走査自体は行われる点に注意してください。速度が問題になる場合は、os.walk()でdirsを書き換えて探索自体を打ち切る方法が有効です。

import os

for root, dirs, files in os.walk("projects"):
    dirs[:] = [d for d in dirs if d not in {".git", "node_modules"}]
    for f in files:
        if f.endswith(".py"):
            print(os.path.join(root, f))

dirs[:]を書き換えると、そのフォルダの中には入らなくなります。


隠しファイルも一致する|globモジュールとの違い

間違えやすい点です。pathlibのglob()/rglob()は、ドットで始まる隠しファイルにも一致します。

import glob
from pathlib import Path

# h フォルダに .env / .hidden.txt / a.txt がある場合
print(sorted(p.name for p in Path("h").glob("*")))
print(sorted(glob.glob("h/*")))

実行結果:

['.env', '.hidden.txt', 'a.txt']
['h/a.txt']

まったく逆の結果になりました。

*と隠しファイル
Path.glob()/rglob() 一致する
glob.glob() 一致しない

globモジュールの感覚でrglob("*")を使うと、.gitや.venvの中まで拾ってしまいます。除外は次章のように明示的に書いてください。

from pathlib import Path

visible = [p for p in Path("h").glob("*") if not p.name.startswith(".")]
print(sorted(p.name for p in visible))

実行結果:

['a.txt']

シンボリックリンクに注意

フォルダへのリンクが親を指していると、無限に潜り続けることがあります。

from pathlib import Path

for p in Path("projects").rglob("*"):
    if p.is_dir() and p.is_symlink():
        continue   # リンクはたどらない
    print(p)

リンクを含む可能性のあるフォルダを走査するときは、is_symlink()での除外を検討してください。


Path.rglob()のまとめ

  • Path.rglob()はサブフォルダも含めて再帰的に検索する
  • glob("**/x")とまったく同じ(短縮形)
  • rglob("*")はフォルダも返すのでis_file()で絞る
  • 戻り値はジェネレータ。next()で途中打ち切りもできる
  • 巨大なフォルダでは時間がかかる。ホームやルートは避ける
  • 除外はp.partsで判定。走査自体を止めるならos.walk()
  • *は隠しファイルにも一致する(globモジュールとは逆)

あわせて読みたい

プログラミング学習に興味がある方は、まずは無料体験会へ

無料体験会・お申し込みはこちら