filecmp – ファイルを比較する

目的:ファイルシステム上のファイルやディレクトリを比較する
利用できるバージョン:2.1 以上

サンプルデータ

この記事のサンプルは、次の filecmp_mkexamples.py で作成したテストファイルを使用します。

import os

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

if __name__ == '__main__':
    os.chdir(os.path.dirname(__file__) or os.getcwd())
    make_example_dir('example')
    make_example_dir('example/dir1/common_dir')
    make_example_dir('example/dir2/common_dir')
    
$ ls -Rlast example
total 0
0 drwxr-xr-x  4 dhellmann  dhellmann  136 Apr 20 17:04 .
0 drwxr-xr-x  9 dhellmann  dhellmann  306 Apr 20 17:04 ..
0 drwxr-xr-x  8 dhellmann  dhellmann  272 Apr 20 17:04 dir1
0 drwxr-xr-x  8 dhellmann  dhellmann  272 Apr 20 17:04 dir2

example/dir1:
total 32
0 drwxr-xr-x  8 dhellmann  dhellmann  272 Apr 20 17:04 .
0 drwxr-xr-x  4 dhellmann  dhellmann  136 Apr 20 17:04 ..
0 drwxr-xr-x  2 dhellmann  dhellmann   68 Apr 20 17:04 common_dir
8 -rw-r--r--  1 dhellmann  dhellmann   21 Apr 20 17:04 common_file
0 drwxr-xr-x  2 dhellmann  dhellmann   68 Apr 20 17:04 dir_only_in_dir1
8 -rw-r--r--  1 dhellmann  dhellmann   22 Apr 20 17:04 file_in_dir1
8 -rw-r--r--  1 dhellmann  dhellmann   22 Apr 20 17:04 file_only_in_dir1
8 -rw-r--r--  1 dhellmann  dhellmann   17 Apr 20 17:04 not_the_same

example/dir2:
total 24
0 drwxr-xr-x  8 dhellmann  dhellmann  272 Apr 20 17:04 .
0 drwxr-xr-x  4 dhellmann  dhellmann  136 Apr 20 17:04 ..
0 drwxr-xr-x  2 dhellmann  dhellmann   68 Apr 20 17:04 common_dir
8 -rw-r--r--  1 dhellmann  dhellmann   21 Apr 20 17:04 common_file
0 drwxr-xr-x  2 dhellmann  dhellmann   68 Apr 20 17:04 dir_only_in_dir2
0 drwxr-xr-x  2 dhellmann  dhellmann   68 Apr 20 17:04 file_in_dir1
8 -rw-r--r--  1 dhellmann  dhellmann   22 Apr 20 17:04 file_only_in_dir2
8 -rw-r--r--  1 dhellmann  dhellmann   17 Apr 20 17:04 not_the_same

興味のある再帰比較オプションを試すために “common_dir” ディレクトリの配下には同じディレクトリ構造(dir1 と dir2)が置かれます。

ファイルを比較する

filecmp モジュールは、ファイルシステム上のファイルやクラスを比較する関数やクラスを提供します。2つのファイルを比較するには cmp() 関数を使用してください。

import filecmp

print 'common_file:', 
print filecmp.cmp('example/dir1/common_file', 
                  'example/dir2/common_file'),
print filecmp.cmp('example/dir1/common_file', 
                  'example/dir2/common_file',
                  shallow=False)

print 'not_the_same:', 
print filecmp.cmp('example/dir1/not_the_same', 
                  'example/dir2/not_the_same'),
print filecmp.cmp('example/dir1/not_the_same', 
                  'example/dir2/not_the_same',
                  shallow=False)

print 'identical:',
print filecmp.cmp('example/dir1/file_only_in_dir1', 
                  'example/dir1/file_only_in_dir1'),
print filecmp.cmp('example/dir1/file_only_in_dir1', 
                  'example/dir1/file_only_in_dir1',
                  shallow=False)

デフォルトでは、 cmp()os.stat() から利用できる情報のみを調べます。shallow 引数は、そのファイルの中身を調べるかを cmp() へ伝えます。デフォルトは、ファイルの中身を調べずに浅い比較を実行します。ファイルの中身を比較しない場合、全く同時に作成された同じサイズのファイルは同一と見なされることに注意してください。

$ python filecmp_cmp.py
common_file: True True
not_the_same: True False
identical: True True

