- Python
Pythonファイル一覧の取得|globとos.listdir・再帰・拡張子
Pythonでファイル一覧を取得するには、1つのフォルダなら os.listdir()、拡張子で絞るなら glob.glob()、サブフォルダまで再帰するなら glob.glob("**/*.txt", recursive=True) か Path.rglob() を使います。 どれも標準ライブラリなので、追加のインストールは不要です。
ただ、方法が5つ以上あるため「どれを選べばよいか」「なぜ結果の並び順が毎回そろわないのか」「隠しファイルが出たり出なかったりするのはなぜか」で手が止まりがちです。この記事では、同じフォルダ構成に対して5つの方法を実行した結果を並べ、戻り値・再帰・パス形式・速度の違いを早見表にまとめました。後半には、複数拡張子・ファイルのみ・更新日時順・自然順ソートなどの目的別の逆引きコードと、5万ファイルでの実測時間を載せています。コードと出力は、すべて Python 3.14.7(Linux)で実行して確かめたものです。
ファイル一覧を取得する方法の早見表
| 方法 | 戻り値 | 再帰 | 返るパスの形 | 隠しファイル | 5万件の所要時間 | 向いている用途 |
|---|---|---|---|---|---|---|
os.listdir(path) |
名前の文字列のリスト | しない | 名前だけ(report.txt) |
含む | 14.3 ms | 1フォルダの名前がほしい |
os.scandir(path) |
DirEntry のイテレータ |
しない | entry.name と entry.path |
含む | 17.9 ms | ファイルかフォルダかの判定や、サイズ・日時も使う |
os.walk(top) |
(フォルダ, サブフォルダ名, ファイル名) のタプルを順に返す |
する | フォルダと名前が別々 | 含む | 21.2 ms(再帰) | フォルダ単位で処理する、特定フォルダを飛ばす |
glob.glob(pattern) |
パス文字列のリスト | recursive=True と ** で可 |
渡したパターンと同じ形(demo/report.txt) |
含まない(既定) | 54.8 ms | ワイルドカードで拡張子や名前を絞る |
Path.iterdir() |
Path のイテレータ |
しない | Path オブジェクト |
含む | 76.6 ms | 取得したあとに Path のメソッドで操作する |
Path.glob() / Path.rglob() |
Path のイテレータ |
rglob か ** で可 |
Path オブジェクト |
含む | 81.5 ms(再帰) | パターン指定と Path 操作をまとめて書く |
Path.walk()(3.12以降) |
(Path, サブフォルダ名, ファイル名) |
する | フォルダが Path |
含む | 22.2 ms(再帰) | os.walk を Path で書く |
所要時間は後半の「5万ファイルでの実測時間」の数値(Python 3.14.7、ext4)です。再帰と書いた行は400フォルダ・5万ファイルから .txt を探した時間、それ以外は1フォルダ5万ファイルを列挙した時間です。
迷ったときの選び方は次のとおりです。
| やりたいこと | 選ぶ方法 |
|---|---|
| 1つのフォルダの名前一覧 | os.listdir() |
| 拡張子で絞る | glob.glob("フォルダ/*.txt") または Path.glob("*.txt") |
| サブフォルダもすべて | glob.glob("フォルダ/**/*.txt", recursive=True) または Path.rglob("*.txt") |
| ファイルだけ・フォルダだけ | os.scandir() と entry.is_file() / entry.is_dir() |
| 一部のフォルダを飛ばして再帰 | os.walk() で dirs[:] を書き換える |
| 数万件以上を速く処理 | os.scandir() または os.walk() |
| 並び順をそろえる | どの方法でも sorted() を自分でかける |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Pythonファイル一覧の取得|globとos.listdir・再帰・拡張子」
検証に使うフォルダ構成
この記事のコードは、すべて次の demo フォルダに対して実行しています。数字入りの名前、大文字の拡張子、ドットで始まる隠しファイル、空のフォルダ、2階層のサブフォルダを入れてあります。
demo/
├── .cache/
│ └── tmp.txt
├── .hidden.txt
├── Photo.JPG
├── data.csv
├── empty/ (空のフォルダ)
├── img1.png
├── img2.png
├── img10.png
├── notes.md
├── report.txt
└── sub/
├── a.txt
├── b.py
└── deep/
└── c.txt
同じ構成を手元に作るコードです。更新日時を1時間ずつずらしてあるので、日時順ソートの結果も再現できます。
import os
import shutil
from pathlib import Path
files = [
"demo/report.txt", "demo/data.csv", "demo/Photo.JPG", "demo/img10.png",
"demo/img2.png", "demo/img1.png", "demo/notes.md", "demo/.hidden.txt",
"demo/sub/a.txt", "demo/sub/b.py", "demo/sub/deep/c.txt", "demo/.cache/tmp.txt",
]
shutil.rmtree("demo", ignore_errors=True)
base = 1790000000 # 2026-09-21 13:13:20 UTC
for i, name in enumerate(files):
p = Path(name)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text("x" * (i * 100 + 10)) # サイズを変えておく
os.utime(p, (base + i * 3600, base + i * 3600))
Path("demo/empty").mkdir()
print("作成:", len(files), "ファイル")
作成: 12 ファイル
5つの方法を同じフォルダで実行して比べる
まずは5つの方法を、並べ替えをせずにそのまま実行します。
import glob
import os
from pathlib import Path
print("1. os.listdir")
print(os.listdir("demo"))
print("2. os.scandir")
with os.scandir("demo") as entries:
print([(e.name, e.is_file()) for e in entries])
print("3. os.walk")
for root, dirs, files in os.walk("demo"):
print(" ", root, dirs, files)
print("4. glob.glob")
print(glob.glob("demo/*"))
print(glob.glob("demo/**/*.txt", recursive=True))
print("5. pathlib")
p = Path("demo")
print([x.name for x in p.iterdir()])
print(list(p.glob("*.png")))
print([str(x) for x in p.rglob("*.txt")])
1. os.listdir
['.cache', 'Photo.JPG', 'notes.md', 'img10.png', 'report.txt', 'img1.png', 'empty', 'data.csv', 'sub', 'img2.png', '.hidden.txt']
2. os.scandir
[('.cache', False), ('Photo.JPG', True), ('notes.md', True), ('img10.png', True), ('report.txt', True), ('img1.png', True), ('empty', False), ('data.csv', True), ('sub', False), ('img2.png', True), ('.hidden.txt', True)]
3. os.walk
demo ['.cache', 'empty', 'sub'] ['Photo.JPG', 'notes.md', 'img10.png', 'report.txt', 'img1.png', 'data.csv', 'img2.png', '.hidden.txt']
demo/.cache [] ['tmp.txt']
demo/empty [] []
demo/sub ['deep'] ['b.py', 'a.txt']
demo/sub/deep [] ['c.txt']
4. glob.glob
['demo/Photo.JPG', 'demo/notes.md', 'demo/img10.png', 'demo/report.txt', 'demo/img1.png', 'demo/empty', 'demo/data.csv', 'demo/sub', 'demo/img2.png']
['demo/report.txt', 'demo/sub/a.txt', 'demo/sub/deep/c.txt']
5. pathlib
['.cache', 'Photo.JPG', 'notes.md', 'img10.png', 'report.txt', 'img1.png', 'empty', 'data.csv', 'sub', 'img2.png', '.hidden.txt']
[PosixPath('demo/img10.png'), PosixPath('demo/img1.png'), PosixPath('demo/img2.png')]
['demo/report.txt', 'demo/.hidden.txt', 'demo/.cache/tmp.txt', 'demo/sub/a.txt', 'demo/sub/deep/c.txt']
この出力から、次の3点が読み取れます。
- 並び順はアルファベット順になっていません。
img10.png、img1.png、img2.pngの順に出ています。これはファイルシステムが返す順番そのままで、環境によって変わります。 glob.glob("demo/*")には.cacheと.hidden.txtが入っていません。 一方、os.listdirやPath.rglobには入っています。ドットで始まる名前の扱いが、globモジュールだけ違います。- 戻り値の形が違います。
os.listdirは名前だけ、glob.globはdemo/付きの文字列、pathlibはPosixPath(...)というオブジェクトです(Windows ではWindowsPathになります)。
os.listdir:名前だけをリストで返す
os.listdir(path) は、フォルダ直下のファイル名とフォルダ名を、文字列のリストで返します。 返るのは名前だけなので、ファイルを開くときは os.path.join("demo", name) でフォルダとつなげます。ファイルとフォルダの区別もないため、区別したいときは os.path.isfile() を1件ずつ呼びます。引数を省略するとカレントフォルダが対象です。
os.scandir:種類や日時の情報も一緒に受け取る
os.scandir(path) は DirEntry というオブジェクトを順に返します。entry.name(名前)、entry.path(フォルダ付きのパス)、entry.is_file()、entry.is_dir()、entry.stat() が使えます。フォルダを読むときに得た種類の情報を覚えているので、is_file() のたびにディスクへ問い合わせる回数が少なくて済みます。 後半の計測でも、ファイルだけに絞る処理は os.listdir と os.path.isfile の組み合わせより約5倍速くなりました。with で囲むと、読み終わった時点でフォルダのハンドルが閉じられます。
os.walk:フォルダを1つずつたどる
os.walk(top) は、たどったフォルダごとに (フォルダのパス, サブフォルダ名のリスト, ファイル名のリスト) を返します。 上の出力では5つのフォルダ(demo、demo/.cache、demo/empty、demo/sub、demo/sub/deep)について1行ずつ表示されています。ファイルのフルパスは os.path.join(root, name) で組み立てます。ループの途中で dirs を書き換えると、たどる先を変えられるのが os.walk ならではの強みです(逆引きコードで扱います)。
glob.glob:ワイルドカードで絞る
glob.glob(pattern) は、シェルと同じワイルドカードに合うパスを返します。 使える記号は次のとおりです。
| 記号 | 意味 | 例 | 合うもの |
|---|---|---|---|
* |
0文字以上の任意の文字(フォルダの区切りは越えない) | demo/*.png |
demo/img1.png |
? |
任意の1文字 | demo/img?.png |
img1.png、img2.png(img10.png は合わない) |
[abc] [0-9] |
かっこ内のどれか1文字 | demo/img[0-9].png |
img1.png、img2.png |
[!abc] |
かっこ内以外の1文字 | demo/[!i]* |
i 以外で始まる名前 |
** |
0階層以上のフォルダ(recursive=True のときだけ) |
demo/**/*.txt |
demo/report.txt、demo/sub/deep/c.txt |
** は recursive=True を付けたときだけ「何階層でも」の意味になります。付け忘れると * と同じ扱いになり、1階層下しか探しません。また demo/**/*.txt は、demo 直下の report.txt にも合います(0階層を含むため)。
pathlib:Pathオブジェクトで受け取る
Path("demo").iterdir() は直下の一覧、Path.glob(pattern) はパターン指定、Path.rglob(pattern) は再帰つきのパターン指定です。rglob("*.txt") は glob("**/*.txt") と同じ意味で、recursive=True を書く必要がありません。戻り値が Path なので、x.name(名前)、x.stem(拡張子なしの名前)、x.suffix(拡張子)、x.stat()、x.read_text() をそのまま続けて書けます。
目的別の逆引きコード
ここからは「やりたいこと」ごとのコードです。どれも先ほどの demo フォルダで実行した結果を、すぐ下に載せています。並び順をそろえるため、結果には sorted() をかけています。
ファイルだけ・フォルダだけを取得する
import glob
import os
from pathlib import Path
# ファイルだけ
print(sorted(e.name for e in os.scandir("demo") if e.is_file()))
print(sorted(x.name for x in Path("demo").iterdir() if x.is_file()))
print(sorted(f for f in os.listdir("demo") if os.path.isfile(os.path.join("demo", f))))
# フォルダだけ
print(sorted(e.name for e in os.scandir("demo") if e.is_dir()))
print(sorted(glob.glob("demo/*/"))) # 末尾を / にするとフォルダだけ
print(sorted(str(x) for x in Path("demo").rglob("*") if x.is_dir())) # 再帰
['.hidden.txt', 'Photo.JPG', 'data.csv', 'img1.png', 'img10.png', 'img2.png', 'notes.md', 'report.txt']
['.hidden.txt', 'Photo.JPG', 'data.csv', 'img1.png', 'img10.png', 'img2.png', 'notes.md', 'report.txt']
['.hidden.txt', 'Photo.JPG', 'data.csv', 'img1.png', 'img10.png', 'img2.png', 'notes.md', 'report.txt']
['.cache', 'empty', 'sub']
['demo/empty/', 'demo/sub/']
['demo/.cache', 'demo/empty', 'demo/sub', 'demo/sub/deep']
os.listdir で絞るときは、os.path.isfile(f) ではなく os.path.isfile(os.path.join("demo", f)) と書きます。 名前だけを渡すとカレントフォルダの中を探すため、結果が空のリストになります。glob.glob("demo/*/") は短く書けますが、隠しフォルダの .cache は含まれません。
拡張子で絞り込む・複数の拡張子を指定する
import glob
import os
from pathlib import Path
# 1つの拡張子
print(sorted(glob.glob("demo/*.png")))
# 複数の拡張子(suffix を集合と比べる。小文字にそろえてから比較)
exts = {".png", ".jpg"}
print(sorted(x.name for x in Path("demo").iterdir() if x.suffix.lower() in exts))
# 複数の拡張子(endswith にタプルを渡す)
print(sorted(f for f in os.listdir("demo") if f.lower().endswith((".png", ".jpg"))))
# 複数の拡張子(glob を拡張子の数だけ呼んでつなげる)
print(sorted(glob.glob("demo/*.png") + glob.glob("demo/*.csv")))
# 波かっこのパターンは glob では使えない
print(glob.glob("demo/*.{png,jpg}"))
# 再帰して複数の拡張子
print(sorted(str(x) for x in Path("demo").rglob("*") if x.suffix in {".txt", ".py"}))
['demo/img1.png', 'demo/img10.png', 'demo/img2.png']
['Photo.JPG', 'img1.png', 'img10.png', 'img2.png']
['Photo.JPG', 'img1.png', 'img10.png', 'img2.png']
['demo/data.csv', 'demo/img1.png', 'demo/img10.png', 'demo/img2.png']
[]
['demo/.cache/tmp.txt', 'demo/.hidden.txt', 'demo/report.txt', 'demo/sub/a.txt', 'demo/sub/b.py', 'demo/sub/deep/c.txt']
シェルで使える *.{png,jpg} という書き方は、Python の glob では使えず、空のリストが返ります。 エラーにならないので気づきにくい点です。複数の拡張子は、suffix を集合と比べる書き方がいちばん読みやすく、拡張子が増えても1か所を直すだけで済みます。
大文字と小文字を区別せずに探す
Linux では *.jpg と書いても Photo.JPG には合いません。
import glob
import sys
from pathlib import Path
print(glob.glob("demo/*.jpg")) # 小文字で探す
print(glob.glob("demo/*.[jJ][pP][gG]")) # 文字クラスで両方に合わせる
print([x.name for x in Path("demo").iterdir() if x.suffix.lower() == ".jpg"])
if sys.version_info >= (3, 12): # 3.12 以降の引数
print([x.name for x in Path("demo").glob("*.jpg", case_sensitive=False)])
[]
['demo/Photo.JPG']
['Photo.JPG']
['Photo.JPG']
OSによらず同じ結果にしたいときは、suffix.lower() で小文字にそろえてから比べるのが確実です。Python 3.12 以降なら Path.glob() と Path.rglob() に case_sensitive=False を渡せます。
隠しファイルを含める・除く
ドットで始まる名前の扱いは、方法によって次のように分かれます。
| 方法 | ドットで始まる名前 |
|---|---|
os.listdir / os.scandir / os.walk |
含む |
Path.iterdir / Path.glob / Path.rglob |
含む |
glob.glob / glob.iglob |
含まない(パターンもドットで始めたときだけ合う) |
glob.glob(..., include_hidden=True)(3.11以降) |
含む |
import glob
import os
from pathlib import Path
print(sorted(glob.glob("demo/*.txt"))) # 隠しファイルなし
print(sorted(glob.glob("demo/.*"))) # ドット始まりだけ
print(sorted(glob.glob("demo/**/*.txt", recursive=True, include_hidden=True)))
# os.listdir から隠しファイルを除く
print(sorted(f for f in os.listdir("demo") if not f.startswith(".")))
# rglob から「隠しファイル」と「隠しフォルダの中身」を除く
p = Path("demo")
print(sorted(
str(x) for x in p.rglob("*.txt")
if not any(part.startswith(".") for part in x.relative_to(p).parts)
))
['demo/report.txt']
['demo/.cache', 'demo/.hidden.txt']
['demo/.cache/tmp.txt', 'demo/.hidden.txt', 'demo/report.txt', 'demo/sub/a.txt', 'demo/sub/deep/c.txt']
['Photo.JPG', 'data.csv', 'empty', 'img1.png', 'img10.png', 'img2.png', 'notes.md', 'report.txt', 'sub']
['demo/report.txt', 'demo/sub/a.txt', 'demo/sub/deep/c.txt']
glob から pathlib へ書き換えたら件数が増えた、というときは、この違いが原因であることが多いです。.git や .venv のような隠しフォルダの中まで拾うかどうかで、件数も時間も大きく変わります。
名前順・自然順・更新日時順・サイズ順に並べる
import os
import re
from datetime import datetime, timezone
from pathlib import Path
p = Path("demo")
pngs = [x.name for x in p.glob("*.png")]
print(sorted(pngs)) # 文字コード順
def natural_key(name):
"""数字の部分を数値として比べる(自然順)"""
return [int(t) if t.isdigit() else t.lower() for t in re.split(r"(\d+)", name)]
print(sorted(pngs, key=natural_key)) # 自然順
names = [f for f in os.listdir("demo") if not f.startswith(".")]
print(sorted(names)) # 大文字が先に来る
print(sorted(names, key=str.casefold)) # 大文字小文字を区別しない
files = [x for x in p.iterdir() if x.is_file()]
by_time = sorted(files, key=lambda x: x.stat().st_mtime, reverse=True)
print([x.name for x in by_time]) # 更新日時の新しい順
newest = max(files, key=lambda x: x.stat().st_mtime) # いちばん新しい1件
stamp = datetime.fromtimestamp(newest.stat().st_mtime, timezone.utc)
print(newest.name, stamp.strftime("%Y-%m-%d %H:%M"))
by_size = sorted(files, key=lambda x: x.stat().st_size, reverse=True)
print([(x.name, x.stat().st_size) for x in by_size[:3]]) # サイズの大きい順に3件
['img1.png', 'img10.png', 'img2.png']
['img1.png', 'img2.png', 'img10.png']
['Photo.JPG', 'data.csv', 'empty', 'img1.png', 'img10.png', 'img2.png', 'notes.md', 'report.txt', 'sub']
['data.csv', 'empty', 'img1.png', 'img10.png', 'img2.png', 'notes.md', 'Photo.JPG', 'report.txt', 'sub']
['.hidden.txt', 'notes.md', 'img1.png', 'img2.png', 'img10.png', 'Photo.JPG', 'data.csv', 'report.txt']
.hidden.txt 2026-09-21 21:13
[('.hidden.txt', 710), ('notes.md', 610), ('img1.png', 510)]
sorted() だけだと img1.png → img10.png → img2.png の順になります。文字列は左から1文字ずつ比べるので、"1" の次の "0" と "." を比べた時点で順番が決まるためです。連番のファイルを番号順に処理したいときは、上の natural_key を key に渡します。 外部ライブラリを使える場合は natsort パッケージでも同じ並びが得られます(この記事では未実行です)。
フルパス・相対パス・名前だけを取り出す
import glob
import os
from pathlib import Path
p = Path("demo")
print(sorted(os.path.basename(f) for f in glob.glob("demo/*.png"))) # 名前だけ
print(sorted(x.stem for x in p.glob("*.png"))) # 拡張子なし
print(sorted(str(x.relative_to(p)) for x in p.rglob("*.py"))) # demo からの相対パス
print(sorted(glob.glob("**/*.py", root_dir="demo", recursive=True))) # 同上(3.10 以降)
print(os.path.isabs(os.path.abspath("demo/data.csv")), p.joinpath("data.csv").resolve().is_absolute())
print([os.path.join("demo", f) for f in sorted(os.listdir("demo")) if f.endswith(".csv")])
['img1.png', 'img10.png', 'img2.png']
['img1', 'img10', 'img2']
['sub/b.py']
['sub/b.py']
True True
['demo/data.csv']
glob.glob() の root_dir は「探し始めるフォルダ」を指定する引数で、結果はそのフォルダからの相対パスになります。os.chdir() でカレントフォルダを動かさずに済むので、ほかの処理に影響しません。絶対パスがほしいときは os.path.abspath() か Path.resolve() を使います。
特定のフォルダを飛ばす・階層の深さを制限する
import glob
import os
# 隠しフォルダと sub/deep を飛ばし、名前順にたどる
result = []
for root, dirs, files in os.walk("demo"):
dirs[:] = sorted(d for d in dirs if not d.startswith(".") and d != "deep")
for name in sorted(files):
if not name.startswith("."):
result.append(os.path.join(root, name))
print(result)
def walk_depth(top, max_depth):
"""max_depth 階層下までのファイルを返す(0 なら直下だけ)"""
base = top.rstrip(os.sep).count(os.sep)
for root, dirs, files in os.walk(top):
if root.count(os.sep) - base >= max_depth:
dirs[:] = [] # これより下へは進まない
for name in files:
yield os.path.join(root, name)
print(sorted(walk_depth("demo", 0)))
print(sorted(glob.glob("demo/*/*"))) # ちょうど1階層下だけ
['demo/Photo.JPG', 'demo/data.csv', 'demo/img1.png', 'demo/img10.png', 'demo/img2.png', 'demo/notes.md', 'demo/report.txt', 'demo/sub/a.txt', 'demo/sub/b.py']
['demo/.hidden.txt', 'demo/Photo.JPG', 'demo/data.csv', 'demo/img1.png', 'demo/img10.png', 'demo/img2.png', 'demo/notes.md', 'demo/report.txt']
['demo/sub/a.txt', 'demo/sub/b.py', 'demo/sub/deep']
dirs[:] = ... と、スライスへの代入で書くのがポイントです。 dirs = [...] と書くと新しいリストを変数に入れ直すだけになり、os.walk が持っているリストは変わらないので、フォルダは飛ばされません。node_modules や .venv のようにファイル数が多いフォルダを入口で飛ばせるので、あとから結果を取り除くより速く終わります。
件数・合計サイズ・空のフォルダを調べる
import os
from pathlib import Path
print(sum(len(files) for _, _, files in os.walk("demo"))) # ファイルの総数
print(sum(x.stat().st_size for x in Path("demo").rglob("*") if x.is_file())) # 合計バイト数
print([root for root, dirs, files in os.walk("demo") if not dirs and not files]) # 空のフォルダ
from collections import Counter
print(Counter(x.suffix.lower() for x in Path("demo").rglob("*") if x.is_file()).most_common())
12
6720
['demo/empty']
[('.txt', 5), ('.png', 3), ('.jpg', 1), ('.md', 1), ('.csv', 1), ('.py', 1)]
名前のパターンや正規表現で絞る
import fnmatch
import glob
import os
import re
names = os.listdir("demo")
print(sorted(fnmatch.filter(names, "img*.png"))) # ワイルドカード
print(sorted(f for f in names if re.fullmatch(r"img\d\.png", f))) # 正規表現(数字1桁)
print(sorted(glob.glob("demo/img?.png"))) # ? は1文字
print(glob.escape("demo/[v1]/*.txt")) # 記号を含む名前をそのまま探すとき
print(next(glob.iglob("demo/**/*.py", recursive=True))) # 最初の1件だけほしいとき
['img1.png', 'img10.png', 'img2.png']
['img1.png', 'img2.png']
['demo/img1.png', 'demo/img2.png']
demo/[[]v1]/[*].txt
demo/sub/b.py
フォルダ名に [ や * が含まれていると、パターンの記号として解釈されます。ユーザーが入力したフォルダ名などをパターンに組み込むときは、glob.escape() を通します。 glob.iglob() はリストを作らず1件ずつ返すので、「1件でも見つかれば十分」という処理に向いています。
結果の順序は保証されない
最初の実行結果で img10.png、img1.png、img2.png の順に出たとおり、どの方法も並び順を約束していません。 今回開いた公式ドキュメントでは、glob モジュールに「パス名は特定の順序では返されない。順序が必要なら結果をソートすること」という趣旨の記述があり、pathlib の Path.glob()・Path.rglob() にも同じ注意書きがあります。Path.iterdir() は「任意の順序」、Path.walk() は「リストがソートされるかどうかはファイルシステムに依存する」と書かれています。
手元のパソコンでは名前順に見えていても、別のOSやファイルシステムでは違う順番になることがあります。たとえば、次のような処理は順番に左右されます。
| 処理 | 順番が変わると起きること | そろえ方 |
|---|---|---|
| 連番の画像をつなげる | フレームの順番が入れ替わる | sorted(..., key=natural_key) |
| CSVを順に読み込んで結合する | 行の並びが実行環境ごとに変わる | sorted(glob.glob(...)) |
| 「最初の1件」を取り出す | 取り出されるファイルが変わる | min() / max() に key を指定する |
| 学習用データを分割する | 分割結果が再現できない | 並べ替えてから分割する |
一覧を取得したら sorted() をかける、と決めておくと、環境の違いで結果が変わることがなくなります。os.walk は dirs.sort() と sorted(files) を組み合わせると、たどる順番までそろえられます。
WindowsとLinuxの違い
この記事の実行結果はすべて Linux のものです。Windows では未実行のため、次の表は公式ドキュメントの記述(pathlib の大文字小文字の扱い)と、一般に知られた仕様をまとめたものです。
| 項目 | Linux | Windows |
|---|---|---|
| 区切り文字 | /(demo/sub/a.txt) |
os や glob の結果は \(demo\sub\a.txt)。パターンには / も書ける |
| 大文字小文字 | 区別する(*.jpg は Photo.JPG に合わない) |
区別しない(*.jpg が Photo.JPG にも合う) |
| 隠しファイル | 名前がドットで始まるもの | ファイルの属性で決まる。glob が既定で除くのは、どちらのOSでもドット始まりの名前 |
Path の型 |
PosixPath |
WindowsPath |
entry.stat() |
呼ぶたびにOSへ問い合わせる | フォルダを読んだ時点の情報を使えるため、追加の問い合わせが少ない |
OSの違いを吸収する書き方は3つあります。パスの連結に os.path.join() か Path の / 演算子を使う、拡張子は suffix.lower() で比べる、結果を sorted() でそろえる、の3つです。区切り文字を / に統一した文字列がほしいときは Path.as_posix() が使えます。
5万ファイルでの実測時間
件数が増えたときの差を確かめるため、次の2種類のフォルダを作って計測しました。各処理を5回(日時順のみ3回)実行し、いちばん短かった時間を載せています。環境は Linux・ext4・仮想マシン上で、数値は環境によって変わります。
- flat:1つのフォルダに空のファイルを5万個
- tree:200フォルダ × (直下に200個 + サブフォルダに50個)= 400フォルダ・5万ファイル。うち
.txtは3万個
計測に使った関数は次のとおりです(フォルダを作る部分は省略しています)。
import time
def measure(label, func, repeat=5):
best = min(_once(func) for _ in range(repeat))
print(f"{label:<40}{best * 1000:8.1f} ms")
def _once(func):
start = time.perf_counter()
func()
return time.perf_counter() - start
# 例: measure("os.listdir", lambda: os.listdir("big/flat"))
1フォルダ5万ファイルを列挙する
| 処理 | Python 3.11.15 | Python 3.14.7 |
|---|---|---|
os.listdir |
18.1 ms | 14.3 ms |
os.scandir(名前を取り出す) |
20.6 ms | 17.9 ms |
glob.glob("*") |
63.7 ms | 54.8 ms |
Path.iterdir |
44.6 ms | 76.6 ms |
Path.glob("*") |
80.0 ms | 65.3 ms |
ファイルだけに絞る・更新日時順に並べる
| 処理 | Python 3.11.15 | Python 3.14.7 |
|---|---|---|
os.listdir + os.path.isfile |
222.1 ms | 131.9 ms |
os.scandir + entry.is_file() |
42.1 ms | 27.3 ms |
Path.iterdir + Path.is_file() |
306.3 ms | 242.5 ms |
os.listdir + os.path.getmtime でソート |
192.2 ms | 193.1 ms |
os.scandir + entry.stat() でソート |
199.5 ms | 183.6 ms |
再帰して .txt を3万件探す
| 処理 | Python 3.11.15 | Python 3.14.7 |
|---|---|---|
os.walk + endswith |
23.7 ms | 21.2 ms |
os.scandir を再帰で呼ぶ |
21.2 ms | 18.6 ms |
glob.glob("**/*.txt", recursive=True) |
63.2 ms | 59.9 ms |
Path.rglob("*.txt") |
126.1 ms | 81.5 ms |
Path.walk + endswith |
(3.12以降) | 22.2 ms |
計測からわかることは次の4点です。
- 名前を列挙するだけなら、どれも5万件で0.1秒未満です。 数百〜数千件の規模では、速度より書きやすさで選んで差し支えありません。
- 差が開くのは「1件ずつ種類を調べる」処理です。
os.scandirのentry.is_file()は、os.path.isfileを1件ずつ呼ぶ書き方の約5倍、Path.is_file()の約9倍の速さでした(3.14.7)。 - 更新日時順は、Linux ではどちらもほぼ同じ時間です。
entry.stat()も結局ファイルごとにOSへ問い合わせるためです。 Path.rglobは 3.11 の126.1 ms から 3.14 の81.5 ms へ短くなっています。os.walkと比べると約4倍の時間ですが、Pathオブジェクトを3万個作る分が含まれます。
数十万件を超える規模や、ネットワーク越しのフォルダを扱うときは、os.scandir か os.walk を選ぶと所要時間を短くできます。再帰版の os.scandir は次の形です。
import os
def scantree(path):
"""path 以下のファイルを DirEntry で1件ずつ返す"""
with os.scandir(path) as entries:
for entry in entries:
if entry.is_dir(follow_symlinks=False):
yield from scantree(entry.path)
else:
yield entry
print(sorted(e.path for e in scantree("demo") if e.name.endswith(".txt")))
['demo/.cache/tmp.txt', 'demo/.hidden.txt', 'demo/report.txt', 'demo/sub/a.txt', 'demo/sub/deep/c.txt']
バージョンごとの新機能
一覧取得まわりは、ここ数年のバージョンで引数が増えています。今回、公式ドキュメントの glob と pathlib のページを開いて、追加されたバージョンを確かめました。
| 機能 | 追加されたバージョン | 内容 |
|---|---|---|
glob.glob(..., root_dir=, dir_fd=) |
3.10 | 探し始めるフォルダを指定する。結果は相対パス |
glob.glob(..., include_hidden=True) |
3.11 | ドットで始まる名前もワイルドカードに合わせる |
Path.glob() / rglob() の case_sensitive |
3.12 | 大文字小文字を区別するかを指定する |
Path.walk() |
3.12 | os.walk の Path 版 |
Path.glob() / rglob() の recurse_symlinks |
3.13 | ** でシンボリックリンクの先もたどる |
末尾が ** のパターンの結果 |
3.13 | フォルダだけでなくファイルも返すようになった |
glob.translate() |
3.13 | パターンを正規表現の文字列に変換する |
Path.info |
3.14 | フォルダを読んだときの種類の情報を保持する |
root_dir は 3.11 の機能として紹介されることがありますが、公式ドキュメントでは root_dir と dir_fd が 3.10、include_hidden が 3.11 での追加です。
新しい機能を Python 3.14.7 で実行した結果です。
import glob
from pathlib import Path
p = Path("demo")
# Path.walk(3.12 以降): os.walk と同じ形で、フォルダが Path になる
for root, dirs, files in p.walk():
dirs.sort()
print(root, dirs, sorted(files))
# 末尾が ** のパターン(3.13 以降はファイルも返す)
print(len(list(p.glob("**"))))
# glob.translate(3.13 以降)
print(glob.translate("**/*.txt", recursive=True, include_hidden=True))
# Path.info(3.14 以降)
first = min(p.iterdir())
print(first.name, first.info.is_dir(), first.info.is_file())
demo ['.cache', 'empty', 'sub'] ['.hidden.txt', 'Photo.JPG', 'data.csv', 'img1.png', 'img10.png', 'img2.png', 'notes.md', 'report.txt']
demo/.cache [] ['tmp.txt']
demo/empty [] []
demo/sub ['deep'] ['a.txt', 'b.py']
demo/sub/deep [] ['c.txt']
17
(?s:(?:.+/)?[^/]*\.txt)\z
.cache True False
p.glob("**") は 3.14.7 では17件(フォルダ5つとファイル12個)でしたが、同じコードを Python 3.11.15 で実行すると5件(フォルダだけ)でした。バージョンで件数が変わる書き方なので、フォルダだけがほしいときは is_dir() で絞ると、どのバージョンでも同じ結果になります。Path.walk() は os.walk() と使い方が同じで、root / name と書くだけでフルパスの Path が得られます。
つまずきやすい点と直し方
存在しないフォルダを渡したときの動きは、方法によって違います。
import glob
import os
from pathlib import Path
for label, func in [
("os.listdir", lambda: os.listdir("nothing")),
("os.listdir(ファイルを指定)", lambda: os.listdir("demo/data.csv")),
("Path.iterdir", lambda: list(Path("nothing").iterdir())),
("glob.glob", lambda: glob.glob("nothing/*")),
("Path.rglob", lambda: list(Path("nothing").rglob("*"))),
("os.walk", lambda: list(os.walk("nothing"))),
]:
try:
print(f"{label}: {func()}")
except OSError as e:
print(f"{label}: {type(e).__name__}: {e}")
os.listdir: FileNotFoundError: [Errno 2] No such file or directory: 'nothing'
os.listdir(ファイルを指定): NotADirectoryError: [Errno 20] Not a directory: 'demo/data.csv'
Path.iterdir: FileNotFoundError: [Errno 2] No such file or directory: 'nothing'
glob.glob: []
Path.rglob: []
os.walk: []
glob.glob・Path.rglob・os.walk は、フォルダがなくてもエラーにならず、空の結果を返します。 パスの打ち間違いに気づきにくいので、「0件のときはメッセージを出す」処理を入れておくと原因を早く見つけられます。
| 症状 | 原因 | 直し方 |
|---|---|---|
| 結果が空のリストになる | パスの打ち間違い、カレントフォルダが想定と違う | os.getcwd() と os.path.isdir(path) を表示して確かめる |
** を書いたのに1階層しか探さない |
recursive=True を付けていない |
glob.glob(pattern, recursive=True) にする |
os.path.isfile(name) がすべて False |
名前だけを渡している | os.path.join(folder, name) でつなげる |
*.{png,jpg} で0件 |
glob は波かっこに対応していない |
suffix を集合と比べる |
| 隠しファイルが出ない/出る | glob と pathlib で既定の扱いが違う |
include_hidden=True か、startswith(".") で除く |
list(p.glob(...)) を2回使うと2回目が空 |
イテレータを使い切っている | 最初に files = list(...) で受け取る |
連番が 1, 10, 2 の順に並ぶ |
文字列として比べている | natural_key を key に渡す |
os.walk でフォルダを飛ばせない |
dirs = [...] と代入している |
dirs[:] = [...] と書く |
よくある質問
Q. globとos.listdirはどちらを使えばよいですか?
A. 名前の一覧がほしいだけなら os.listdir()、拡張子や名前のパターンで絞るなら glob.glob() が向いています。os.listdir() は名前だけを返し、glob.glob() はフォルダ付きのパスを返すので、あとでファイルを開く処理が続くなら glob.glob() か pathlib のほうがコードが短くなります。
Q. サブフォルダの中もまとめて取得するには?
A. glob.glob("フォルダ/**/*.txt", recursive=True)、Path("フォルダ").rglob("*.txt")、os.walk("フォルダ") の3つがあります。パターンで絞るなら前の2つ、フォルダごとに処理を分けたい・特定のフォルダを飛ばしたいなら os.walk() を選びます。
Q. 取得した一覧の順番が実行するたびに違うのはなぜですか?
A. どの方法も、ファイルシステムが返した順番のまま結果を返すためです。公式ドキュメントにも、順序が必要ならソートするよう書かれています。sorted() を1つかけるだけでそろいます。連番のファイルは自然順のキーを渡してください。
Q. ファイル数がとても多いとき、いちばん速いのはどれですか?
A. 今回の計測では、名前の列挙は os.listdir()、種類の判定を伴う処理は os.scandir()、再帰は os.walk() と os.scandir() の再帰が短い時間で終わりました。pathlib は Path オブジェクトを作る分だけ時間がかかりますが、5万件でも0.1秒前後です。
まとめ
- 1フォルダの名前は
os.listdir()、パターンで絞るならglob.glob()、再帰はrecursive=Trueを付けたglob.glob()かPath.rglob()を使います。 - 戻り値の形が違います。
os.listdirは名前だけ、globはフォルダ付きの文字列、pathlibはPathオブジェクトです。 globは既定で隠しファイルを含まず、pathlibとosは含みます。 件数が合わないときはまずここを確かめます。- 並び順は保証されません。 取得したら
sorted()、連番には自然順のキーを使います。 - 複数の拡張子は
suffix.lower() in {".png", ".jpg"}と書くと、大文字小文字の違いにも対応できます。 - ファイルかフォルダかを判定しながら大量に処理するなら
os.scandir()が、5万件の計測で最も短い時間でした。 root_dir(3.10)、include_hidden(3.11)、Path.walk()とcase_sensitive(3.12) など、新しいバージョンほど書き方の選択肢が増えています。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Pythonファイル一覧の取得|globとos.listdir・再帰・拡張子」
株式会社RJCでは、Pythonでのファイル一覧の取得や整理の方法を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。