6.3. difflib

: Lib/difflib.py


HTML context diff, unified diff filecmp

class difflib.SequenceMatcher

(hashable)1980 Ratcliff Obershelp """junk" "junk" (junk Ratcliff Obershelp )

: Ratcliff-Obershelp 32 SequenceMatcher 2

junk : SequenceMatcher junk () 1% 200 "popular" junk SequenceMatcher autojunk False

3.2 : autojunk

class difflib.Differ

Differ SequenceMatcher ()

Differ 2:

'- ' 1
'+ ' 2
'  '
'? '

'?'

class difflib.HtmlDiff

HTML ( HTML )

:

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

HtmlDiff

tabsize 8

wrapcolumn None

linejunk charjunk ndiff() (HtmlDiff HTML ) ndiff()

public :

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

fromlines tolines () HTML

fromdesc todesc ()

context numlines context True False numlines 5 context True numlines context False numlines "next" ("next" )

3.5 : charset HTML 'ISO-8859-1' 'utf-8'

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

fromlines tolines () HTML

make_file()

Tools/scripts/diff.py

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

a b () ((generator)) context diff ()

/ () n 3

diff (*** --- ) io.IOBase.readlines() io.IOBase.writelines() diff ()

lineterm ""

fromfile, tofile, fromfiledate, tofiledate 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 possibilities

possibilities ( 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)

a b () ((generator)) Differ

linejunk charjunk ( None)

linejunk: 1 junk None IS_LINE_JUNK() 1 ('#') SequenceMatcher

charjunk: (1) junk 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() sequence 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')

a b () ((generator)) unified diff ()

(/) n 3

diff (---, +++, @@ ) io.IOBase.readlines() io.IOBase.writelines() diff ()

lineterm ""

fromfile, tofile, fromfiledate, tofiledate 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 a b (bytes ) ( bytes ) *dfunc* dfunc unified_diff() context_diff()

n bytes n str dfunc(a, b, fromfile, tofile, fromfiledate, tofiledate, n, lineterm) dfunc bytes a b

3.5 .

difflib.IS_LINE_JUNK(line)

line '#' ndiff() linejunk

difflib.IS_CHARACTER_JUNK(ch)

ch ndiff() charjunk

Pattern Matching: The Gestalt Approach
John W. Ratcliff D. E. Metzener Dr. Dobbs Journal 19887

6.3.1. SequenceMatcher

SequenceMatcher :

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

isjunk None () junk isjunk None lambda x: 0 ; :

lambda x: x in " \t"

a b (hashable)

autojunk junk

3.2 : autojunk

SequenceMatcher 3: bjunk isjunk True b ; bpopular () popular ; b2j b dict 3 set_seqs() set_seq2() b

3.2 : bjunk bpopular

SequenceMatcher :

set_seqs(a, b)

2

SequenceMatcher 2 1 set_seq2() 1 set_seq1()

set_seq1(a)

12

set_seq2(b)

21

find_longest_match(alo, ahi, blo, bhi)

a[alo:ahi] b[blo:bhi]

isjunk None find_longest_match() a[i:i+k] b[j:j+k] (i, j, k) alo <= i <= i+k <= ahi blo <= j <= j+k <= bhi (i', j', k') k >= k', i <= i' i == i', j <= j' 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 junk junk junk junk

junk ' abcd' 2 ' abcd' 'abcd' 2 '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()

3 (i, j, n) a[i:i+n] == b[j:j+n] 3 i j

(len(a), len(b), 0) n == 0 (i, j, n) (i', j', n') 323i+n != i' j+n != j' 3

>>> 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 2M 2.0*M / T 1.0 0.0

get_matching_blocks() get_opcodes() quick_ratio() real_quick_ratio()

quick_ratio()

ratio()

real_quick_ratio()

ratio()

3 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

6.3.2. SequenceMatcher

2 "junk" :

>>> 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

2 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]

6.3.3. Differ

Differ 100

Differ :

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

linejunk charjunk ( None):

linejunk: junk None junk

charjunk: (1) junk None junk

junk find_longest_match() isjunk

Differ 1:

compare(a, b)

2

readlines() writelines()

6.3.4. Differ

2 ( 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 junk junk Differ()

2:

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

result pretty-print:

>>> 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.

6.3.5. 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()