difflib --- 計算差異的輔助工具?

源代碼: Lib/difflib.py


此模塊提供用于比較序列的類和函數。 例如,它可以用于比較文件,并可以產生各種格式的不同信息,包括 HTML 和上下文以及統一格式的差異點。 有關目錄和文件的比較,請參見 filecmp 模塊。

class difflib.SequenceMatcher?

這是一個靈活的類,可用于比較任何類型的序列對,只要序列元素為 hashable 對象。 其基本算法要早于由 Ratcliff 和 Obershelp 于 1980 年代末期發表并以“格式塔模式匹配”的夸張名稱命名的算法,并且更加有趣一些。 其思路是找到不包含“垃圾”元素的最長連續匹配子序列;所謂“垃圾”元素是指其在某種意義上沒有價值,例如空白行或空白符。 (處理垃圾元素是對 Ratcliff 和 Obershelp 算法的一個擴展。) 然后同樣的思路將遞歸地應用于匹配序列的左右序列片段。 這并不能產生最小編輯序列,但確實能產生在人們看來“正確”的匹配。

耗時: 基本 Ratcliff-Obershelp 算法在最壞情況下為立方時間而在一般情況下為平方時間。 SequenceMatcher 在最壞情況下為平方時間而在一般情況下的行為受到序列中有多少相同元素這一因素的微妙影響;在最佳情況下則為線性時間。

自動垃圾啟發式計算: SequenceMatcher 支持使用啟發式計算來自動將特定序列項視為垃圾。 這種啟發式計算會統計每個單獨項在序列中出現的次數。 如果某一項(在第一項之后)的重復次數超過序列長度的 1% 并且序列長度至少有 200 項,該項會被標記為“熱門”并被視為序列匹配中的垃圾。 這種啟發式計算可以通過在創建 SequenceMatcher 時將 autojunk 參數設為 False 來關閉。

3.2 新版功能: autojunk 形參。

class difflib.Differ?

這個類的作用是比較由文本行組成的序列,并產生可供人閱讀的差異或增量信息。 Differ 統一使用 SequenceMatcher 來完成行序列的比較以及相似(接近匹配)行內部字符序列的比較。

Differ 增量的每一行均以雙字母代碼打頭:

雙字母代碼

含義

'- '

行為序列 1 所獨有

'+ '

行為序列 2 所獨有

'  '

行在兩序列中相同

'? '

行不存在于任一輸入序列

以 '?' 打頭的行嘗試將視線引至行以外而不存在于任一輸入序列的差異。 如果序列包含制表符則這些行可能會令人感到迷惑。

class difflib.HtmlDiff?

這個類可用于創建 HTML 表格(或包含表格的完整 HTML 文件)以并排地逐行顯示文本比較,行間與行外的更改將突出顯示。 此表格可以基于完全或上下文差異模式來生成。

這個類的構造函數:

__init__(tabsize=8, wrapcolumn=None, linejunk=None, charjunk=IS_CHARACTER_JUNK)?

初始化 HtmlDiff 的實例。

tabsize 是一個可選關鍵字參數,指定制表位的間隔,默認值為 8

wrapcolumn 是一個可選關鍵字參數,指定行文本自動打斷并換行的列位置,默認值為 None 表示不自動換行。

linejunkcharjunk 均是可選關鍵字參數,會傳入 ndiff() (被 HtmlDiff 用來生成并排顯示的 HTML 差異)。 請參閱 ndiff() 文檔了解參數默認值及其說明。

下列是公開的方法

make_file(fromlines, tolines, fromdesc='', todesc='', context=False, numlines=5, *, charset='utf-8')?

比較 fromlinestolines (字符串列表) 并返回一個字符串,表示一個完整 HTML 文件,其中包含各行差異的表格,行間與行外的更改將突出顯示。

fromdesctodesc 均是可選關鍵字參數,指定來源/目標文件的列標題字符串(默認均為空白字符串)。

