[Tutor] Word-by-word diff in Python

Alexandre Ratti alex@gabuzomeu.net
Fri, 19 Apr 2002 11:27:41 +0200


Hi Danny,


At 20:29 18/04/2002 -0700, Danny Yoo wrote:
>Interesting!  Hmmm... if the indentation or formatting is significant, we
>could transform a line-by-line diff utility into a word-by-word by turning
>the newlines into some sort of sentinel "NEWLINE"  character.

Yes, thanks; that's the key idea. I had toyed with difflib but I fed it two 
strings instead of word lists; hence it spat back a character-based diff.

>We could then apply a string.split() to break the lines into individual
>words.  Python comes with a standard library module called "difflib":
>
>     http://www.python.org/doc/current/lib/module-difflib.html

One problem I had with this module is that it was added to the standard 
library in 2.1 and 2.2, whereas I try to make my app compatible with Python 
2.0.

I found a partial backported version in ViewCVS; it features the 
SequenceMatcher class, but the ndiff class is not included.

> >>> difflib.ndiff(revision_1, revision_2)
><generator object at 0x81641b8>
> >>> diff = difflib.ndiff(revision_1, revision_2)

>To grab the whole diff at once, let's convince Python to give it to us as
>a list:

> >>> results = list(difflib.ndiff(revision_1, revision_2))

>And the output here can be modified to look like a nice HTML formatted
>text with strikeouts and everything.  *grin*

Yes, that's good. I found an example that used the SequenceMatcher class 
directly, though it's lower-level. Here is a test implementation:

##
from difflib import SequenceMatcher

class TextDiff:
     """Create diffs of text snippets."""

     def __init__(self, source, target):
         """source = source text - target = target text"""
         self.nl = "<NL>"
         self.delTag = "<span class='deleted'>%s</span>"
         self.insTag = "<span class='inserted'>%s</span>"
         self.source = source.replace("\n", "\n%s" % self.nl).split()
         self.target = target.replace("\n", "\n%s" % self.nl).split()
         self.deleteCount, self.insertCount, self.replaceCount = 0, 0, 0
         self.diffText = None
         self.cruncher = SequenceMatcher(None, self.source,
                                         self.target)
         self._buildDiff()

     def _buildDiff(self):
         """Create a tagged diff."""
         outputList = []
         for tag, alo, ahi, blo, bhi in self.cruncher.get_opcodes():
             if tag == 'replace':
                 # Text replaced = deletion + insertion
                 outputList.append(self.delTag % " 
".join(self.source[alo:ahi]))
                 outputList.append(self.insTag % " 
".join(self.target[blo:bhi]))
                 self.replaceCount += 1
             elif tag == 'delete':
                 # Text deleted
                 outputList.append(self.delTag % " 
".join(self.source[alo:ahi]))
                 self.deleteCount += 1
             elif tag == 'insert':
                 # Text inserted
                 outputList.append(self.insTag % " 
".join(self.target[blo:bhi]))
                 self.insertCount += 1
             elif tag == 'equal':
                 # No change
                 outputList.append(" ".join(self.source[alo:ahi]))
         diffText = " ".join(outputList)
         diffText = " ".join(diffText.split())
         self.diffText = diffText.replace(self.nl, "\n")

     def getStats(self):
         "Return a tuple of stat values."
         return (self.insertCount, self.deleteCount, self.replaceCount)

     def getDiff(self):
         "Return the diff text."
         return self.diffText

if __name__ == "__main__":
     ch1 = """Today, a generation raised in the shadows of the Cold
     War assumes new responsibilities in a world warmed by the sunshine of
     freedom"""

     ch2 = """Today, pythonistas raised in the shadows of the Cold
     War assumes responsibilities in a world warmed by the sunshine of
     spam and freedom"""

     differ = TextDiff(ch1, ch2)

     print "%i insertion(s), %i deletion(s), %i replacement(s)" % 
differ.getStats()
     print differ.getDiff()

1 insertion(s), 1 deletion(s), 1 replacement(s)
Today, a <span class='deleted'>generation</span> <span 
class='inserted'>pythonista</span> raised in the shadows of the Cold
  War assumes <span class='deleted'>new</span> responsibilities in a world 
warmed by the sunshine of
  <span class='inserted'>spam and</span> freedom
##


Cheers.

Alexandre