difflib — Comparar Secuencias

Propósito:Comparar secuencias, especialmente lineas de texto.

El módulo difflib contiene herramientas para computar y trabajar con diferencias entre secuencias. Es especialmente útil para comparando texto, e incluye funciones que producen informes usando varios formatos de diferencia comunes.

Los ejemplos en esta sección usarán todos los datos de prueba comunes en el módulo difflib_data.py:

difflib_data.py
text1 = """Lorem ipsum dolor sit amet, consectetuer adipiscing
elit. Integer eu lacus accumsan arcu fermentum euismod. Donec
pulvinar porttitor tellus. Aliquam venenatis. Donec facilisis
pharetra tortor.  In nec mauris eget magna consequat
convalis. Nam sed sem vitae odio pellentesque interdum. Sed
consequat viverra nisl. Suspendisse arcu metus, blandit quis,
rhoncus ac, pharetra eget, velit. Mauris urna. Morbi nonummy
molestie orci. Praesent nisi elit, fringilla ac, suscipit non,
tristique vel, mauris. Curabitur vel lorem id nisl porta
adipiscing. Suspendisse eu lectus. In nunc. Duis vulputate
tristique enim. Donec quis lectus a justo imperdiet tempus."""

text1_lines = text1.splitlines()

text2 = """Lorem ipsum dolor sit amet, consectetuer adipiscing
elit. Integer eu lacus accumsan arcu fermentum euismod. Donec
pulvinar, porttitor tellus. Aliquam venenatis. Donec facilisis
pharetra tortor. In nec mauris eget magna consequat
convalis. Nam cras vitae mi vitae odio pellentesque interdum. Sed
consequat viverra nisl. Suspendisse arcu metus, blandit quis,
rhoncus ac, pharetra eget, velit. Mauris urna. Morbi nonummy
molestie orci. Praesent nisi elit, fringilla ac, suscipit non,
tristique vel, mauris. Curabitur vel lorem id nisl porta
adipiscing. Duis vulputate tristique enim. Donec quis lectus a
justo imperdiet tempus.  Suspendisse eu lectus. In nunc."""

text2_lines = text2.splitlines()

Comparar cuerpos de texto

La clase Differ funciona en secuencias de líneas de texto y produce deltas legibles por humanos, o instrucciones de cambio, incluyendo diferencias dentro de líneas individuales. La salida predeterminada producida por Differ es similar a la herramienta de línea de comandos diff de Unix. Incluye los valores original de entrada de ambas listas, incluyendo valores comunes, y datos de marcado para indicar qué cambios fueron hechos.

  • Las líneas con prefijo - estaban en la primera secuencia, pero no en la segunda.
  • Las líneas con el prefijo + estaban en la segunda secuencia, pero no la primera.
  • Si una línea tiene una diferencia incremental entre versiones, un línea extra con el prefijo ? se usa para resaltar el cambio dentro del nueva versión.
  • Si una línea no ha cambiado, se imprime con un espacio en blanco adicional en la columna izquierda para que esté alineado con la otra salida que puede tener diferencias.

Rompiendo el texto en una secuencia de líneas individuales antes de pasarlo a compare() produce un resultado más legible que pasando cadenas grandes.

difflib_differ.py
import difflib
from difflib_data import *

d = difflib.Differ()
diff = d.compare(text1_lines, text2_lines)
print('\n'.join(diff))

El comienzo de ambos segmentos de texto en los datos de muestra es el mismo, por lo que la primera línea se imprime sin ninguna anotación adicional.

  Lorem ipsum dolor sit amet, consectetuer adipiscing
  elit. Integer eu lacus accumsan arcu fermentum euismod. Donec

La tercera línea de los datos ha sido cambiada para incluir una coma en el texto modificado. Ambas versiones de la línea están impresas, con la información extra en la línea 5 que muestra la columna donde se modificó el texto, incluyendo el hecho de que se agregó el carácter ,.

- pulvinar porttitor tellus. Aliquam venenatis. Donec facilisis
+ pulvinar, porttitor tellus. Aliquam venenatis. Donec facilisis
?         +

Las siguientes líneas del resultado muestran que se eliminó un espacio adicional.

- pharetra tortor.  In nec mauris eget magna consequat
?                 -

+ pharetra tortor. In nec mauris eget magna consequat

A continuación, se realizó un cambio más complejo, reemplazando varias palabras en una frase.

