ふたつのディレクトリの中身が一致するか
プロジェクトの途中からまたは完成したプロジェクトをコピーして別のプロジェクトを作ることはよくあることだと思います。
その際、それぞれに開発していったファイルの中身が一致するのかしないのか気になることもしかりではないでしょうか。
今回はそのようなケースに遭遇した際に使えるよう、ふたつのディレクトリを指定してその中のファイルが一致するかしないかをチェックするPythonのコードを用意してみました。
ディレクトリ全体でチェックをする
この後紹介するコードのロジックは、基本的にふたつのフォルダにあるファイルのハッシュを取得し一致するかを調べるものとなります。
まずはディレクトリ全体のハッシュ同士を比較してみます。
compare-dir.py
import os
import hashlib
def calculate_directory_hash(directory):
#import した hashlib から sha256用のオブジェクトを取得
hash_object = hashlib.sha256()
# ハッシュを取得しながらディレクトリ内を走査
for root, dirs, files in os.walk(directory):
for file in files:
file_path = os.path.join(root, file)
try:
with open(file_path, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
hash_object.update(chunk)
except IOError:
# ファイルにアクセスできない場合の処理
pass
return hash_object.hexdigest()
def compare_directories(directory1, directory2):
hash1 = calculate_directory_hash(directory1)
hash2 = calculate_directory_hash(directory2)
if hash1 == hash2:
print("ふたつのディレクトリは完全一致しました")
else:
print("ふたつのディレクトリ内には異なる箇所が存在します")
# エントリーポイント
directory1 = "dir1"
directory2 = "dir2"
compare_directories(directory1, directory2)
コード内にもコメントがありますが、もう少し詳しく処理を解説していきます。
import した os ライブラリから walk関数を使います。
これは、ディレクトリを引数にとり、再帰的にディレクトリをたどっていくジェネレーターを返します。
それをループさせて
で構成されるタプルを取得しています。
ファイルをオープンし、lambda: を使って指定サイズを読み込む無名関数を作成しています。
iterは第一引数に渡した関数(先の無名関数)から、第二引数の値が返るまで反復をするイテレーターを生成する関数です。
for chunk in イテレーター とすることで、イテレーターのループをchunk変数で受け取ります。
chunk変数に受け取った値をupdateメソッドに細切れで渡したのち最後に、hexdigest()を呼ぶことにより連結された内容でのハッシュを得られます。
ファイル単位で違いを取得
ふたつのディレクトリが一致しなかった場合は、何が一致していないかの確認も必要だと思います。
そこで先のコードを改変して、今度はファイル単位で違いを検出してみます。
compare-files.py
import os
import hashlib
def calculate_file_hash(file_path):
hash_object = hashlib.sha256()
try:
with open(file_path, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
hash_object.update(chunk)
except IOError:
# ファイルにアクセスできない場合の処理
pass
return hash_object.hexdigest()
def compare_directories(directory1, directory2):
different_files = []
files1 =[];
for root, dirs, files in os.walk(directory1):
for file in files:
file_path1 = os.path.join(root, file)
# relpathを使ってディレクトリ1からの相対パスを取得して、ディレクトリ2に連結
file_path2 = os.path.join(directory2, os.path.relpath(file_path1, directory1))
# 後からの比較用に相対パスを保持
files1.append(os.path.relpath(file_path1, directory1))
if not os.path.exists(file_path2):
different_files.append(file_path1)
else:
hash1 = calculate_file_hash(file_path1)
hash2 = calculate_file_hash(file_path2)
if hash1 != hash2:
different_files.append(file_path1)
# ディレクトリ2に存在するファイルを取得(相対パス)
files2 = get_files_in_directory(directory2)
# 先ほど取得したリストと比較して2にだけ存在するファイルを取得
only_dir2_files = list(set(files2) - set(files1))
# 違いに追加
for file in only_dir2_files:
different_files.append(os.path.join(directory2,file))
return different_files
# 指定したディレクトリにあるファイル一覧を相対パスで取得する
def get_files_in_directory(directory):
ret_files = []
for root, dirs, files in os.walk(directory):
for file in files:
file_path = os.path.join(root, file)
# 相対パスを取得
ret_files.append(os.path.relpath(file_path, directory))
return ret_files
# エントリーポイント
directory1 = "dir1"
directory2 = "dir2"
different_files = compare_directories(directory1, directory2)
if different_files:
print("次のファイルが異なります:")
for file in different_files:
print(file)
else:
print("ディレクトリ内のファイルはすべて一致しています。")
ディレクトリ1を基準にして、ディレクトリ2で該当のファイルを探して存在しないか、ハッシュが違う場合は異なるファイルとして検出します。
ディレクトリ1を基準にする都合で、そのままではディレクトリ2にだけ存在するファイルは検出できないので、別途走査します。
その際も、os.path.existsを使えばいいのかもしれませんが、setにキャストして差集合を求めました。(その方がなんとなくディスクIOが少なくなるような気がしたので)