home changes contents help options

145:ディレクトリ内のファイルを再帰的に処理

os モジュールの walk 関数を用いると、あるディレクトリ内のファイルを再帰的に探索することができます。

次のようにすると、ディレクトリ spam 内のファイルのパスが得られます。

import os

for root, dirs, files in os.walk(u'root_dir'):
    for file_ in files:
        full_path = os.path.join(root, file_)
        print full_path

print full_path の部分を希望する処理内容に置き換えてください。

使用例

カレントディレクトリに含まれるファイルのファイル名先頭に文字列 python_recipe_ をつけます。

import os

for root, dirs, files in os.walk(u'.'):
    for file_ in files:
        oldfilename = file_
        oldfilepath = os.path.join(root, oldfilename)
        newfilename = u'python_recipe_' + oldfilename
        newfilepath = os.path.join(root, newfilename)
        os.rename(oldfilepath, newfilepath)

ディレクトリ root_dir に含まれる、拡張子 .txt のファイルををすべて読み込んで all.txt 1つにまとめます。

import os

with open(u'all.txt', 'w') as all_txt:
    for root, dirs, files in os.walk(u'root_dir'):
        for file_ in files:
            if os.path.splitext(file_)[1] == u'.txt':
                with open(os.path.join(root, file_)) as txt:
                    all_txt.write(txt.read())

ex_change 関数はディレクトリに含まれるファイルの拡張子を変更します。

import os

def ex_change(dir, ex):
       for root, dirs, files in os.walk(dir):
           for file_ in files:
               oldfilename = file_
               newfilename = u'.'.join([
                   os.path.splitext(oldfilename)[0],
                   ex])

               oldfilepath = os.path.join(root, oldfilename)
               newfilepath = os.path.join(root, newfilename)
               os.rename(oldfilepath, newfilepath)

 # 使用例: root_dir 内のファイルの拡張子を .mp3 に書き換える
 ex_change(u'root_dir', u'mp3') 

os.walk について

os.walk で得られるもの

os.walk は 3 要素のタプルを返すジェネレータです。それぞれ、「今調べている最中のディレクトリ名」「見つかったディレクトリのリスト」「見つかったファイルのリスト」です。

次のようなディレクトリ root_dir を調べるとこのようになります。

root_dir --- file0
          |- file1
          |
          |- dir0 ---- file2
          |         |- file3
          |
          |- dir1 ---- file4
          |         |- file5
          |         |
          |         |- dir4 ---- file8
          |                   |- file9
          |
          |- dir2
          |
          |- dir3 --- file6
                   |- file7
>>> for x in os.walk('root_dir'):
...     print x
...
('root_dir', ['dir0', 'dir1', 'dir2', 'dir3'], ['file0', 'file1'])
('root_dir\\dir0', [], ['file2', 'file3'])
('root_dir\\dir1', ['dir4'], ['file4', 'file5'])
('root_dir\\dir1\\dir4', [], ['file8', 'file9'])
('root_dir\\dir2', [], [])
('root_dir\\dir3', [], ['file6', 'file7'])

探索順序

os.walk 関数には探索順を指定するための topdown 引数があります。True のときはトップダウンで、 False のときはボトムアップでディレクトリを探索します。デフォルト値は True です。

トップダウン時の特徴

今調べているディレクトリ内のファイルリストを先に作り、その後、各ディレクトリ内を調べに行きます。

ディレクトリ root_dir をトップダウンで探索した場合、次の順序でアクセスします。

root_dir
root_dir\dir0
root_dir\dir1
root_dir\dir2
root_dir\dir3
root_dir\dir1\dir4

普段はこちらを使用するとよいでしょう。トップダウンで問題があるのはディレクトリに変更を加える処理をおこなうときです。これについてはボトムアップの項で説明します。

os.walk をトップダウンで使っているときは、処理中にディレクトリリストを変更することで、その後の探索対象を変更することができます。

'dir3' ディレクトリ以下を調べないようにするには dirs リストから 'dir3' を取り除きます。

>>> for root, dirs, files in os.walk(u'root_dir'):
...   dirs[:] = (dir for dir in dirs if dir != u'dir3')
...   for file_ in files:
...     print os.path.join(root, file_)
...
root_dir\file0
root_dir\file1
root_dir\dir0\file2
root_dir\dir0\file3
root_dir\dir1\file4
root_dir\dir1\file5
root_dir\dir1\dir4\file8
root_dir\dir1\dir4\file9

ボトムアップ時の特徴

今調べているディレクトリのファイルを後回しにして次の各ディレクトリ内を調べに行きます。

ディレクトリ root_dir ボトムアップで探索した場合、次の順序でアクセスします。

root_dir\dir1\dir4
root_dir\dir0
root_dir\dir1
root_dir\dir2
root_dir\dir3
root_dir

ボトムアップはディレクトリの変更作業などに向いています。

ためしに root_dir ディレクトリ内のすべてのディレクトリ名に _new を加える処理を今まで使っていたトップダウンで行い、どうなるのかを見てみましょう。

>>> for root, dirs, files in os.walk(u'root_dir', topdown=True):
...     for dir in dirs:
...         olddir = os.path.join(root, dir)
...         newdir = os.path.join(root, dir + '_new')
...         os.rename(olddir, newdir)
...         print newdir
...
root_dir\dir0_new
root_dir\dir1_new
root_dir\dir2_new
root_dir\dir3_new

dir4 の処理が抜け落ちてしまっています。dir1 が dir1_new に先に変更されてしまうので、 dir1\dir4 も dir1_new\dir4 になります。このため dir1\dir4 はなくなってしまい、 dir1\dir4_new に変更することができなかったのです。

同様の処理をボトムアップで行ってみます。

>>> for root, dirs, files in os.walk('root_dir', topdown=False):
...     for dir in dirs:
...         olddir = os.path.join(root, dir)
...         newdir = os.path.join(root, dir + '_new')
...         os.rename(olddir, newdir)
...         print newdir
...
root_dir\dir1\dir4_new
root_dir\dir0_new
root_dir\dir1_new
root_dir\dir2_new
root_dir\dir3_new

こちらは意図したとおりに動作します。

処理中のカレントディレクトリの変更

os.walk は実行中にカレントディレクトリが変更されることはないと仮定しています。 os.chdir などを使わないようにしてください。

エラー処理

os.walk 関数には onerror 引数があり、例外発生時に処理を行う関数を渡すことができます。何も指定しなければ例外は無視され、処理続行となります。