Pythonで、os.walk()を使って、特定のディレクトリを除いたファイル一覧を取得する

Pythonで、特定のディレクトリを除いたファイル一覧を取得することがあったため、メモを残します。

 
目次

環境

 
また、ディレクトリやファイル構成は以下の通りです。

$ pwd
path/to/root/e.g._os_walk

$ tree
.
├── bar
│   ├── foo.txt
│   ├── fuga
│   │   ├── ham.txt
│   │   └── spam.txt
│   └── hoge
│       ├── ham.txt
│       └── spam.txt
├── baz
│   ├── fuga
│   └── hoge
├── foo
│   ├── foo.txt
│   └── hoge
│       ├── ham.txt
│       └── spam.txt
├── make_file.py
└── os_walk.py

 
上記の構成を再現する場合は、以下のPythonスクリプトを流します。

今回は、pathlibのPathでtouch()でファイルを作成しています。
utility - Implement touch using Python? - Stack Overflow

from pathlib import Path

# parents=Trueとして、親ディレクトリがない場合は同時に作成
Path('foo/hoge').mkdir(parents=True, exist_ok=True)
Path('foo/foo.txt').touch()
Path('foo/hoge/spam.txt').touch()
Path('foo/hoge/ham.txt').touch()
Path('bar/hoge').mkdir(parents=True, exist_ok=True)
Path('bar/fuga').mkdir(parents=True, exist_ok=True)
Path('bar/foo.txt').touch()
Path('bar/hoge/spam.txt').touch()
Path('bar/hoge/ham.txt').touch()
Path('bar/fuga/spam.txt').touch()
Path('bar/fuga/ham.txt').touch()
Path('baz/hoge').mkdir(parents=True, exist_ok=True)
Path('baz/fuga').mkdir(parents=True, exist_ok=True)

 

os.walk()の動き

こんな感じのPythonスクリプトを用意し、動作を確認します。

root_dir = os.path.abspath(os.path.dirname(__file__))

for root, dirs, files in os.walk(root_dir):
    print('-'*10)
    print('root:{}'.format(root))
    print('dirs:{}'.format(dirs))
    print('files:{}'.format(files))

 
実行すると、bar -> baz -> fooの順でディレクトリを検索しているようでした。

$ python os_walk.py 
----------
root:path/to/root/e.g._os_walk
dirs:['bar', 'baz', 'foo']
files:['make_file.py', 'os_walk.py']
----------
root:path/to/root/e.g._os_walk/bar
dirs:['fuga', 'hoge']
files:['foo.txt']
----------
root:path/to/root/e.g._os_walk/bar/fuga
dirs:[]
files:['ham.txt', 'spam.txt']
----------
root:path/to/root/e.g._os_walk/bar/hoge
dirs:[]
files:['ham.txt', 'spam.txt']
----------
root:path/to/root/e.g._os_walk/baz
dirs:['fuga', 'hoge']
files:[]
----------
root:path/to/root/e.g._os_walk/baz/fuga
dirs:[]
files:[]
----------
root:path/to/root/e.g._os_walk/baz/hoge
dirs:[]
files:[]
----------
root:path/to/root/e.g._os_walk/foo
dirs:['hoge']
files:['foo.txt']
----------
root:path/to/root/e.g._os_walk/foo/hoge
dirs:[]
files:['ham.txt', 'spam.txt']

 

ファイルの一覧を取得

os.walk()を使って、ファイル一覧を取得してみます。

root_dir = os.path.abspath(os.path.dirname(__file__))

target_files = []
for root, dirs, files in os.walk(root_dir):
    targets = [os.path.join(root, f) for f in files]
    target_files.extend(targets)

for f in target_files:
    print(f)

 
実行すると、空ディレクトリ(/baz/hoge/baz/fuga)以外のファイル一覧を取得できました。

path/to/root/e.g._os_walk/make_file.py
path/to/root/e.g._os_walk/os_walk.py
path/to/root/e.g._os_walk/bar/foo.txt
path/to/root/e.g._os_walk/bar/fuga/ham.txt
path/to/root/e.g._os_walk/bar/fuga/spam.txt
path/to/root/e.g._os_walk/bar/hoge/ham.txt
path/to/root/e.g._os_walk/bar/hoge/spam.txt
path/to/root/e.g._os_walk/foo/foo.txt
path/to/root/e.g._os_walk/foo/hoge/ham.txt
path/to/root/e.g._os_walk/foo/hoge/spam.txt

 

特定のディレクトリを除いたファイルの一覧を取得

公式ドキュメントを読むと、

topdown が True のとき、呼び出し側は dirnames リストを、インプレースで ( たとえば、 del やスライスを使った代入で ) 変更でき、 walk() は dirnames に残っているサブディレクトリ内のみを再帰します。これにより、検索を省略したり、特定の訪問順序を強制したり、呼び出し側が walk() を再開する前に、呼び出し側が作った、または名前を変更したディレクトリを、 walk() に知らせたりすることができます。

os.walk() | 16.1. os — 雑多なオペレーティングシステムインタフェース — Python 3.6.1 ドキュメント

とのことでした。

os.walk()の引数topdownのデフォルト値はTrueのため、dirnamesリストを変更すれば良さそうです。

今回は、hogeディレクトリ以下を除外したファイルの一覧を作成してみます。

 

スライス(dirs[:])で差し替え

まずは、dirsをスライス(dirs[:])にて差し替えます。