- convalis. Nam sed sem vitae odio pellentesque interdum. Sed
?                 - --

+ convalis. Nam cras vitae mi vitae odio pellentesque interdum. Sed
?               +++ +++++   +

La última oración del párrafo se modificó significativamente, por lo que la diferencia se representa eliminando la versión anterior y agregando la nueva.

  consequat viverra nisl. Suspendisse arcu metus, blandit quis,
  rhoncus ac, pharetra eget, velit. Mauris urna. Morbi nonummy
  molestie orci. Praesent nisi elit, fringilla ac, suscipit non,
  tristique vel, mauris. Curabitur vel lorem id nisl porta
- adipiscing. Suspendisse eu lectus. In nunc. Duis vulputate
- tristique enim. Donec quis lectus a justo imperdiet tempus.
+ adipiscing. Duis vulputate tristique enim. Donec quis lectus a
+ justo imperdiet tempus.  Suspendisse eu lectus. In nunc.

La función ndiff() produce esencialmente el mismo resultado. El procesamiento está específicamente diseñado para trabajar con datos de texto y eliminando el «ruido» en la entrada.

Otros formatos de salida

Mientras que la clase Differ muestra todas las líneas de entrada, un diff unificado incluye solo las líneas modificadas y un poco de contexto. La función unified_diff() produce este tipo de salida.

difflib_unified.py
import difflib
from difflib_data import *

diff = difflib.unified_diff(
    text1_lines,
    text2_lines,
    lineterm='',
)
print('\n'.join(diff))

El argumento lineterm se usa para indicar a unified_diff() que omita añadir nuevas líneas a las líneas de control que devuelve porque las línes de entrada no las incluyen. Las líneas nuevas se agregan a todas las líneas cuando son impresas. La salida debería ser familiar para los usuarios de muchas herramientas populares de control de versiones.

$ python3 difflib_unified.py

---
+++
@@ -1,11 +1,11 @@
 Lorem ipsum dolor sit amet, consectetuer adipiscing
 elit. Integer eu lacus accumsan arcu fermentum euismod. Donec
-pulvinar porttitor tellus. Aliquam venenatis. Donec facilisis
-pharetra tortor.  In nec mauris eget magna consequat
-convalis. Nam sed sem vitae odio pellentesque interdum. Sed
+pulvinar, porttitor tellus. Aliquam venenatis. Donec facilisis
+pharetra tortor. In nec mauris eget magna consequat
+convalis. Nam cras vitae mi vitae odio pellentesque interdum. S
ed
 consequat viverra nisl. Suspendisse arcu metus, blandit quis,
 rhoncus ac, pharetra eget, velit. Mauris urna. Morbi nonummy
 molestie orci. Praesent nisi elit, fringilla ac, suscipit non,
 tristique vel, mauris. Curabitur vel lorem id nisl porta
-adipiscing. Suspendisse eu lectus. In nunc. Duis vulputate
-tristique enim. Donec quis lectus a justo imperdiet tempus.
+adipiscing. Duis vulputate tristique enim. Donec quis lectus a
+justo imperdiet tempus.  Suspendisse eu lectus. In nunc.

Usar context_diff() produce una salida legible similar.

Datos basura

Todas las funciones que producen secuencias de diferencia aceptan argumentos para indicar qué líneas se deben ignorar y cuáles son los caracteres dentro de una línea que deben ser ignorados. Estos parámetros pueden ser utilizados por ejemplo para omitir los cambios de marcado o espacio en blanco en dos versiones de un archivo.

difflib_junk.py
# This example is adapted from the source for difflib.py.

from difflib import SequenceMatcher


def show_results(match):
    print('  a    = {}'.format(match.a))
    print('  b    = {}'.format(match.b))
    print('  size = {}'.format(match.size))
    i, j, k = match
    print('  A[a:a+size] = {!r}'.format(A[i:i + k]))
    print('  B[b:b+size] = {!r}'.format(B[j:j + k]))


A = " abcd"
B = "abcd abcd"

print('A = {!r}'.format(A))
print('B = {!r}'.format(B))

print('\nWithout junk detection:')
s1 = SequenceMatcher(None, A, B)
match1 = s1.find_longest_match(0, len(A), 0, len(B))
show_results(match1)

print('\nTreat spaces as junk:')
s2 = SequenceMatcher(lambda x: x == " ", A, B)
match2 = s2.find_longest_match(0, len(A), 0, len(B))
show_results(match2)

