この記事では,pythonを用いて2つのディレクトリーの間で格納さりているディレクトリーやファイルが等しいかを確認する方法を解説する.
最初にbashのdiffコマンドを呼び出す方法,次にpythonのmoduleのfilecmpを用いた方法を紹介する.後者を使うメリットとしては,ファイルが無かったり,違う部分だけを探す場合に早いことが挙げられる.diffコマンドはどこが違うまで出力するため,データサイズが大きくなると時間が掛かる.
このプログラムを作成(moduleをいじるだけ)するに至った経緯としては,HDDからHDDにデータをバックアップする最中にエラーが起きてしまった.その時に,どこまでしっかりバックアップ出来ているか確認する必要が生まれたからだ.
下準備
動作はjupyter notebook上を想定している.また,使っているパソコンmacOS Mojave ver. 10.14.6である.
今回使用するmoduleは以下のもの.
import os
import subprocess
import inspect
import filecmp
サンプルデータはPyMOTWの記事にあるコードを使わせてもらった.再掲させてもらう.
def mkfile(filename, body=None):
with open(filename, 'w') as f:
f.write(body or filename)
return
def make_example_dir(top):
if not os.path.exists(top):
os.mkdir(top)
curdir = os.getcwd()
os.chdir(top)
os.mkdir('dir1')
os.mkdir('dir2')
mkfile('dir1/file_only_in_dir1')
mkfile('dir2/file_only_in_dir2')
os.mkdir('dir1/dir_only_in_dir1')
os.mkdir('dir2/dir_only_in_dir2')
os.mkdir('dir1/common_dir')
os.mkdir('dir2/common_dir')
mkfile('dir1/common_file', 'this file is the same')
mkfile('dir2/common_file', 'this file is the same')
mkfile('dir1/not_the_same')
mkfile('dir2/not_the_same')
mkfile('dir1/file_in_dir1', 'This is a file in dir1')
os.mkdir('dir2/file_in_dir1')
os.chdir(curdir)
return
os.chdir( os.getcwd())
make_example_dir("example")
make_example_dir('example/dir1/common_dir')
make_example_dir('example/dir2/common_dir')
diff コマンドを用いて
diffコマンドを用いて,ディレクトリーを再帰的に探索し,違うファイル,片方にしかないファイルなどを探し出す.
用途によってオプションを適当につけること.bash なら”man diff”でマニュアルが確認可能.
オプションの-qで,ファイルの差分の表示を押さえているがチェックの時間はあまり変わっていない.
path1 = "example/dir1/"
path2 = "example/dir2/"
proc = subprocess.Popen(["diff","-r","-q",path1,path2], stdout=subprocess.PIPE)
for s in proc.communicate():
if s == None:
continue
print(s.decode("utf-8"))

filecmpを用いた方法
filecmpはpythonのモジュールの一つである.デフォルトだとdiffに比べてチェック時間がやたら早い.というのも,os.stat()によって得られるsignatureを比較して同じだったらTrueを返すようになっているからだ.
以下,基本的なfilecmpの使い方について述べる.
以下のコードは指定したディレクトリー間で一階層分だけ評価する.
注意すべき点は,Common subdirectoriesではその中に入っているファイルまで同じかは一切評価せずに,名前だけ合致しているかを確認しているだけということだ.
path1 = 'example/dir1'
path2 = 'example/dir2'
filecmp.dircmp(path1,path2).report()

アトリビュートを用いて,個々の要素にアクセスすることも可能である.
dc = filecmp.dircmp(path1,path2,ignore=["common_file"])
print ('Common:', dc.common)
print ('Left :', dc.left_list)
print ('Right:', dc.right_list)
print("Left only :" ,dc.left_only)
print("Right only:", dc.right_only)
print ('Same :', dc.same_files)
print ('Different :', dc.diff_files)
print ('Funny :', dc.funny_files)
print('subdirectories :',dc.subdirs)

