指定ディレクトリ配下のサブディレクトリやファイルを洗い出したい
普段、作業をしている中で作業ディクレクトリ配下がどんなサブディレクトリになっているか?
どんなファイルがあるんだっけか?と、必要に迫られるときが、ないと思っていても往々にしてあります。
そんなとき、Windows powershell であれば tree /f コマンド、linux/macであればlsコマンドなどで、
調査するのが普通なのですが、カスタマイズできないところが、げに残念。
じゃあ、オレオレプログラミングで、ディレクトリ調査コマンドなどを作ってみるか!というところですが、
正直、楽にこなしたい。
んで、pythonに目を向けると、これまた便利な組み込みAPIがあるようで、こいつを利用しない手はないでしょうと!
os.path.walk( )を使う
os.pathモジュールには、様々なディレクトリ操作関数が提供されています。
その中でも、再帰的に指定ディレクトリ配下を走査する機能をもつAPIが、 os.walk( )
一旦は、こいつを下記のフォルダ&ファイル構成を持ったディレクトリ(walk-around)に対して、単純に利用してみます。
図1.解析対象となるディレクトリ
単純なコード
# -*- coding: utf-8 -*-
import os
import sys
def walk_around_path_simple(rd):
for rt, dirs, fls in os.walk(rd):
print rt
print dirs
print fls
if __name__ == '__main__':
argv = sys.argv
root_dir = argv[1]
walk_around_path_simple(root_dir)
quit()
リスト 1
上記のプログラムに引数として、walk-aroundパスを与えると、下の結果を得ます。
図2. 出力結果
ルートディレクトリのwalk-aroundをベースに
dir1、dir2、memo、sourceディレクトリがリスト型で出力され、
順次、dir1、dir2、memoディレクトリ配下のファイルが再帰的に表示されている様子が見えます。
ですが、
pythonのリスト型で、そのままプレーンで出力されるので、ちょっと分かりにくいですし、
どっちかというと、print で直接標準出力に出すより、変数として結果を得たいですよね。
なるべく上の構造を変更させずに結果を得たい...じゃあどうするか?
pythonのジェネレータ機能を利用して、コルーチンを実現を試行してみます。
ジェネレータで コルーチンしてみる
その前に、サブルーチン(シングルスレッドでの動作を想定)のモデルについて
通常、プログラマが意識する関数の動作は、関数先頭から末尾まで、
処理が完了してから、一つ以上の "結果" を、呼び元に返す(※)動作です。
(※)原理主義的に考えたとき、関数は結果を一つだけ返すべき!だとは思っています。
対して、コルーチンは?
端的にいうと、
関数の処理途中でその時点での結果を、呼び元に返し、更に再び、処理途中から処理を再開するモデルです。
コルーチンざっくりイメージ
コルーチン <----------コルーチン コール
0. 処理実行
1. この時点での
処理結果を返す-------->コルーチン呼び出し側へ
2. 処理再開
3. この時点での
処理結果を返す-------->コルーチン呼び出し側へ
4. 処理再開
以下、続く
この間、コルーチンのコンテキストは保持されている。
pythonでどうやる?
pythonでは、コルーチンをジェネレータ機能を使って実現します。
ここで、ジェネレータ機能とはなんぞや?はてな?となりますが、
これはイテレータを、その都度作成し、結果を返す機能です。
そのため、pythonのコルーチンは反復的な性格を持つことになります。
ジェネレータ機能を持つpythonのyieldステートメントは、
結果を返すイテレータを生成するとともに、呼び出し側に結果とともに、制御を渡します。
yieldを使ったwalk_around_path
# -*- coding: utf-8 -*-
import os
import sys
from os.path import join
def walk_around_path(rd):
for rt, dirs, fls in os.walk(rd):
yield rt
for fl in fls:
yield os.path.join(rt, fl)
if __name__ == '__main__':
argv = sys.argv
root_dir = argv[1]
for file in walk_around_path(root_dir):
print file
quit()
リスト 2
図3. 出力結果
以降は、ここまでのプログラムをカスタマイズしていきます。
カスタマイズの方針
機能をカスタマイズするとき、状況に応じてなるべく柔軟に、手軽にカスタマイズしたいです。
理想を言えば、プログラムを書き換えずそれが実現出来ればいいですよね。
lispなど、多くの関数型 プログラミング言語には、eval 式が導入されていて、
文字列として与えられた式をプログラムとして実行できる機能(リフレクション)を持ちます。
pythonにも、eval・exec が導入され、それぞれ下記の機能を持ちます。
eval : 単一のステートメントを評価実行
ex.
>>> val = 100
>>> print eval('val * 100')
10000
exec : 複数のステートメントを評価実行
ex.
>>> val = 100
>>> exec("""if val == 100:
... print 'val is 100'
... else:
... print 'val is not 100'
... """)
val is 100
リフレクションを用いて、簡単にプログラムを可変に出来たら...ソイツは最高です。
リフレクション -その1 特定ディレクトリに対する操作
リフレクション( eval ・exec )のお話はちょっとおいて...(後でこれを利用します)
ディレクトリ・ファイル走査で、特定のディレクトリは走査対象外としたい場合があります。
走査対象外としたいディレクトリがやたらネストが深く、走査に時間が掛かる...といった場合。
そういう時、os.walk ( ) が返すタプルにあるサブディレクトリ リストから、
走査対象外としたいディレクトリを削除すると以降、走査対象外となります。
ex.
走査対象外としたいディレクトリがmemoディレクトリの時、下記のようにしてあげればオッケー。
☆dirnames.remove('memo')
os.walk( ) が返すタプル -> (dirpath, dirnames, filenames)
dirpath : ルートディレクトリ
dirnames : サブディレクトリのリスト
filenames : ファイル名のリスト
14.1.4 ファイルとディレクトリ
http://docs.python.jp/2.5/lib/os-file-dir.html より
os.walk ( ) のタプル出力イメージは、図2.出力結果を参考のこと
ひとまずコイツを素直にコーディングすると...
memoフォルダは走査対象としない
# -*- coding: utf-8 -*-
import os
import sys
from os.path import join
def walk_around_path_remove_target(rd):
for root, dirs, files in os.walk(rd):
yield root
if 'memo' in dirs:
dirs.remove('memo')
for file in files:
yield os.path.join(root, file)
if __name__ == '__main__':
argv = sys.argv
root_dir = argv[1]
for file in walk_around_path_remove_target(root_dir):
print file
quit()
リスト 3
となります。
図4. 出力結果
走査対象に、memoフォルダ配下が含まれていないことが確認できます。
ただディレクトリに対する操作部分、用途によってカスタマイズを容易にしたいので、
exec ( ) に、ディレクトリ操作部分のコード文字列を渡し、可変できるよう工夫します。
execによるリフレクション ディレクトリ操作
# -*- coding: utf-8 -*-
import os
import sys
from os.path import join
def walk_around_path_exec(rd, cmd_for_dir):
for rt, dirs, fls in os.walk(rd):
yield rt
exec(cmd_for_dir)
for fl in fls:
yield os.path.join(rt, fl)
if __name__ == '__main__':
argv = sys.argv
root_dir = argv[1]
# プログラム引数に、☆で示したプログラムリストを与えてもOK
cmd_for_dir = argv[2]
# ☆
cmd_for_dir = """
if 'memo' in dirs:
dirs.remove('memo')
"""
for file in walk_around_path_exec(root_dir, cmd_for_dir):
print file
quit()
リスト 4
結果は、図4.出力結果と同じ。
利点と難点
この手法のメリットは、プログラムの基本構造を変えることなく、カスタマイズが可能なこと。
難点として、デバッグが難しかったり、安全面とセキュリティ面で不安が残る点です。
安全面でいうと、
破壊的なディレクトリ削除をうっかりしてしまったり、
セキュリティ面でいうと、
悪意をもったユーザに任意のコードを実行させる隙を作ってしまうこと(SQLインジェクションの手法に似ている)があります。
ただ、強力かつ便利な手法なので僕は使いどころと、用途を選んで活用しています。
リフレクション -その2 特定ファイルに対する操作
もう一つリフレクションの応用。
特定サイズ(1000Byte)以上のファイルだけ、標準出力にプロンプトしたい場合。
今回は、deadmanwalking.pyというファイルが1000Byte超過しているとします。
普通に書くのであれば、ファイル探索してる部分で、
if os.path.getsize(os.path.join(rt, fl)) >= 1000:
print 'file size is over 1000 byte -> ' + fl
とかしてあげれば良いです。
とはいえ、ここも色々カスタマイズするシチュエーションがあるでしょうから、
コードを exec ( ) に文字列で渡すカタチにした方が、便利です。
なので、下のようにしてあげれば良いです。
execによるリフレクション ファイル操作
# -*- coding: utf-8 -*-
import os
import sys
from os.path import join
def walk_around_path_exec(rd, cmd_for_dir, cmd_for_fl):
for rt, dirs, fls in os.walk(rd):
yield rt
exec(cmd_for_dir)
for fl in fls:
yield os.path.join(rt, fl)
exec(cmd_for_fl)
if __name__ == '__main__':
argv = sys.argv
root_dir = argv[1]
# プログラム引数に、☆1で示したプログラムリストを与えてもOK
cmd_for_dir = argv[2]
# ☆1
cmd_for_dir = """
if 'memo' in dirs:
dirs.remove('memo')
"""
# プログラム引数に、☆2で示したプログラムリストを与えてもOK
cmd_for_file = argv[3]
# ☆2
cmd_for_file = """
if os.path.getsize(os.path.join(rt, fl)) >= 1000:
print 'file size is over 1000 byte -> ' + file
"""
for file in walk_around_path_exec(root_dir, cmd_for_dir, cmd_for_file):
print file
quit()
リスト 5
図5. 出力結果
環境と必要モジュール
OS : Windows 8 64bit
Python : Ver 2.7.8
Pythonモジュール :
os
sys
join from os.path