contextnumlines 均是可選關鍵字參數。 當只要顯示上下文差異時就將 context 設為 True,否則默認值 False 為顯示完整文件。 numlines 默認為 5。 當 contextTruenumlines 將控制圍繞突出顯示差異部分的上下文行數。 當 contextFalsenumlines 將控制在使用 "next" 超鏈接時突出顯示差異部分之前所顯示的行數(設為零則會導致 "next" 超鏈接將下一個突出顯示差異部分放在瀏覽器頂端,不添加任何前導上下文)。

在 3.5 版更改: 增加了 charset 關鍵字參數。 HTML 文檔的默認字符集從 'ISO-8859-1' 更改為 'utf-8'

make_table(fromlines, tolines, fromdesc='', todesc='', context=False, numlines=5)?

比較 fromlinestolines (字符串列表) 并返回一個字符串,表示一個包含各行差異的完整 HTML 表格,行間與行外的更改將突出顯示。

此方法的參數與 make_file() 方法的相同。

Tools/scripts/diff.py 是這個類的命令行前端,其中包含一個很好的使用示例。

difflib.context_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\n')?

比較 ab (字符串列表);返回上下文差異格式的增量信息 (一個產生增量行的 generator)。

所謂上下文差異是一種只顯示有更改的行再加幾個上下文行的緊湊形式。 更改被顯示為之前/之后的樣式。 上下文行數由 n 設定,默認為三行。

默認情況下,差異控制行(以 *** or --- 表示)是通過末尾換行符來創建的。 這樣做的好處是從 io.IOBase.readlines() 創建的輸入將得到適用于 io.IOBase.writelines() 的差異信息,因為輸入和輸出都帶有末尾換行符。

對于沒有末尾換行符的輸入,應將 lineterm 參數設為 "",這樣輸出內容將統一不帶換行符。

上下文差異格式通常帶有一個記錄文件名和修改時間的標頭。 這些信息的部分或全部可以使用字符串 fromfile, tofile, fromfiledatetofiledate 來指定。 修改時間通常以 ISO 8601 格式表示。 如果未指定,這些字符串默認為空。

>>> s1 = ['bacon\n', 'eggs\n', 'ham\n', 'guido\n']
>>> s2 = ['python\n', 'eggy\n', 'hamster\n', 'guido\n']
>>> sys.stdout.writelines(context_diff(s1, s2, fromfile='before.py', tofile='after.py'))
*** before.py
--- after.py
***************
*** 1,4 ****
! bacon
! eggs
! ham
  guido
--- 1,4 ----
! python
! eggy
! hamster
  guido

請參閱 difflib 的命令行接口 獲取更詳細的示例。

difflib.get_close_matches(word, possibilities, n=3, cutoff=0.6)?

返回由最佳“近似”匹配構成的列表。 word 為一個指定目標近似匹配的序列(通常為字符串),possibilities 為一個由用于匹配 word 的序列構成的列表(通常為字符串列表)。

可選參數 n (默認為 3) 指定最多返回多少個近似匹配; n 必須大于 0.

可選參數 cutoff (默認為 0.6) 是一個 [0, 1] 范圍內的浮點數。 與 word 相似度得分未達到該值的候選匹配將被忽略。

候選匹配中(不超過 n 個)的最佳匹配將以列表形式返回,按相似度得分排序,最相似的排在最前面。

>>> get_close_matches('appel', ['ape', 'apple', 'peach', 'puppy'])
['apple', 'ape']
>>> import keyword
>>> get_close_matches('wheel', keyword.kwlist)
['while']
>>> get_close_matches('pineapple', keyword.kwlist)
[]
>>> get_close_matches('accept', keyword.kwlist)
['except']
difflib.ndiff(a, b, linejunk=None, charjunk=IS_CHARACTER_JUNK)?

比較 ab (字符串列表);返回 Differ 形式的增量信息 (一個產生增量行的 generator)。

可選關鍵字形參 linejunkcharjunk 均為過濾函數 (或為 None):

