pythonにてディレクトリを再帰的にコピーする場合、shutil.copytreeを使用できます。このとき引数ignoreを使用すると指定したglobパターンにマッチするファイル、ディレクトリを除いてコピーができますが、指定の仕方がちょっと特殊(shutil.ignore_patterns()にてコールバック関数を生成)だったので、
- 無視したいglobパターンをリストで指定(
ignore_patterns()は可変長引数を取るため、iterableで渡せない) - 除くファイルではなく含むファイルを指定
みたいなことができないようです。事務作業するときにちょっと困ったので、自分なりにignore_patterns()に代わる関数を書いてみました。
環境は以下。
- python==3.8.9
結論
以下コード参照
in_patternsにコピーしたい(=include)、ex_patternsにコピーしたくない(=exclude)ファイル/ディレクトリのglobパターンのリストを指定(どちらか1つでもよい)CopyTreeIgnoreをインスタンス化 (IgnPtn)shutil.copytree()のignore引数にIgnPtn.ignore()を渡す
すると、shutil.copytree()に指定したsrcのディレクトリを再帰的に探索し、include/excludeの設定を反映してコピーされます。ちなみにcopytree()は深さ優先探索です。再帰だから当たり前かもしれませんが。
補足説明
ignore関数
ignore指定する場合、公式ドキュメントにはignore_patterns()を使用してコールバック関数を生成せよと書いてあります。例えば*.txt, *.jpgにマッチするファイルを除いてディレクトリをコピーしたいとき、
# 無視するファイル、ディレクトリ名をglobパターンにて指定 # 可変長引数で受けるため、iterableな型は受け付けない ignptn = shutil.ignore_patterns('*.txt', '*.jpg') # src path src = 'path/to/src/directory' # dst path dst = 'path/to/dst/direcotry' # 引数にignptnを指定 shutil.copytree(src, dst, ignore=ignptn)
のように使用します。ignore_patterns()が生成する関数(ignore関数と呼ぶこととします)は、
- 引数を2つ(str, list[str])とる
- 無視するファイルのリスト(list[str])を返す
関数です。copytree()は対象ディレクトリを再帰的に探索し、それぞれのディレクトリ内でこのignore関数を呼び出します。ignore関数の戻り値に含まれるファイル・ディレクトリは無視してコピーします。
ignore関数をコードにおこすと、引数・戻り値は以下の通りです。
def ignore(directory: str, files: List[str]) -> List[str]: ''' directory:再帰探索中のディレクトリの絶対パス files:direcotry内のファイル一覧 戻り値:無視するファイルのパスのリスト(相対パス指定する場合はdirectoryを基準とする) '''
ignore_patterns()によるignore関数は「files内を探索し、指定のglobパターンとマッチする文字列を戻り値のリストに加える」ということをしています。
つまり、各ディレクトリ内でコピーしたいファイル以外のリストを返すような関数を書いてやれば、任意のファイルを除く/含むようにコピーすることが可能です。あとはそれっぽく実装するだけですね。
CopyTreeIgnore.ignore()はinclude/excludeを鑑みた結果、無視するファイル一覧をリストとして返しますが、重複がないようにsetによる集合演算を使用しています。「include設定によって無視されるファイル群」(_create_include_set()で生成)と「exclude設定によって無視されるファイル群」(_create_exclude_set()で生成)を求め、和集合を取っています。別に重複のせいで著しくパフォーマンスが落ちることはないと思いますが、なんとなくすっきりするので。
callbacks引数
CopytreeIgnore()はcallbacksという引数を取るようにしています。この引数に関数オブジェクトを与えてやることで、ignore()とは別にディレクトリを探索する毎に任意の関数を呼び出せるようにしています。まあinclude/excludeでほぼ事足りるのですが、ちょっとトリッキーなことをしたいときに別関数を実装して引数指定してやることで拡張可能にしました。自分の場合、
- 同名の別拡張子ファイルが同ディレクトリに存在している場合、片方だけをコピーする
- 特定の階層以下を無視する(ignore指定では名前が競合する場合などに有用)
- 他ソフトの動作待ちのために適宜sleepさせる
- コピーのログを出力する
みたいなことをしたかったので。。
まとめ
shutil.copytree()の引数ignoreに渡すコールバック関数を拡張したクラスCopyTreeIgnoreを実装しました。shutilでも意外と痒いところに手が届かないことがあるんですね。そもそもshutilはファイル操作の為の関数群なので多くを求めすぎかもしれませんが…