root_dir = os.path.abspath(os.path.dirname(__file__))

target_files = []
for root, dirs, files in os.walk(root_dir):
    dirs[:] = [d for d in dirs if 'hoge' not in os.path.join(root, d)]
    targets = [os.path.join(root, f) for f in files]
    target_files.extend(targets)

for f in target_files:
    print(f)

 
実行してみると、

  • path/to/root/os_walk/bar/hoge/ham.txt
  • path/to/root/e.g._os_walk/bar/hoge/spam.txt
  • path/to/root/e.g._os_walk/foo/hoge/ham.txt
  • path/to/root/e.g._os_walk/foo/hoge/spam.txt

が除外されたファイル一覧を取得できました。

path/to/root/e.g._os_walk/make_file.py
path/to/root/e.g._os_walk/os_walk.py
path/to/root/e.g._os_walk/bar/foo.txt
path/to/root/e.g._os_walk/bar/fuga/ham.txt
path/to/root/e.g._os_walk/bar/fuga/spam.txt
path/to/root/e.g._os_walk/foo/foo.txt

 
念のため、この時のos.walk()の挙動も確認します。

root_dir = os.path.abspath(os.path.dirname(__file__))

target_files = []
for root, dirs, files in os.walk(root_dir):
    dirs[:] = [d for d in dirs if 'hoge' not in os.path.join(root, d)]
    print('-'*10)
    print('root:{}'.format(root))
    print('dirs:{}'.format(dirs))
    print('files:{}'.format(files))
    targets = [os.path.join(root, f) for f in files]
    target_files.extend(targets)

 
実行してみると、hogeディレクトリが結果に含まれていませんでした。

----------
root:path/to/root/e.g._os_walk
dirs:['bar', 'baz', 'foo']
files:['make_file.py', 'os_walk.py']
----------
root:path/to/root/e.g._os_walk/bar
dirs:['fuga']
files:['foo.txt']
----------
root:path/to/root/e.g._os_walk/bar/fuga
dirs:[]
files:['ham.txt', 'spam.txt']
----------
root:path/to/root/e.g._os_walk/baz
dirs:['fuga']
files:[]
----------
root:path/to/root/e.g._os_walk/baz/fuga
dirs:[]
files:[]
----------
root:path/to/root/e.g._os_walk/foo
dirs:[]
files:['foo.txt']

 

remove(dirs.remove('hoge'))で差し替え

続いて、dirsdirs.remove('hoge')にて差し替えます。

list.remove()は該当するものがない場合は例外を送出することに注意します。
python - Is there a simple way to delete a list element by value? - Stack Overflow

target_files = []
for root, dirs, files in os.walk(root_dir):
    try:
        dirs.remove('hoge')
    except:
        pass

    targets = [os.path.join(root, f) for f in files]
    target_files.extend(targets)

for f in target_files:
    print(f)

 
実行結果はスライスと同じだったため、省略します。

 

代入で差し替え (NG)

スライスを使わない代入で差し替えてみます。

    target_files = []
    for root, dirs, files in os.walk(root_dir):
        dirs = [d for d in dirs if 'hoge' not in os.path.join(root, d)]
        print('-'*10)
        print('root:{}'.format(root))
        print('dirs:{}'.format(dirs))
        print('files:{}'.format(files))

        targets = [os.path.join(root, f) for f in files]
        target_files.extend(targets)

    for f in target_files:
        print(f)

 
実行するとhogeディレクトリが含まれたままでした。 公式ドキュメントのように、スライスなどを使うのが良さそうです。

----------
root:path/to/root/e.g._os_walk
dirs:['bar', 'baz', 'foo']
files:['make_file.py', 'os_walk.py']
----------
root:path/to/root/e.g._os_walk/bar
dirs:['fuga']
files:['foo.txt']
----------
root:path/to/root/e.g._os_walk/bar/fuga
dirs:[]
files:['ham.txt', 'spam.txt']
----------
root:path/to/root/e.g._os_walk/bar/hoge
dirs:[]
files:['ham.txt', 'spam.txt']
----------
root:path/to/root/e.g._os_walk/baz
dirs:['fuga']
files:[]
----------
root:path/to/root/e.g._os_walk/baz/fuga
dirs:[]
files:[]
----------
root:path/to/root/e.g._os_walk/baz/hoge
dirs:[]
files:[]
----------
root:path/to/root/e.g._os_walk/foo
dirs:[]
files:['foo.txt']
----------
root:path/to/root/e.g._os_walk/foo/hoge
dirs:[]
files:['ham.txt', 'spam.txt']
path/to/root/e.g._os_walk/make_file.py
path/to/root/e.g._os_walk/os_walk.py
path/to/root/e.g._os_walk/bar/foo.txt
path/to/root/e.g._os_walk/bar/fuga/ham.txt
path/to/root/e.g._os_walk/bar/fuga/spam.txt
path/to/root/e.g._os_walk/bar/hoge/ham.txt
path/to/root/e.g._os_walk/bar/hoge/spam.txt
path/to/root/e.g._os_walk/foo/foo.txt
path/to/root/e.g._os_walk/foo/hoge/ham.txt
path/to/root/e.g._os_walk/foo/hoge/spam.txt

 

ソースコード

GitHubに上げました。e.g._os_walkディレクトリ以下が今回のファイルです。
thinkAmi-sandbox/python_misc_samples