あるディレクトリに含まれるすべてのディレクトリやファイルを処理する場合、osモジュールのos.walk()関数を使うのが簡単です。

Pythonのos.walk()関数は指定されたディレクトリを起点(ルート)として、そこに含まれるディレクトリツリーを再帰的に辿ります。

サンプルとして次のようなディレクトリツリーを例に見ていきましょう。

test_dir/
├── dir1
│   ├── file1-1
│   └── file1-2
├── dir2
│   ├── file2-1
│   ├── file2-2
│   └── subdir
│       └── subfile
└── tmp -> /var/tmp(ディレクトリへのリンク)

ツリーのすべてのディレクトリを辿る

まずは、os.walk()関数の基本的な動作を見ていきましょう

os.walk()関数を呼び出すとジェネレータオブジェクトを返します。

このジェネレータはイテレートされるたびに、辿っているディレクトリの情報を含むタプル(dirpath, dirnames, filenames)を返します。タプルのそれぞれの要素は次の通りです。

dirpath
ディレクトリのパス(文字列)
dirnames
ディレクトリに含まれるディレクトリのリスト(文字列のリスト)
filenames
ディレクトリに含まれるファイルのリスト(文字列のリスト)

実際に例を見た方が理解が早いでしょう次の例はforでイテレートするごと、ジェネレータから返されるタプルを表示します。

>>> import os
>>> for root, dirs, files in os.walk('test_dir'):
...   print('root : {}'.format(root))
...   print('dirs : {}'.format(dirs))
...   print('files: {}'.format(files))
...   print('-----')
...
root : test_dir
dirs : ['dir2', 'dir1', 'tmp']
files: []
-----
root : test_dir/dir2
dirs : ['subdir']
files: ['file2-2', 'file2-1']
-----
root : test_dir/dir2/subdir
dirs : []
files: ['subfile']
-----
root : test_dir/dir1
dirs : []
files: ['file1-2', 'file1-1']
-----

最初にos.walk()関数の引数に渡されたディレクトリのパス、そしてそのディレクトリに含まれるディレクトリのリストおよびファイルのリストを要素とするタプルが返されます。その後にサブディレクト、またそのサブディレクトリと辿っているのがわかります。

ディレクトリへのシンボリックリンク(test_dir/tmp)は、dirsのリストに含まれます。os.walk()関数はデフォルトでリンクを辿りません。

os.walk()関数は引数に渡されたディレクトリをルートとして、トップダウンにディレクトリを走査します。このデフォルトの動作を変更するには「topdown=False」を指定します。そうするとボトムアップでディレクトリを走査します。

>>> for root, dirs, files in os.walk('test_dir', topdown=False):
...   print('root : {}'.format(root))
...   print('dirs : {}'.format(dirs))
...   print('files: {}'.format(files))
...   print('-----')
...
root : test_dir/dir2/subdir
dirs : []
files: ['subfile']
-----
root : test_dir/dir2
dirs : ['subdir']
files: ['file2-2', 'file2-1']
-----
root : test_dir/dir1
dirs : []
files: ['file1-2', 'file1-1']
-----
root : test_dir
dirs : ['dir2', 'dir1', 'tmp']
files: []
-----

すべてのファイルを辿る

os.walk()関数を使う目的の一つは、ディレクトリの中にあるすべてファイルに対して何かしら処理をしたい場合です。

しかし、filesリストの要素はファイル名だけでパスの情報がありませんので、このままではファイル対する処理ができません。このような場合。os.path.join()関数を使ってファイルのパスを作ります。

すべてのファイルに対して処理をする次の例を見てみましょう。この例はすべてのファイルの名前に「.txt」を追加します。わかりやすいように、join()で構築したファイルのパスも表示しています。

>>> for root, dirs, files in os.walk('test_dir'):
...   for f in files:
...     path = os.path.join(root, f)
...     print(path)
...     os.rename(path, path + '.txt')
...
test_dir/dir2/file2-2
test_dir/dir2/file2-1
test_dir/dir2/subdir/subfile
test_dir/dir1/file1-2
test_dir/dir1/file1-1

走査対象から特定のディレクトリを除外する

繰り返し中にdirnamesリストをdel、remove、スライスの代入などを使ってその場で変更することができます。そうすると、変更後のdirnamesリストに従って処理が継続されます。

例えば、dirnamesリストを並べ替えてディレクトリを辿る順番を変更したり、要素を削除してそのディレクトリを辿らないようにすることできます。

次の例では「dir2」というディレクトリは辿られません。

>>> for root, dirs, files in os.walk('test_dir'):
...   print('root       : {}'.format(root))
...   print('before dirs: {}'.format(dirs))
...   if 'dir2' in dirs:
...     dirs.remove('dir2')
...   print('after dirs : {}'.format(dirs))
...   print('files:       {}'.format(files))
...   print('-----')
...
root       : test_dir
before dirs: ['dir2', 'dir1', 'tmp']
after dirs : ['dir1', 'tmp']
files:       []
-----
root       : test_dir/dir1
before dirs: []
after dirs : []
files:       ['file1-2', 'file1-1']
-----

dirnamesリストの変更はトップダウンで走査するときだけ意味があります。ボトムアップするときには、既に親ディレクトリのdirnamesリストが作られているので、変更しても効果ありません。

シンボリックリンクを辿る

デフォルトでシンボリックリンクを辿らないのは前述しました。もしシンボリックリンクも辿りたければ「followlinks=True」を引数に渡します。

次は/var/tmpへのシンボリックリンクであるtest_dir/tmpを辿る例です。

>>> for root, dirs, files in os.walk('test_dir', followlinks=True):
...   print('root : {}'.format(root))
...   print('dirs : {}'.format(dirs))
...   print('files: {}'.format(files))
...   print('-----')
...
root : test_dir
dirs : ['dir2', 'dir1', 'tmp']
files: []
-----

...

root : test_dir/tmp
dirs : ['.ftp.XBMwyo', 'kernel_panics']
files: ['hosts', 'filesystemui.socket']
-----
root : test_dir/tmp/.ftp.XBMwyo
dirs : []
files: []
-----
root : test_dir/tmp/kernel_panics
dirs : []
files: []
-----

まとめ

あるディレクトリ配下のすべてのファイル処理することはよくあります。是非、os.walk()の使い方をマスターして使ってみてください。