1. 程式人生 > >【Python】 文件目錄比較工具filecmp和difflib

【Python】 文件目錄比較工具filecmp和difflib

返回 我沒 直接 既然 hash this 上下 direct 不同

  在一些運維場景中,常常需要比較兩個環境中的應用目錄結構(是否有文件/目錄層面上的增刪)以及比較兩個環境中同名文件內容的不同(即文件層面上的改)。Python自帶了兩個內建模塊可以很好地完成這個工作,filecmp和difflib。前者主要用於比較目錄結構上的不同以及籠統的文件內容比較;後者用於比較兩個文件具體內容上的不同。綜合使用兩個模塊可以比較完備地做一次比較。

【filecmp】

  filecmp提供一些方法可以很方便地進行對比兩個目錄在結構上的不同以及籠統的文件內容上的異同。比如

  filecmp.cmp(f1,f2[,shallow])  用於籠統地比較兩個文件內容是否相同,shallow可以指定True或者False,當為True的時候這個方法會把文件的屬性(os.stat方法調用看到的信息)也作為比較依據的一部分。整個方法最後返回True或者False告訴調用者兩個文件的比較結果。

  filecmp.cmpfiles(d1,d2,common[,shallow])  用於比較兩個目錄下同名的那些文件是否都相同,common接受一個list或者tuple來表示比較哪些同名的文件。

  除了上面了兩個模塊的靜態方法之外,filecmp中還有一個dircmp類用於更加完備的比較處理工作。

  dircmp類的定義是這麽描述的:

    dircmp(a,b,ignore=None,hide=None)
      A and B are directories.
      IGNORE is a list of names to ignore,
        defaults to [‘RCS‘, ‘CVS‘, ‘tags‘].
      HIDE is a list of names to hide,
        defaults to [os.curdir, os.pardir].

  在構造完一個dircmp類對象之後,可以調用下面這幾個方法來輸出對比的信息

  d.report()  只比較當前目錄的內容,不涉及當前目錄下子目錄中內容是否相同。輸出的結果是類似於下面這樣的樣子:

diff testdir1 testdir2
Only in testdir1 : [‘subdir1‘]
Only in testdir2 : [‘subdir2‘]
Identical files : [‘same.txt‘]
Differing files : [‘file.txt‘]

  上面的結果中涉及到了只在目錄A中、只在目錄B中以及同名內容一致(identical)和同名內容不同(differing)四種類別的結果。其實除此之外還有可能得到同名子目錄、因為某些問題而無法直接對比內容的文件(比如文件的內容無法hash)等結果。

  

  d.report_partial_closure()  比較當前目錄以及往下一級子目錄的內容

  d.report_full_closure()  遞歸比較當前目錄下所有子目錄,裏面的內容。返回的格式化輸出中,會按照各個子目錄的不同分別列出對比情況。

  上面的三個方法都是filecmp模塊幫助我們格式化好的輸出。如果需要獲取第一手的比較結果,則應該調用dircmp類的一些其他屬性。比如:
  left_list  目錄A中的子目錄和文件列表,相當於os.listdir

  right_list  目錄B中的子目錄和文件列表

  left/right_only  僅存在於目錄A/B中的子目錄和文件

  common  兩目錄中同名的子目錄和文件

  common_files  兩目錄中同名的子文件,內容不一定相同

  common_dirs  兩目錄中同名的子目錄,內容不一定相同

  common_funny  兩目錄中同名的不可比較文件

  same_files  兩目錄中同名且內容相同的子文件

  diff_files  兩目錄中同名但內容不相同的子文件

  funny_files  兩目錄中同名但無法比較的子文件

  以上所有屬性都是返回了一個List,其中是各個文件/目錄的名字

  需要註意的是dircmp類默認只對比當前目錄層級,對於想要深入遞歸地對比後輩目錄的話就需要采取一些手段。可以使用dircmp的subdirs這個屬性

  subdirs  是一個字典,字典的鍵是當前dircmp類對象對比的兩個目錄下同名的子目錄(也就是說subdirs.keys()等於是common_dirs),每個鍵對應的值是另一dircmp對象,而這對象對比的就是兩個同名子目錄的下的內容了。也就是說不斷地調取subdirs這個屬性就可以實現遞歸比較了。雖然在這次應用中我沒有采取用subdirs而是稍微麻煩一點采用了判斷common_dirs的辦法,但是兩者原理是一樣的。我的嘗試待會兒寫在下面

  

【difflib】

  difflib深入到文件內部,不僅僅給出“文件內容不相同”級別的提示,而是具體說明了哪些地方不相同。常用於文本文件的比較。可以看出,使用difflib還是需要被對比的文件是可以被hash的。下面的說明將基於文本文件的對比來。

  既然涉及到了詳細的文件內容,那麽就需要有一種表示給人看的,呈現文件內容對比結果的方式。字符界面上比較常見的方法,是像linux中的diff命令的結果那樣。比如:

[[email protected] tmp]# cat file1
this is file1
my name is takanashi
today is a little bit cold
tomorrow is holiday
[[email protected] tmp]# cat file2
this is file2
my name is Takanashi
today is a little bit cold
i wouldnt work tomorrow
[[email protected] tmp]# diff file1 file2
1,2c1,2
< this is file1
< my name is takanashi
---
> this is file2
> my name is Takanashi
4c4
< tomorrow is holiday
---
> i wouldnt work tomorrow

  關於字符提示的詳細說明這裏就不多提了,可以參看linux篇的介紹。這裏提到主要是想說明,difflib這個模塊的一些方法也是會輸出這樣形式的對比結果的。

  difflib模塊給出了一些用於比較文本的類,最簡單的一種是Differ

  ■  Differ類

  Differ類有compare方法用於直接比較兩段文本,比較的結果是通過類似上面那種表現形式來呈現。比如上面的file1和file2兩個文件,通過這樣一個腳本來比較:

