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モジュールとは逆)