再帰せずに2つのディレクトリ内のファイルセットを比較するには filecmp.cmpfiles() を使用してください。その引数はディレクトリの名前と、その2つのディレクトリでチェックするファイルのリストです。共通ファイルのリストはファイル名のみを含めます(ディレクトリは必ずミスマッチになります)。そして、そのファイルは両方のディレクトリに存在していなければなりません。次のコードは、共通リストを作成するシンプルな方法を紹介します。もっと短く書けるならコメントで教えてください。 cmp() と同様に、shallow フラグを受け取って比較することもできます。

import filecmp
import os

# 両方のディレクトリに存在する要素を決定する
d1_contents = set(os.listdir('example/dir1'))
d2_contents = set(os.listdir('example/dir2'))
common = list(d1_contents & d2_contents)
common_files = [ f 
                for f in common 
                if os.path.isfile(os.path.join('example/dir1', f))
                ]
print 'Common files:', common_files

# ディレクトリを比較する
match, mismatch, errors = filecmp.cmpfiles('example/dir1', 
                                           'example/dir2', 
                                           common_files)
print 'Match:', match
print 'Mismatch:', mismatch
print 'Errors:', errors

cmpfiles() はマッチしたファイル、マッチしなかったファイル、(パーミッションや何らかの理由で)比較できなかったファイルのファイル名を含む3つのリストを返します。

$ python filecmp_cmpfiles.py
Common files: ['not_the_same', 'file_in_dir1', 'common_file']
Match: ['not_the_same', 'common_file']
Mismatch: ['file_in_dir1']
Errors: []

dircmp を使用する

前節で紹介した関数は相対的にシンプルな比較に適していますが、巨大なディレクトリツリーの再帰的な比較、またはもっと複雑な解析には dircmp クラスがさらに便利です。最も簡単な使用方法は、 report() メソッドで2つのディレクトリを比較するレポートを表示できます。

import filecmp

filecmp.dircmp('example/dir1', 'example/dir2').report()

その実行結果は、再帰せずに渡されたディレクトリのコンテンツの比較結果を表示するプレーンテキストのレポートです。このケースでは、”not_the_same” というファイルが同一と見なされます。それはそのファイルの中身が比較されないからです。dircmp には cmp() のようなファイルの中身を比較する方法がありません。

$ python filecmp_dircmp_report.py
diff example/dir1 example/dir2
Only in example/dir1 : ['dir_only_in_dir1', 'file_only_in_dir1']
Only in example/dir2 : ['dir_only_in_dir2', 'file_only_in_dir2']
Identical files : ['common_file', 'not_the_same']
Common subdirectories : ['common_dir']
Common funny cases : ['file_in_dir1']

さらに詳細に、再帰的な比較を行うには report_full_closure() を使用してください。

import filecmp

filecmp.dircmp('example/dir1', 'example/dir2').report_full_closure()

その実行結果は、全てのサブディレクトリを比較します。

$ python filecmp_dircmp_report_full_closure.py
diff example/dir1 example/dir2
Only in example/dir1 : ['dir_only_in_dir1', 'file_only_in_dir1']
Only in example/dir2 : ['dir_only_in_dir2', 'file_only_in_dir2']
Identical files : ['common_file', 'not_the_same']
Common subdirectories : ['common_dir']
Common funny cases : ['file_in_dir1']

diff example/dir1/common_dir example/dir2/common_dir
Common subdirectories : ['dir1', 'dir2']

diff example/dir1/common_dir/dir2 example/dir2/common_dir/dir2
Identical files : ['common_file', 'file_only_in_dir2', 'not_the_same']
Common subdirectories : ['common_dir', 'dir_only_in_dir2', 'file_in_dir1']

diff example/dir1/common_dir/dir2/common_dir example/dir2/common_dir/dir2/common_dir

diff example/dir1/common_dir/dir2/dir_only_in_dir2 example/dir2/common_dir/dir2/dir_only_in_dir2

diff example/dir1/common_dir/dir2/file_in_dir1 example/dir2/common_dir/dir2/file_in_dir1

diff example/dir1/common_dir/dir1 example/dir2/common_dir/dir1
Identical files : ['common_file', 'file_in_dir1', 'file_only_in_dir1', 'not_the_same']
Common subdirectories : ['common_dir', 'dir_only_in_dir1']

diff example/dir1/common_dir/dir1/common_dir example/dir2/common_dir/dir1/common_dir