linejunk: 此函數接受單個字符串參數,如果其為垃圾字符串則返回真值,否則返回假值。 默認為 None。 此外還有一個模塊層級的函數 IS_LINE_JUNK(),它會過濾掉沒有可見字符的行,除非該行添加了至多一個井號符 ('#') -- 但是下層的 SequenceMatcher 類會動態分析哪些行的重復頻繁到足以形成噪音,這通常會比使用此函數的效果更好。

charjunk: 此函數接受一個字符(長度為 1 的字符串),如果其為垃圾字符則返回真值,否則返回假值。 默認為模塊層級的函數 IS_CHARACTER_JUNK(),它會過濾掉空白字符(空格符或制表符;但包含換行符可不是個好主意!)。

Tools/scripts/ndiff.py 是這個函數的命令行前端。

>>> diff = ndiff('one\ntwo\nthree\n'.splitlines(keepends=True),
...              'ore\ntree\nemu\n'.splitlines(keepends=True))
>>> print(''.join(diff), end="")
- one
?  ^
+ ore
?  ^
- two
- three
?  -
+ tree
+ emu
difflib.restore(sequence, which)?

返回兩個序列中產生增量的那一個。

給出一個由 Differ.compare()ndiff() 產生的 序列,提取出來自文件 1 或 2 (which 形參) 的行,去除行前綴。

示例:

>>> diff = ndiff('one\ntwo\nthree\n'.splitlines(keepends=True),
...              'ore\ntree\nemu\n'.splitlines(keepends=True))
>>> diff = list(diff) # materialize the generated delta into a list
>>> print(''.join(restore(diff, 1)), end="")
one
two
three
>>> print(''.join(restore(diff, 2)), end="")
ore
tree
emu
difflib.unified_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\n')?

比較 ab (字符串列表);返回統一差異格式的增量信息 (一個產生增量行的 generator)。

所以統一差異是一種只顯示有更改的行再加幾個上下文行的緊湊形式。 更改被顯示為內聯的樣式(而不是分開的之前/之后文本塊)。 上下文行數由 n 設定,默認為三行。

默認情況下,差異控制行 (以 ---, +++@@ 表示) 是通過末尾換行符來創建的。 這樣做的好處是從 io.IOBase.readlines() 創建的輸入將得到適用于 io.IOBase.writelines() 的差異信息,因為輸入和輸出都帶有末尾換行符。

對于沒有末尾換行符的輸入,應將 lineterm 參數設為 "",這樣輸出內容將統一不帶換行符。

上下文差異格式通常帶有一個記錄文件名和修改時間的標頭。 這些信息的部分或全部可以使用字符串 fromfile, tofile, fromfiledatetofiledate 來指定。 修改時間通常以 ISO 8601 格式表示。 如果未指定,這些字符串默認為空。

>>> s1 = ['bacon\n', 'eggs\n', 'ham\n', 'guido\n']
>>> s2 = ['python\n', 'eggy\n', 'hamster\n', 'guido\n']
>>> sys.stdout.writelines(unified_diff(s1, s2, fromfile='before.py', tofile='after.py'))
--- before.py
+++ after.py
@@ -1,4 +1,4 @@
-bacon
-eggs
-ham
+python
+eggy
+hamster
 guido

請參閱 difflib 的命令行接口 獲取更詳細的示例。

difflib.diff_bytes(dfunc, a, b, fromfile=b'', tofile=b'', fromfiledate=b'', tofiledate=b'', n=3, lineterm=b'\n')?

使用 dfunc 比較 ab (字節串對象列表);產生以 dfunc 所返回格式表示的差異行列表(也是字節串)。 dfunc 必須是可調用對象,通常為 unified_diff()context_diff()

