Python Difflib 差异和比较 Ndiff

4 投票
3 回答
15331 浏览
提问于 2025-04-16 10:17

我想做的事情类似于变更控制系统的功能,它们会比较两个文件,并在文件每次变化时保存一个小的差异。
我在阅读这个页面:http://docs.python.org/library/difflib.html,但似乎没能完全理解。

我试图在下面的简单程序中重现这个功能,
但我发现我似乎缺少了一点,那就是差异(Delta)至少包含和原始文件一样多的内容,甚至更多。

难道不可能只得到纯粹的变化吗?
我问这个问题的原因应该很明显——为了节省磁盘空间。
我可以每次都保存整个代码块,但更好的做法是先保存当前的代码,然后只保存小的变化。

我还在努力弄清楚为什么很多difflib的函数返回的是生成器而不是列表,这有什么好处呢?

difflib能满足我的需求吗,还是我需要找一个功能更强大的专业工具?

# Python Difflib demo 
# Author: Neal Walters 
# loosely based on http://ahlawat.net/wordpress/?p=371
# 01/17/2011 

# build the files here - later we will just read the files probably 
file1Contents="""
for j = 1 to 10: 
   print "ABC"
   print "DEF" 
   print "HIJ"
   print "JKL"
   print "Hello World"
   print "j=" + j 
   print "XYZ"
"""

file2Contents = """
for j = 1 to 10: 
   print "ABC"
   print "DEF" 
   print "HIJ"
   print "JKL"
   print "Hello World"
   print "XYZ"
print "The end"
"""

filename1 = "diff_file1.txt" 
filename2 = "diff_file2.txt" 

file1 = open(filename1,"w") 
file2 = open(filename2,"w") 

file1.write(file1Contents) 
file2.write(file2Contents) 

file1.close()
file2.close() 
#end of file build 

lines1 = open(filename1, "r").readlines()
lines2 = open(filename2, "r").readlines()

import difflib

print "\n FILE 1 \n" 
for line in lines1:
  print line 

print "\n FILE 2 \n" 
for line in lines2: 
  print line 

diffSequence = difflib.ndiff(lines1, lines2) 

print "\n ----- SHOW DIFF ----- \n" 
for i, line in enumerate(diffSequence):
    print line

diffObj = difflib.Differ() 
deltaSequence = diffObj.compare(lines1, lines2) 
deltaList = list(deltaSequence) 

print "\n ----- SHOW DELTALIST ----- \n" 
for i, line in enumerate(deltaList):
    print line



#let's suppose we store just the diffSequence in the database 
#then we want to take the current file (file2) and recreate the original (file1) from it
#by backward applying the diff 

restoredFile1Lines = difflib.restore(diffSequence,1)  # 1 indicates file1 of 2 used to create the diff 

restoreFileList = list(restoredFile1Lines)

print "\n ----- SHOW REBUILD OF FILE1 ----- \n" 
# this is not showing anything! 
for i, line in enumerate(restoreFileList): 
    print line

谢谢!

更新:

contextDiffSeq = difflib.context_diff(lines1, lines2) 
contextDiffList = list(contextDiffSeq) 

print "\n ----- SHOW CONTEXTDIFF ----- \n" 
for i, line in enumerate(contextDiffList):
    print line

----- 显示上下文差异 -----




* 5,9 **

 print "HIJ"

 print "JKL"

 print "Hello World"
  • print "j=" + j

    print "XYZ"

--- 5,9 ----

 print "HIJ"

 print "JKL"

 print "Hello World"

 print "XYZ"
  • print "The end"

另一个更新:

在以前的Panvalet和Librarian时代,主机的源代码管理工具,你可以这样创建一个变更集:

++ADD 9
   print "j=" + j 

这简单来说就是在第9行后添加一行(或多行)。
然后还有像++REPLACE或++UPDATE这样的词。

http://www4.hawaii.gov/dags/icsd/ppmo/Stds_Web_Pages/pdf/it110401.pdf

