フォルダの中にあるCSVファイルを、まとめて読み込みたい。そんな場面に出会ったことはありませんか?
ファイルが3つなら名前を手で書けば済みます。けれど、毎日1つずつ増えていくログや、100個を超える売上データとなると、そうはいきません。
そこで活躍するのが、Pythonの標準ライブラリに入っているglobモジュールです。*.csvのようなパターンを渡すだけで、条件に合うファイルの一覧を返してくれます。
今回は、globの基本的な使い方から、初心者がつまずきやすい落とし穴までを順番に整理します。サンプルの実行結果は、手元のPython 3.11で確かめたものです。
globとは何をするモジュールなのか¶
globは、ワイルドカードと呼ばれる記号を使って、ファイルやフォルダの名前を探すためのモジュールです。名前の由来はUnixのシェルにあった仕組みで、ターミナルでls *.txtと打ったときと同じような感覚で使えます。
インストールは不要で、import globと書くだけで使えます。いちばん基本になるのは、条件に合うパスをリストで返すglob.glob()という関数です。
私は10年ほどエンジニアとして開発に関わってきましたが、データ集計のスクリプトを書くとき、最初の1行目はほぼ毎回globでした。ファイルを集める部分さえ決まれば、あとは中身を処理するだけになるからです。
3つのワイルドカードを覚えよう¶
globで使える記号は、実はそれほど多くありません。まずは次の3つを押さえておけば、日常のファイル探しはほとんどこなせます。
それぞれの意味を表にまとめてみました。
| 記号 | 意味 | 例 | マッチする名前の例 |
|---|---|---|---|
* |
0文字以上の任意の文字列 | *.txt |
a.txt、log10.txt |
| ? | ちょうど1文字 | log?.txt | log1.txt、log2.txt |
| [ ] | カッコ内のどれか1文字 | log[0-9].txt | log1.txt、log2.txt |
| [! ] | カッコ内以外の1文字 | [!a]*.txt |
b.txt、log1.txt |
** |
0個以上のフォルダ(recursive=True時のみ) | **/*.txt |
sub/deep/e.txt |
表の最後にある**は、少し特別な記号です。この記号だけは、ある引数を付けないと期待どおりに動きません。
実際に動かして確かめてみましょう。demoというフォルダに、いくつかのファイルを置いた状態で試しています。
import glob
# demoフォルダ直下の .txt をすべて探す
print(sorted(glob.glob('demo/*.txt')))
# ['demo/a.txt', 'demo/b.txt', 'demo/log1.txt', 'demo/log10.txt', 'demo/log2.txt']
# log の後ろがちょうど1文字のものだけ
print(sorted(glob.glob('demo/log?.txt')))
# ['demo/log1.txt', 'demo/log2.txt']
# 見つからないときはエラーではなく空のリスト
print(glob.glob('nothing/*.xyz'))
# []
?を使うと、log10.txtのように2文字のものが外れているのがわかります。数字の桁数で絞り込みたいときに便利です。
最後の例も大事なポイントです。条件に合うファイルが1つもなくても、globはエラーを出さずに空のリストを返します。
サブフォルダまで探すにはrecursive=Trueが必要¶
フォルダの中にさらにフォルダがあって、その奥まで全部探したい。そんなときは**を使います。
ところが、ここで多くの人がつまずきます。**を書いただけでは、すべての階層は探してくれないのです。
**だけでは1階層しか探さない¶
まずは、引数を付けずに**を使った場合の結果を見てみましょう。demoの中にsubフォルダ、さらにその中にdeepフォルダがある構成です。
import glob
# recursive を付けない場合
print(sorted(glob.glob('demo/**/*.txt')))
# ['demo/sub/d.txt']
# recursive=True を付けた場合
print(sorted(glob.glob('demo/**/*.txt', recursive=True)))
# ['demo/a.txt', 'demo/b.txt', 'demo/log1.txt', 'demo/log10.txt',
# 'demo/log2.txt', 'demo/sub/d.txt', 'demo/sub/deep/e.txt']
recursiveを付けないと、**はただの*と同じ扱いになります。そのため、ちょうど1階層下のsub/d.txtしか見つかりません。
recursive=Trueを付けると、**は0個以上のフォルダにマッチするようになります。0個も含むので、demo直下のファイルも結果に入っている点に注目してください。
エラーにならず、それらしい結果が返ってくるのがやっかいなところです。**を書いたらrecursive=Trueもセットで付ける、と覚えておきましょう。
大きなフォルダでは時間がかかることもある¶
公式ドキュメントでは、大きなディレクトリツリーで**を使うと、非常に長い時間がかかる場合があると注意されています。ホームフォルダ全体やネットワークドライブを丸ごと探すのは避けたほうが無難です。
探す範囲は、できるだけ必要なフォルダに絞りましょう。たとえばdata/2026/**/*.csvのように、入口を具体的にするだけでもずいぶん速くなります。
隠しファイルが結果に出てこない理由¶
Linuxやmacでは、名前がドットで始まるファイルは隠しファイルとして扱われます。.envや.gitignoreのようなファイルです。
globは、シェルの動きに合わせて、*や**でこうした隠しファイルにマッチしないようになっています。先ほどの例でも、demoの中にあった.env.txtや.hiddenフォルダの中身は結果に出ていませんでした。
隠しファイルも含めたい場合は、Python 3.11で追加されたinclude_hidden引数を使います。手元で試すと、次のような結果になりました。
import glob
files = glob.glob('demo/**/*.txt', recursive=True, include_hidden=True)
print(sorted(files))
# ['demo/.env.txt', 'demo/.hidden/f.txt', 'demo/a.txt', 'demo/b.txt', ...]
Python 3.10以前を使っている場合は、この引数がありません。その場合は、.*のようにドットから始まるパターンを別に書いて探す必要があります。
設定ファイルの棚卸しのように、隠しファイルこそが探したい対象という場面もあります。見つからないと思ったら、まずは隠しファイル扱いになっていないかを疑ってみてください。
結果の順番は保証されない¶
globの結果を表示してみると、きれいに並んでいるように見えることがあります。ですが、globの返す順番は保証されていません。
公式ドキュメントにも、結果が並んでいるかどうかはファイルシステムによると書かれています。手元のPCでは名前順に見えても、本番のサーバーでは違う順番になることがあるのです。
ここで、私自身の失敗をひとつ紹介します。日付ごとのCSVをglobで集めて上から順に処理するスクリプトを書いたところ、開発環境では正しく動いたのに、サーバーに載せたとたんに集計の順番がばらばらになりました。
原因は、まさにこの順番の違いでした。それ以来、順番に意味がある処理では、必ずsorted()で並べ替えるようにしています。
ただし、sorted()は文字列として並べるので、log10.txtがlog2.txtより先に来ます。数字の大きさで並べたい場合は、key引数を使ったひと工夫が必要です。
並べ替えの考え方は、こちらの記事でくわしく解説しています。
【関連記事】Pythonのsortedとsortの違いとは?
知っておくと便利な関数と引数¶
ここまでの内容で、基本的なファイル探しはできるようになりました。ここからは、一歩進んだ使い方を紹介します。
iglobで1件ずつ受け取る¶
glob.glob()は、見つかったパスをすべてリストにしてから返します。ファイルが何万件もあると、そのぶんメモリを使います。
そんなときは、glob.iglob()を使いましょう。使い方はglob()と同じですが、結果をジェネレータとして1件ずつ返してくれます。
root_dirで基準のフォルダを指定する¶
Python 3.10からは、root_dir引数で探しはじめるフォルダを指定できるようになりました。このとき返ってくるパスは、root_dirから見た相対パスになります。
import glob
print(sorted(glob.glob('*.txt', root_dir='demo')))
# ['a.txt', 'b.txt', 'log1.txt', 'log10.txt', 'log2.txt']
フォルダ名を除いたファイル名だけが欲しいときに便利です。ただし、ファイルを開くときにはフォルダ名を付け直す必要がある点に注意してください。
角カッコを含む名前はescapeで守る¶
ファイル名に角カッコが入っていると、思わぬ結果になることがあります。たとえばdemoフォルダに[draft].txtという名前のファイルを追加して探すと、次のようになります。
import glob
print(glob.glob('demo/[draft].txt'))
# ['demo/a.txt']
print(glob.glob(glob.escape('demo/[draft].txt')))
# ['demo/[draft].txt']
[draft]が、d・r・a・f・tのどれか1文字という意味に解釈されてしまったのです。その結果、a.txtが見つかってしまいました。
glob.escape()を通すと、記号がただの文字として扱われます。ユーザーが入力したファイル名をそのままパターンに使うときは、必ずescapeしておきましょう。
pathlibのglobとの使い分け¶
実は、ファイルを探す方法はglobモジュールだけではありません。pathlibのPathオブジェクトにも、glob()とrglob()というメソッドがあります。
2つの違いを表で比べてみましょう。
| 比べるポイント | globモジュール | pathlibのglob |
|---|---|---|
| 返ってくるもの | 文字列のリスト | Pathオブジェクトのジェネレータ |
| サブフォルダまで探す | recursive=Trueと**を指定 |
rglob()を使う |
| 隠しファイル | 既定では含まない | 手元の3.11では含まれた |
| 向いている場面 | 短いスクリプト、文字列で扱いたいとき | パスの加工や読み書きまで続けたいとき |
注意したいのは、隠しファイルの扱いです。手元のPython 3.11で試したところ、Path('demo').glob('*.txt')の結果には.env.txtが含まれていました。
同じ*.txtでも、どちらを使うかで結果が変わるわけです。乗り換えるときは、一度結果を見比べておくと安心です。
pathlibの基本的な使い方は、こちらの記事にまとめています。
【関連記事】Pythonのpathlibとは?
実践例:フォルダ内のCSVをまとめて処理する¶
最後に、ここまでの内容を組み合わせた例を見てみましょう。dataフォルダとそのサブフォルダにあるCSVを集めて、それぞれの行数を表示するスクリプトです。
import csv
import glob
import os
def count_rows(pattern):
# 順番をそろえるために sorted() で並べ替える
paths = sorted(glob.glob(pattern, recursive=True))
if not paths:
print('対象のファイルが見つかりませんでした')
return
for path in paths:
with open(path, encoding='utf-8', newline='') as f:
rows = sum(1 for _ in csv.reader(f))
print(f'{os.path.basename(path)}: {rows}行')
count_rows('data/**/*.csv')
recursive=Trueとsorted()をセットにしているのがポイントです。さらに、ファイルが見つからなかったときのメッセージも入れています。
globは見つからなくてもエラーを出さないので、何も起きずに終わってしまうことがあります。空のときに一言出すだけで、パターンの書き間違いにすぐ気づけるようになります。
集めたファイルをコピーしたり移動したりしたい場合は、shutilと組み合わせるのが定番です。ファイル操作の基本とあわせて、次の記事も参考にしてください。
【関連記事】Pythonのshutilとは?
【関連記事】Pythonのosモジュールとは?
まとめ¶
今回は、globモジュールを使ってファイルをまとめて探す方法を紹介しました。最後に、つまずきやすいポイントを表で振り返っておきましょう。
| 落とし穴 | 対策 |
|---|---|
**が奥まで探してくれない |
recursive=Trueを付ける |
| 隠しファイルが出てこない | include_hidden=Trueを付ける(3.11以降) |
| 結果の順番が環境で変わる | sorted()で並べ替える |
| 角カッコ入りの名前で誤マッチ | glob.escape()を通す |
| 見つからなくても無反応 | 空のリストかどうかを確認する |
globは数行で書けるぶん、思いどおりに動いていないことに気づきにくい道具です。結果を一度printして確かめるクセをつけるだけで、多くのトラブルは防げます。
ファイルを1つずつ手で指定していた作業が、パターン1つで片づくようになると、自動化の楽しさがぐっと増します。ぜひ手元のフォルダで試してみてください。
ここまでお読みいただきありがとうございました。