允許你比較編碼未知或不一致的數據。 除 n 之外的所有輸入都必須為字節串對象而非字符串。 作用方式為無損地將所有輸入 (除 n 之外) 轉換為字符串,并調用 dfunc(a, b, fromfile, tofile, fromfiledate, tofiledate, n, lineterm)dfunc 的輸出會被隨即轉換回字節串,這樣你所得到的增量行將具有與 ab 相同的未知/不一致編碼。

3.5 新版功能.

difflib.IS_LINE_JUNK(line)?

對于可忽略的行返回 True。 如果 line 為空行或只包含單個 '#'line 行就是可忽略的,否則就是不可忽略的。 此函數被用作較舊版本 ndiff()linejunk 形參的默認值。

difflib.IS_CHARACTER_JUNK(ch)?

對于可忽略的字符返回 True。 字符 ch 如果為空格符或制表符則 ch 就是可忽略的,否則就是不可忽略的。 此函數被用作 ndiff()charjunk 形參的默認值。

參見

模式匹配:格式塔方法

John W. Ratcliff 和 D. E. Metzener 對于一種類似算法的討論。 此文于 1988 年 7 月發表于 Dr. Dobb's Journal

SequenceMatcher 對象?

SequenceMatcher 類具有這樣的構造器:

class difflib.SequenceMatcher(isjunk=None, a='', b='', autojunk=True)

可選參數 isjunk 必須為 None (默認值) 或為接受一個序列元素并當且僅當其為應忽略的“垃圾”元素時返回真值的單參數函數。 傳入 None 作為 isjunk 的值就相當于傳入 lambda x: False;也就是說不忽略任何值。 例如,傳入:

lambda x: x in " \t"

如果你以字符序列的形式對行進行比較,并且不希望區分空格符或硬制表符。

可選參數 ab 為要比較的序列;兩者默認為空字符串。 兩個序列的元素都必須為 hashable

可選參數 autojunk 可用于啟用自動垃圾啟發式計算。

3.2 新版功能: autojunk 形參。

SequenceMatcher 對象接受三個數據屬性: bjunkb 當中 isjunkTrue 的元素集合;bpopular 是被啟發式計算(如果其未被禁用)視為熱門候選的非垃圾元素集合;b2j 是將 b 當中剩余元素映射到一個它們出現位置列表的字典。 所有三個數據屬性將在 b 通過 set_seqs()set_seq2() 重置時被重置。

3.2 新版功能: bjunkbpopular 屬性。

SequenceMatcher 對象具有以下方法:

set_seqs(a, b)?

設置要比較的兩個序列。

SequenceMatcher 計算并緩存有關第二個序列的詳細信息,這樣如果你想要將一個序列與多個序列進行比較,可使用 set_seq2() 一次性地設置該常用序列并重復地對每個其他序列各調用一次 set_seq1()

set_seq1(a)?

設置要比較的第一個序列。 要比較的第二個序列不會改變。

set_seq2(b)?

設置要比較的第二個序列。 要比較的第一個序列不會改變。

find_longest_match(alo, ahi, blo, bhi)?

找出 a[alo:ahi]b[blo:bhi] 中的最長匹配塊。