diff example/dir1/common_dir/dir1/dir_only_in_dir1 example/dir2/common_dir/dir1/dir_only_in_dir1

プログラム内で差異を使用する

レポートの作成に加えて、dircmp は直接プログラム内で利用可能な便利なファイルリストを算出します。それぞれの属性はそれが要求されたときのみ算出されるので、dircmp をインスタンス化するのはそう大きなオーバーヘッドになりません。

比較対象のディレクトリを含むサブディレクトリとファイルは left_listright_list で表示されます。

import filecmp

dc = filecmp.dircmp('example/dir1', 'example/dir2')
print 'Left :', dc.left_list
print 'Right:', dc.right_list
$ python filecmp_dircmp_list.py
Left : ['common_dir', 'common_file', 'dir_only_in_dir1', 'file_in_dir1', 'file_only_in_dir1', 'not_the_same']
Right: ['common_dir', 'common_file', 'dir_only_in_dir2', 'file_in_dir1', 'file_only_in_dir2', 'not_the_same']

無視するファイル名のリストをコンストラクタへ渡すことでその入力をフィルタできます。デフォルトでは、RCS, CVS や tags が無視されます。

import filecmp

dc = filecmp.dircmp('example/dir1', 'example/dir2', ignore=['common_file'])
print 'Left :', dc.left_list
print 'Right:', dc.right_list

このケースでは、”common_file” が比較対象外のファイルリストにあります。

$ python filecmp_dircmp_list_filter.py
Left : ['common_dir', 'dir_only_in_dir1', 'file_in_dir1', 'file_only_in_dir1', 'not_the_same']
Right: ['common_dir', 'dir_only_in_dir2', 'file_in_dir1', 'file_only_in_dir2', 'not_the_same']

両方の入力ディレクトリの共通ファイルは common で保持されます。それぞれのディレクトリにしか存在しないファイルは left_onlyright_only で表示されます。

import filecmp

dc = filecmp.dircmp('example/dir1', 'example/dir2')
print 'Common:', dc.common
print 'Left  :', dc.left_only
print 'Right :', dc.right_only
$ python filecmp_dircmp_membership.py
Common: ['not_the_same', 'common_file', 'file_in_dir1', 'common_dir']
Left  : ['dir_only_in_dir1', 'file_only_in_dir1']
Right : ['dir_only_in_dir2', 'file_only_in_dir2']

common の仲間には、さらにファイル、ディレクトリ、”funny” (2つのディレクトリ、または os.stat() からエラーが発生した場所で異なる型を持つ) の要素に分割されます。

import filecmp

dc = filecmp.dircmp('example/dir1', 'example/dir2')
print 'Common     :', dc.common
print 'Directories:', dc.common_dirs
print 'Files      :', dc.common_files
print 'Funny      :', dc.common_funny

このサンプルデータでは、”file_in_dir1” という要素はディレクトリ内にあるファイルであり、且つサブディレクトリです。そのため “funny” リストに表示されます。

$ python filecmp_dircmp_common.py
Common     : ['not_the_same', 'common_file', 'file_in_dir1', 'common_dir']
Directories: ['common_dir']
Files      : ['not_the_same', 'common_file']
Funny      : ['file_in_dir1']

ファイル間の差異は同様に分割されます。

import filecmp

dc = filecmp.dircmp('example/dir1', 'example/dir2')
print 'Same      :', dc.same_files
print 'Different :', dc.diff_files
print 'Funny     :', dc.funny_files

“not_the_same” は os.stat() のみで比較されたファイルであり、そのファイルの中身は調べないことを覚えておいてください。

$ python filecmp_dircmp_diff.py
Same      : ['not_the_same', 'common_file']
Different : []
Funny     : []

最後に、再帰的な比較が簡単にできるようにサブディレクトリは subdirs 属性で新たな dircmp オブジェクトを取得することもできます。

import filecmp

dc = filecmp.dircmp('example/dir1', 'example/dir2')
print 'Subdirectories:'
print dc.subdirs
$ python filecmp_dircmp_subdirs.py
Subdirectories:
{'common_dir': <filecmp.dircmp instance at 0x85da0>}

See also

filecmp
本モジュールの標準ライブラリドキュメント
Directories from os
ディレクトリのコンテンツを表示する
difflib
2つのシーケンス間の差異を算出する
Bookmark and Share