Por defecto Differ no ignora ninguna línea o caracteres explícitamente, sino más bien confia en la capacidad de SequenceMatcher para detectar ruido. El valor por defecto para ndiff() es ignorar los caracteres de espacio y tabulación.

$ python3 difflib_junk.py

A = ' abcd'
B = 'abcd abcd'

Without junk detection:
  a    = 0
  b    = 4
  size = 5
  A[a:a+size] = ' abcd'
  B[b:b+size] = ' abcd'

Treat spaces as junk:
  a    = 1
  b    = 0
  size = 4
  A[a:a+size] = 'abcd'
  B[b:b+size] = 'abcd'

Comparar tipos arbitrarios

La clase SequenceMatcher compara dos secuencias de cualquier tipo, siempre y cuando los valores sean hashable. Utiliza un algoritmo para identificar los bloques contiguos más largos de las secuencias, eliminando los valores «basura» que no contribuyen a los datos reales.

La función get_opcodes() devuelve una lista de instrucciones para modificar la primera secuencia para que coincida con la segunda. Las instrucciones se codifican como tuplas de cinco elementos, incluida una cadena de instrucción (el «opcode», ve the table below) y dos pares de índices de inicio y final en las secuencias (denotadas como i1, i2, j1, y j2).

Instrucciones difflib.get_opcodes()
Opcode Definición
'replace' Reemplaza a[i1:i2] con b[j1:j2]
'delete' Elimina``a[i1:i2]`` en su totalidad
'insert' Inserta b[j1:j2] en a[i1:i1]
'equal' Las subsecuencias ya son iguales
difflib_seq.py
import difflib

s1 = [1, 2, 3, 5, 6, 4]
s2 = [2, 3, 5, 4, 6, 1]

print('Initial data:')
print('s1 =', s1)
print('s2 =', s2)
print('s1 == s2:', s1 == s2)
print()

matcher = difflib.SequenceMatcher(None, s1, s2)
for tag, i1, i2, j1, j2 in reversed(matcher.get_opcodes()):

    if tag == 'delete':
        print('Remove {} from positions [{}:{}]'.format(
            s1[i1:i2], i1, i2))
        print('  before =', s1)
        del s1[i1:i2]

    elif tag == 'equal':
        print('s1[{}:{}] and s2[{}:{}] are the same'.format(
            i1, i2, j1, j2))

    elif tag == 'insert':
        print('Insert {} from s2[{}:{}] into s1 at {}'.format(
            s2[j1:j2], j1, j2, i1))
        print('  before =', s1)
        s1[i1:i2] = s2[j1:j2]

    elif tag == 'replace':
        print(('Replace {} from s1[{}:{}] '
               'with {} from s2[{}:{}]').format(
                   s1[i1:i2], i1, i2, s2[j1:j2], j1, j2))
        print('  before =', s1)
        s1[i1:i2] = s2[j1:j2]

    print('   after =', s1, '\n')

print('s1 == s2:', s1 == s2)

Este ejemplo compara dos listas de enteros y usa get_opcodes() para derivar las instrucciones para convertir la lista original en la versión más nueva. Las modificaciones se aplican en orden inverso para que los índices de lista permanezcan precisos después de que los elementos se agreguen y eliminen.

$ python3 difflib_seq.py

Initial data:
s1 = [1, 2, 3, 5, 6, 4]
s2 = [2, 3, 5, 4, 6, 1]
s1 == s2: False

Replace [4] from s1[5:6] with [1] from s2[5:6]
  before = [1, 2, 3, 5, 6, 4]
   after = [1, 2, 3, 5, 6, 1]

s1[4:5] and s2[4:5] are the same
   after = [1, 2, 3, 5, 6, 1]

Insert [4] from s2[3:4] into s1 at 4
  before = [1, 2, 3, 5, 6, 1]
   after = [1, 2, 3, 5, 4, 6, 1]

s1[1:4] and s2[0:3] are the same
   after = [1, 2, 3, 5, 4, 6, 1]

Remove [1] from positions [0:1]
  before = [1, 2, 3, 5, 4, 6, 1]
   after = [2, 3, 5, 4, 6, 1]

s1 == s2: True

SequenceMatcher funciona con clases personalizadas, así como tipos incorporados, siempre que sean hashables.

Ver también