如果 isjunk 被省略或為 Nonefind_longest_match() 將返回 (i, j, k) 使得 a[i:i+k] 等于 b[j:j+k],其中 alo <= i <= i+k <= ahi 并且 blo <= j <= j+k <= bhi。 對于所有滿足這些條件的 (i', j', k'),如果 i == i', j <= j' 也被滿足,則附加條件 k >= k', i <= i'。 換句話說,對于所有最長匹配塊,返回在 a 當中最先出現的一個,而對于在 a 當中最先出現的所有最長匹配塊,則返回在 b 當中最先出現的一個。

>>> s = SequenceMatcher(None, " abcd", "abcd abcd")
>>> s.find_longest_match(0, 5, 0, 9)
Match(a=0, b=4, size=5)

如果提供了 isjunk,將按上述規則確定第一個最長匹配塊,但額外附加不允許塊內出現垃圾元素的限制。 然后將通過(僅)匹配兩邊的垃圾元素來盡可能地擴展該塊。 這樣結果塊絕對不會匹配垃圾元素,除非同樣的垃圾元素正好與有意義的匹配相鄰。

這是與之前相同的例子,但是將空格符視為垃圾。 這將防止 ' abcd' 直接與第二個序列末尾的 ' abcd' 相匹配。 而只可以匹配 'abcd',并且是匹配第二個序列最左邊的 'abcd'

>>> s = SequenceMatcher(lambda x: x==" ", " abcd", "abcd abcd")
>>> s.find_longest_match(0, 5, 0, 9)
Match(a=1, b=0, size=4)

如果未找到匹配塊,此方法將返回 (alo, blo, 0)

此方法將返回一個 named tuple Match(a, b, size)

get_matching_blocks()?

返回描述非重疊匹配子序列的三元組列表。 每個三元組的形式為 (i, j, n),其含義為 a[i:i+n] == b[j:j+n]。 這些三元組按 ij 單調遞增排列。

最后一個三元組用于占位,其值為 (len(a), len(b), 0)。 它是唯一 n == 0 的三元組。 如果 (i, j, n)(i', j', n') 是在列表中相鄰的三元組,且后者不是列表中的最后一個三元組,則 i+n < i'j+n < j';換句話說,相鄰的三元組總是描述非相鄰的相等塊。

>>> s = SequenceMatcher(None, "abxcd", "abcd")
>>> s.get_matching_blocks()
[Match(a=0, b=0, size=2), Match(a=3, b=2, size=2), Match(a=5, b=4, size=0)]
get_opcodes()?

返回描述如何將 a 變為 b 的 5 元組列表,每個元組的形式為 (tag, i1, i2, j1, j2)。 在第一個元組中 i1 == j1 == 0,而在其余的元組中 i1 等于前一個元組的 i2,并且 j1 也等于前一個元組的 j2

tag 值為字符串,其含義如下:

含義

'replace'

a[i1:i2] 應由 b[j1:j2] 替換。

'delete'

a[i1:i2] 應被刪除。 請注意在此情況下 j1 == j2

'insert'

b[j1:j2] 應插入到 a[i1:i1]。 請注意在此情況下 i1 == i2

'equal'

a[i1:i2] == b[j1:j2] (兩個子序列相同)。

例如

>>> a = "qabxcd"
>>> b = "abycdf"
>>> s = SequenceMatcher(None, a, b)
>>> for tag, i1, i2, j1, j2 in s.get_opcodes():
...     print('{:7}   a[{}:{}] --> b[{}:{}] {!r:>8} --> {!r}'.format(
...         tag, i1, i2, j1, j2, a[i1:i2], b[j1:j2]))
delete    a[0:1] --> b[0:0]      'q' --> ''
equal     a[1:3] --> b[0:2]     'ab' --> 'ab'
replace   a[3:4] --> b[2:3]      'x' --> 'y'
equal     a[4:6] --> b[3:5]     'cd' --> 'cd'
insert    a[6:6] --> b[5:6]       '' --> 'f'
get_grouped_opcodes(n=3)?

返回一個帶有最多 n 行上下文的分組的 generator

get_opcodes() 所返回的組開始,此方法會拆分出較小的更改簇并消除沒有更改的間隔區域。

這些分組以與 get_opcodes() 相同的格式返回。

ratio()?

返回一個取值范圍 [0, 1] 的浮點數作為序列相似性度量。

其中 T 是兩個序列中元素的總數量,M 是匹配的數量,即 2.0*M / T。 請注意如果兩個序列完全相同則該值為 1.0,如果兩者完全不同則為 0.0

如果 get_matching_blocks()get_opcodes() 尚未被調用則此方法運算消耗較大,在此情況下你可能需要先調用 quick_ratio()real_quick_ratio() 來獲取一個上界。

注解

注意: ratio() 調用的結果可能會取決于參數的順序。 例如:

>>> SequenceMatcher(None, 'tide', 'diet').ratio()
0.25
>>> SequenceMatcher(None, 'diet', 'tide').ratio()
0.5
quick_ratio()?

相對快速地返回一個 ratio() 的上界。

real_quick_ratio()?

非常快速地返回一個 ratio() 的上界。

這三個返回匹配部分占字符總數的比率的方法可能由于不同的近似級別而給出不一樣的結果,但是 quick_ratio()real_quick_ratio() 總是會至少與 ratio() 一樣大:

>>> s = SequenceMatcher(None, "abcd", "bcde")
>>> s.ratio()
0.75
>>> s.quick_ratio()
0.75
>>> s.real_quick_ratio()
1.0

SequenceMatcher 的示例?

以下示例比較兩個字符串,并將空格視為“垃圾”:

>>> s = SequenceMatcher(lambda x: x == " ",
...                     "private Thread currentThread;",
...                     "private volatile Thread currentThread;")

ratio() 返回一個 [0, 1] 范圍內的整數作為兩個序列相似性的度量。 根據經驗,ratio() 值超過 0.6 就意味著兩個序列是近似匹配的:

>>> print(round(s.ratio(), 3))
0.866

如果你只對兩個序列相匹配的位置感興趣,則 get_matching_blocks() 就很方便:

>>> for block in s.get_matching_blocks():
...     print("a[%d] and b[%d] match for %d elements" % block)
a[0] and b[0] match for 8 elements
a[8] and b[17] match for 21 elements
a[29] and b[38] match for 0 elements

請注意 get_matching_blocks() 返回的最后一個元組總是只用于占位的 (len(a), len(b), 0),這也是元組末尾元素(匹配的元素數量)為 0 的唯一情況。

如果你想要知道如何將第一個序列轉成第二個序列,可以使用 get_opcodes():

>>> for opcode in s.get_opcodes():
...     print("%6s a[%d:%d] b[%d:%d]" % opcode)
 equal a[0:8] b[0:8]
insert a[8:8] b[8:17]
 equal a[8:29] b[17:38]

參見

Differ 對象?

請注意 Differ 所生成的增量并不保證是 最小 差異。 相反,最小差異往往是違反直覺的,因為它們會同步任何可能的地方,有時甚至意外產生相距 100 頁的匹配。 將同步點限制為連續匹配保留了一些局部性概念,這偶爾會帶來產生更長差異的代價。

Differ 類具有這樣的構造器:

class difflib.Differ(linejunk=None, charjunk=None)

可選關鍵字形參 linejunkcharjunk 均為過濾函數 (或為 None):

linejunk: 接受單個字符串作為參數的函數,如果其為垃圾字符串則返回真值。 默認值為 None,意味著沒有任何行會被視為垃圾行。

charjunk: 接受單個字符(長度為 1 的字符串)作為參數的函數,如果其為垃圾字符則返回真值。 默認值為 None,意味著沒有任何字符會被視為垃圾字符。

這些垃圾過濾函數可加快查找差異的匹配速度,并且不會導致任何差異行或字符被忽略。 請閱讀 find_longest_match() 方法的 isjunk 形參的描述了解詳情。

Differ 對象是通過一個單獨方法來使用(生成增量)的:

compare(a, b)?

比較兩個由行組成的序列,并生成增量(一個由行組成的序列)。

每個序列必須包含一個以換行符結尾的單行字符串。 這樣的序列可以通過文件類對象的 readlines() 方法來獲取。 所生成的增量同樣由以換行符結尾的字符串構成,可以通過文件類對象的 writelines() 方法原樣打印出來。

Differ 示例?

此示例比較兩段文本。 首先我們設置文本為以換行符結尾的單行字符串構成的序列(這樣的序列也可以通過文件類對象的 readlines() 方法來獲取):

>>> text1 = '''  1. Beautiful is better than ugly.
...   2. Explicit is better than implicit.
...   3. Simple is better than complex.
...   4. Complex is better than complicated.
... '''.splitlines(keepends=True)
>>> len(text1)
4
>>> text1[0][-1]
'\n'
>>> text2 = '''  1. Beautiful is better than ugly.
...   3.   Simple is better than complex.
...   4. Complicated is better than complex.
...   5. Flat is better than nested.
... '''.splitlines(keepends=True)

接下來我們實例化一個 Differ 對象:

>>> d = Differ()

請注意在實例化 Differ 對象時我們可以傳入函數來過濾掉“垃圾”行和字符。 詳情參見 Differ() 構造器說明。

最后,我們比較兩個序列:

>>> result = list(d.compare(text1, text2))

result 是一個字符串列表,讓我們將其美化打印出來:

>>> from pprint import pprint
>>> pprint(result)
['    1. Beautiful is better than ugly.\n',
 '-   2. Explicit is better than implicit.\n',
 '-   3. Simple is better than complex.\n',
 '+   3.   Simple is better than complex.\n',
 '?     ++\n',
 '-   4. Complex is better than complicated.\n',
 '?            ^                     ---- ^\n',
 '+   4. Complicated is better than complex.\n',
 '?           ++++ ^                      ^\n',
 '+   5. Flat is better than nested.\n']

作為單獨的多行字符串顯示出來則是這樣:

>>> import sys
>>> sys.stdout.writelines(result)
    1. Beautiful is better than ugly.
-   2. Explicit is better than implicit.
-   3. Simple is better than complex.
+   3.   Simple is better than complex.
?     ++
-   4. Complex is better than complicated.
?            ^                     ---- ^
+   4. Complicated is better than complex.
?           ++++ ^                      ^
+   5. Flat is better than nested.

difflib 的命令行接口?

這個實例演示了如何使用 difflib 來創建一個類似于 diff 的工具。 它同樣包含在 Python 源碼發布包中,文件名為 Tools/scripts/diff.py

#!/usr/bin/env python3
""" Command line interface to difflib.py providing diffs in four formats:

* ndiff:    lists every line and highlights interline changes.
* context:  highlights clusters of changes in a before/after format.
* unified:  highlights clusters of changes in an inline format.
* html:     generates side by side comparison with change highlights.

"""

import sys, os, difflib, argparse
from datetime import datetime, timezone

def file_mtime(path):
    t = datetime.fromtimestamp(os.stat(path).st_mtime,
                               timezone.utc)
    return t.astimezone().isoformat()

def main():

    parser = argparse.ArgumentParser()
    parser.add_argument('-c', action='store_true', default=False,
                        help='Produce a context format diff (default)')
    parser.add_argument('-u', action='store_true', default=False,
                        help='Produce a unified format diff')
    parser.add_argument('-m', action='store_true', default=False,
                        help='Produce HTML side by side diff '
                             '(can use -c and -l in conjunction)')
    parser.add_argument('-n', action='store_true', default=False,
                        help='Produce a ndiff format diff')
    parser.add_argument('-l', '--lines', type=int, default=3,
                        help='Set number of context lines (default 3)')
    parser.add_argument('fromfile')
    parser.add_argument('tofile')
    options = parser.parse_args()

    n = options.lines
    fromfile = options.fromfile
    tofile = options.tofile

    fromdate = file_mtime(fromfile)
    todate = file_mtime(tofile)
    with open(fromfile) as ff:
        fromlines = ff.readlines()
    with open(tofile) as tf:
        tolines = tf.readlines()

    if options.u:
        diff = difflib.unified_diff(fromlines, tolines, fromfile, tofile, fromdate, todate, n=n)
    elif options.n:
        diff = difflib.ndiff(fromlines, tolines)
    elif options.m:
        diff = difflib.HtmlDiff().make_file(fromlines,tolines,fromfile,tofile,context=options.c,numlines=n)
    else:
        diff = difflib.context_diff(fromlines, tolines, fromfile, tofile, fromdate, todate, n=n)

    sys.stdout.writelines(diff)

if __name__ == '__main__':
    main()