[python] 2つのディレクトリーの差分検出、簡易版

この記事では,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

コメント

  1. […] [python] 2つのディレクトリーの差分検出、簡易版 | あきとしの … […]

タイトルとURLをコピーしました