import difflib

d = difflib.Differ()
with open(file1,r) as file1:
    content1 = file1.read().splitlines()
with open(file2,r) as file2:
    content2 = file2.read().splitlines()

print \n.join(d.compare(content1,content2))

  可以註意到,Differ類處理的對象並不是一塊文本(或者說字符串)而是一個列表,列表是根據文本字符串通過\n split出來的,這一點也適用於difflib其他一些工具類。所以說difflib其實是基於行的比較。

  compare方法返回的是一個生成器,裏面是各行比較的結果。運行上面代碼輸出是:

- this is file1
? ^

+ this is file2
? ^

- my name is takanashi
? ^

+ my name is Takanashi
? ^

today is a little bit cold
- tomorrow is holiday
+ i wouldnt work tomorrow

  兩文件的前兩句有所不同,比較結果前有‘-‘號的表示left_only,‘+‘號表示right_only,此外對於類似的行difflib會做進一步比較找出變更的地方。在相關行下方額外添加一行?開頭的行,這行中的^號標識出變更發生的位置。兩文件第三行是相同的,所以只輸出了一遍,行前空格是為了和上下行對齊。後面兩行因為差別較大,被認為是各自獨特的行所以沒有?開頭的那行了。

  另外,如果是B文件中某一行比A文件中的行增加或減少了一些字符,那麽在?開頭那行裏會用-和+來表示增減字符是哪些。比如:

- this a file12
?            -

+ this is a file2
?   +++

  

  ■  unified_diff方法

  上面說Differ類會把相同的行打印一次。如果兩文件相同部分很多,只有一點不同,那麽把相同的行都顯示出來,即使只打印一遍也還是有點不好的。而difflib.unified_diff方法可以解決這一個問題。

  unified_diff方法接受n這個參數表名只顯示發現不同處上下各n行的內容。相似的方法還有context_diff。不太用所以不詳細展開了

  ■  SequenceMatcher類

  這個類首先可以用於指定忽略一些字符的比較。在其構造方法中指定第一個參數是函數對象。這個函數接受一個字符並且經過一定判斷後返回True或False。根據這個返回結果類將判斷要不要把這個字符計入比較結果。比如s = SequenceMatcher(lambda x : x == ‘ ‘,‘some string A‘,‘ some string B‘)。

  上面這個s可以調用方法s.find_longest_match(ab,ae,bb,be)。這個方法返回的是元組(i,j,k),表示上面比較的字符串A,B中A[ab:ae]與B[bb:be]兩部分中可以找到最長公共部分A[i:i+k]和B[j:j+k]。

  這個類另一個NB的地方在於不僅僅可以對比字符串,而可以對比任何序列。比如兩個列表的對比,也可以通過它來實現。此時構造方法的第一個參數那個函數對象,接受的就不是一個字符而是一個序列中的元素了。

  

  ■  HtmlDiff類

  這個類是我用的,它在Differ類的基礎上將原先字符界面的結果呈現改成了更加友好的html界面顯示。一目了然

  構造方法:__init__(tabsize=8, wrapcolumn=None, linejunk=None, charjunk=IS_CHARACTER_JUNK)

  tabsize是在html中顯示的制表符的空格數量,默認是8但是我覺得太大了,改成2或者4更好看一些。wrapcolumn指定界面上的比較欄中文字最大寬度,超過此寬度會自動換行。默認是None也就是不換行,在碰到有很長的行的時候,頁面寬度就會很大。linejunk和charjunk就是和ndiff方法中的相關參數差不多的,兩個都是函數對象,用於指出怎麽樣的行或者怎麽樣的字符不計入比較。

  HtmlDiff類主要用兩個方法,make_table和make_file,兩者參數類似,只不過前者返回的是可以組成一個獨立html文件的html代碼(帶<html><head>等標簽),而後者是生成一個html表格的代碼(從<table>開始)。以make_file為例,其參數是make_file(fromlines, tolines [, fromdesc][, todesc][, context][, numlines])。fromlines和tolines是承載比較內容的兩個列表,如上面所說,不是字符串是字符串經過‘\n‘split過的列表。然後fromdesc和todesc用於指定生成的html中對比欄目中擡頭的文字。context參數默認是False,如果設置為True,那麽html中只會顯示有變化的行上線numlines行數的內容,大部分相同的內容就不重復顯示了。

  講了半天,下面是我對HtmlDiff類的一個使用:

    def check_diff(self, index, wrapcolumn):
        file1, file2 = self.differing[index]
        with open(file1, r) as f:
            content1 = f.read().splitlines()
        with open(file2, r) as f:
            content2 = f.read().splitlines()
        htmlDiff = HtmlDiff(tabsize=2,wrapcolumn=wrapcolumn)
        with open(tmp.html, w) as f:
            f.write(htmlDiff.make_file(content1, content2, fromdesc=self.dir1, todesc=self.dir2))
        webbrowser.open(tmp.html)

  這是部分代碼,結合了webbrowser模塊之後,可以把生成的HTML對比文件立刻打開,得到的HTML界面大概長這樣:

技術分享

  可以看到是比較友好的界面。

  

  

【Python】 文件目錄比較工具filecmp和difflib