3 个回答

1

如果你只是想查看文件的变化,可以使用统一差异或上下文差异。你看到的文件比较大,是因为它包含了那些相同的行。

返回生成器的好处是,不需要一次性把所有内容都放在内存里。这对于比较非常大的文件时特别有用。

6

我也在想,为什么很多difflib的函数返回的是生成器而不是列表,这样做有什么好处呢?

想一想,如果你要比较文件,这些文件理论上(而且实际上)可能会非常大。如果直接返回一个列表,那就意味着要把所有的数据都读到内存里,这样做可不太聪明。

至于只返回差异,使用生成器还有另一个好处——你可以逐个遍历这些差异,只保留你感兴趣的行。

如果你查看一下difflib的文档,关于Differ风格的差异,你会看到一段话是这样写的:

Each line of a Differ delta begins with a two-letter code:
Code    Meaning
'- '    line unique to sequence 1
'+ '    line unique to sequence 2
'  '    line common to both sequences
'? '    line not present in either input sequence

所以,如果你只想要差异,可以很容易地通过使用str.startswith来过滤出来。

你还可以使用difflib.context_diff来获取一个紧凑的差异,只显示变化的部分。

4

差异(diffs)需要包含足够的信息,以便将一个版本更新到另一个版本。所以,对于你在一个非常小的文档中只改动一行的实验来说,存储整个文档可能会更便宜。

库函数返回迭代器,这样可以方便那些内存有限或者只需要查看结果序列部分内容的用户。在Python中,这没问题,因为每个迭代器都可以通过一个简单的 list(an_iterator) 表达式转换成列表。

大多数差异比较是基于文本行的,但也可以逐个字符进行比较,difflib 就能做到这一点。你可以看看 Differ 类在 difflib 中的用法。

虽然示例中使用的是人类友好的输出,但内部管理的差异信息则更加紧凑,适合计算机处理。此外,差异通常包含冗余信息(比如要删除的行的文本),这样可以确保补丁和合并更改的安全性。如果你觉得可以的话,可以通过自己的代码去除这些冗余信息。

我刚刚读到 difflib 选择了最少惊讶原则,而不是追求最优,这一点我不反对。有一些 众所周知 的算法可以快速生成最小的更改集。

我曾经用大约1250行Java代码编写了一个通用的差异引擎,并实现了其中一个最优算法(JRCS)。它适用于任何可以进行相等比较的元素序列。如果你想自己构建一个解决方案,我认为将JRCS翻译或重新实现成Python不超过300行代码。

处理 difflib 生成的输出,使其更加紧凑也是一个选择。这是一个小文件的示例,包含三个更改(一个添加、一个更改和一个删除):

---  
+++  
@@ -7,0 +7,1 @@
+aaaaa
@@ -9,1 +10,1 @@
-c= 0
+c= 1
@@ -15,1 +16,0 @@
-    m = re.match(code_re, text)

补丁所说的内容可以很容易地简化为:

+7,1 
aaaaa
-9,1 
+10,1
c= 1
-15,1

对于你自己的示例,简化后的输出将是:

-8,1
+9,1
print "The end"

为了安全起见,保留一个前导标记('>')用于必须插入的行可能是个好主意。

-8,1
+9,1
>print "The end"

这样更接近你的需求了吗?

这是一个简单的函数,用于进行压缩。你需要编写自己的代码来应用这种格式的补丁,但应该很简单。

def compact_a_unidiff(s):
    s = [l for l in s if l[0] in ('+','@')]
    result = []
    for l in s:
        if l.startswith('++'):
            continue
        elif l.startswith('+'):
            result.append('>'+ l[1:])
        else:
            del_cmd, add_cmd = l[3:-3].split()
            del_pair, add_pair = (c.split(',') for c in (del_cmd,add_cmd))
            if del_pair[1]  != '0':
                result.append(del_cmd)
            if add_pair[1] != '0':
                result.append(add_cmd)
    return result

撰写回答