繰り返し,Common subdirectoriesを探索する場合は,report_full_closure()メソッドを用いる.
# compare all subdirectories
filecmp.dircmp(path1, path2).report_full_closure()

filecmp.dircmpを上書きする
僕の目的としては,filecmp.direcmpのreport_full_closure()の出力が近い.だが,これではパッと見て,二つのディレクトリー間で違う場所があるかどうかわかりにくい.特に,Common subdirectories とIdentical filesの出力を消去したい.
path = inspect.getfile(filecmp)
subprocess.check_call(["open",path])
上のコードを用いて,ソースコードを確認すると,reportメソッドのコメントの部分に,”Output format is purposely lousy” と書いてあり,とりあえず全部出力するようにしてくれているそう.感謝感激です ( ;∀;).
アウトプットの出力を変更するには,ソースコード書き換えることは一つの手段.だけれども,version upしてしまうともう一度書き換えなきればいけない.そこで,ここではclassを継承して関数を上書きする手法を取る.
注意事項としては以下の3点が挙げられる.
・Common subdirectoriesに対して,クラスを渡している部分(phase4の中)があるので,そこも更新.
・__getattr__ を用いて値を取得している.なので,methodmap内にある関数も更新する必要がある.
・出力は探索した層で差分があった時のみ行うようにする.探索自体は繰り返し行う.
コードは以下のようになる.
class myDircmp(filecmp.dircmp):
def __init__(self,a,b, ignore = None, hide=None):
super().__init__(a,b)
def phase4(self): # Find out differences between common subdirectories
self.subdirs = {}
for x in self.common_dirs:
a_x = os.path.join(self.left, x)
b_x = os.path.join(self.right, x)
self.subdirs[x] = myDircmp(a_x, b_x, self.ignore, self.hide)
self.methodmap["subdirs"] = phase4
def myReport(self):
flag = False
s = 'diff %s %sn' % (self.left, self.right)
if self.left_only:
flag = True
self.left_only.sort()
s += 'Only in %s : %sn'% ( self.left, self.left_only)
if self.right_only:
flag = True
self.right_only.sort()
s += 'Only in %s : %sn' % ( self.right, self.right_only)
#if self.same_files:
# self.same_files.sort()
# print('Identical files :', self.same_files)
if self.diff_files:
flag = True
self.diff_files.sort()
s += 'Differing files : %sn' % self.diff_files
if self.funny_files:
flag = True
self.funny_files.sort()
s += 'Trouble with common files : %sn' % self.funny_files
#if self.common_dirs:
# self.common_dirs.sort()
# print('Common subdirectories :', self.common_dirs)
if self.common_funny:
flag = True
self.common_funny.sort()
s += 'Common funny cases : %sn' % self.common_funny
if flag:
print(s)
def myFullReport(self): # Report on self and subdirs recursively
self.myReport()
for sd in self.subdirs.values():
sd.myFullReport()
dc = myDircmp(path1,path2)
dc.myFullReport()
print("finished!!")

差分がある時のみ表示されるので,チェックが非常に容易となった.
———-雑感(`・ω・´)———-
__getattr__とgetattrを使えば,必要な時に関数から生成される値を用いれることが分かった.有効な場面があれば参考にしていきたい.
sourceのこの部分ですね.
methodmap = dict(subdirs=phase4,
same_files=phase3, diff_files=phase3, funny_files=phase3,
common_dirs = phase2, common_files=phase2, common_funny=phase2,
common=phase1, left_only=phase1, right_only=phase1,
left_list=phase0, right_list=phase0)
def __getattr__(self, attr):
if attr not in self.methodmap:
raise AttributeError(attr)
self.methodmap[attr](self)
return getattr(self, attr)
参考文献
・filecmp — ファイルおよびディレクトリの比較,https://docs.python.org/ja/3/library/filecmp.html
・python –__getattr__ とgetattrの関係は?,https://codeday.me/jp/qa/20190109/121538.html
コメント
[…] [python] 2つのディレクトリーの差分検出、簡易版 | あきとしの … […]