¿Eliminar líneas duplicadas por pares?

16

Encontré este caso de uso hoy. Parece simple a primera vista, pero perder el tiempo con sort, uniq, sedy awkreveló que es no trivial.

¿Cómo puedo eliminar todos los pares de líneas duplicadas? En otras palabras, si hay un número par de duplicados de una línea dada, elimínelos todos; Si hay un número impar de líneas duplicadas, elimine todas menos una. (Se puede suponer una entrada ordenada).

Una solución elegante y limpia es preferible.

Entrada de ejemplo:

a
a
a
b
b
c
c
c
c
d
d
d
d
d
e

Salida de ejemplo:

a
d
e
Comodín
fuente

Respuestas:

6

Resolví la sedrespuesta poco después de publicar esta pregunta; nadie más ha usadosed hasta ahora, así que aquí está:

sed '$!N;/^\(.*\)\n\1$/d;P;D'

Un poco de juego con el problema más general (¿qué hay de eliminar líneas en conjuntos de tres? ¿O cuatro o cinco?) Proporcionó la siguiente solución extensible:

sed -e ':top' -e '$!{/\n/!{N;b top' -e '};};/^\(.*\)\n\1$/d;P;D' temp

Extendido para eliminar triples de líneas:

sed -e ':top' -e '$!{/\n.*\n/!{N;b top' -e '};};/^\(.*\)\n\1\n\1$/d;P;D' temp

O para eliminar cuadrantes de líneas:

sed -e ':top' -e '$!{/\n.*\n.*\n/!{N;b top' -e '};};/^\(.*\)\n\1\n\1\n\1$/d;P;D' temp

sed tiene una ventaja adicional sobre la mayoría de las otras opciones, que es su capacidad de operar realmente en una secuencia, sin necesidad de más almacenamiento de memoria que el número real de líneas para verificar si hay duplicados.


Como Cuonglm señaló en los comentarios , es necesario establecer la configuración regional en C para evitar fallas al eliminar correctamente las líneas que contienen caracteres de varios bytes. Entonces los comandos anteriores se convierten en:

LC_ALL=C sed '$!N;/^\(.*\)\n\1$/d;P;D' temp
LC_ALL=C sed -e ':top' -e '$!{/\n/!{N;b top' -e '};};/^\(.*\)\n\1$/d;P;D' temp
LC_ALL=C sed -e ':top' -e '$!{/\n.*\n/!{N;b top' -e '};};/^\(.*\)\n\1\n\1$/d;P;D' temp
# Etc.
Comodín
fuente
2
@Wildcard: es posible que desee establecer la configuración regional en C, de lo contrario, en la configuración regional de varios bytes, los caracteres no válidos en esa configuración regional causarán un error en el comando.
Cuonglm
4

No es muy elegante, pero es tan simple como se me ocurre:

uniq -c input | awk '{if ($1 % 2 == 1) { print substr($0, 9) }}'

El substr () solo recorta la uniqsalida. Eso funcionará hasta que tenga más de 9,999,999 duplicados de una línea (en cuyo caso, la salida de uniq puede tener más de 9 caracteres).

Jeff Schaller
fuente
Lo intenté uniq -c input | awk '{if ($1 %2 == 1) { print $2 } }'y pareció funcionar igual de bien. ¿Alguna razón por la cual la substrversión es mejor?
Joseph R.
1
@JosephR., Si hay algún espacio en blanco en las líneas, la versión en su comentario fallará.
Comodín el
Eso es verdad. En ese caso, ¿no sería un bucle para imprimir los campos $2para $NFser más robusto?
Joseph R.
@JosephR .: ¿Por qué crees que tu alternativa sería más sólida? Es posible que tenga dificultades para que funcione correctamente cuando hay varios espacios consecutivos; por ejemplo, foo   bar.
G-Man dice 'reinstalar a Mónica' el
@JosephR., No, porque cambiaría / eliminaría la delimitación de espacios en blanco. uniq(al menos en GNU coreutils) parece usar de manera confiable exactamente 9 caracteres antes del texto en sí; Sin embargo, no puedo encontrar esto documentado en ninguna parte, y no está en las especificaciones POSIX .
Comodín el
4

Prueba este awkscript a continuación:

#!/usr/bin/awk -f
{
  if ((NR!=1) && (previous!=$0) && (count%2==1)) {
    print previous;
    count=0;
  }
  previous=$0;
  count++;
}
END {
  if (count%2==1) {
    print previous;
  }
}

Se supone que el lines.txtarchivo está ordenado.

La prueba:

$ chmod +x script.awk
$ ./script.awk lines.txt
a
d
e
Jay jargot
fuente
4

Con pcregreppara una muestra dada:

pcregrep -Mv '(.)\n\1$' file

o de una manera más general:

pcregrep -Mv '(^.*)\n\1$' file
jimmij
fuente
¿No debería haber un ancla de "fin de línea" al final? De lo contrario, fallará en una línea que coincida con la línea anterior a ella, además de tener caracteres finales.
Comodín el
@Wildcard, sí, eso está mejor. corregido, gracias.
jimmij
¡Muy genial! (+1)
JJoao
4

Si la entrada está ordenada:

perl -0pe  'while(s/^(.*)\n\1\n//m){}'
JJoao
fuente
Tienes un fallo de anclaje aquí. Intente ejecutarlo, por ejemplo, pineapple\napple\ncoconuty la salida es pinecoconut.
Comodín
@Wildcard: gracias. Tienes razón. Ver si mi actualización tiene sentido ...
JJoao
1
Sí. Me preguntaba por qué estaba usando en \nlugar de $darle el /mmodificador, pero luego me di cuenta de que $usaría dejaría una línea en blanco en lugar de líneas eliminadas. Se ve bien ahora; Eliminé la versión incorrecta ya que solo agregaba ruido. :)
Comodín
@wildcard, gracias por la reducción de ruido ☺
JJoao
3

Me gusta pythonpara esto, por ejemplo con python2.7+

from itertools import groupby
with open('input') as f:
    for k, g in groupby(f):
            if len(list(g)) % 2:
                    print(k),
iruvar
fuente
2

Como entendí la pregunta, opté por awk, usando un hash de cada registro, en este caso supongo que RS = \ n, pero se puede cambiar para considerar cualquier otro tipo de arreglos, se puede arreglar para considerar un número par de repeticiones, en lugar de las impares, con un parámetro o un pequeño cuadro de diálogo. Cada línea se usa como el hash y su recuento aumenta, al final del archivo se escanea la matriz e imprime cada recuento par del registro. Incluyo el recuento para verificar, pero eliminar una [x] es suficiente para resolver ese problema.

HTH

código de líneas de conteo

#!/usr/bin/nawk -f
{a[$0]++}
END{for (x in a) if (a[x]%2!=0) print x,a[x] }

Data de muestra:

a
One Sunny Day
a
a
b
my best friend
my best friend
b
c
c
c
One Sunny Day
c
d
my best friend
my best friend
d
d
d
One Sunny Day
d
e
x
k
j
my best friend
my best friend

Ejecución de muestra:

countlines feed.txt
j 1
k 1
x 1
a 3
One Sunny Day 3
d 5
e 1
Moisés Najar
fuente
Es una buena pieza de awkcódigo, pero desafortunadamente awklas matrices asociativas no están ordenadas en absoluto, ni conservan el orden.
Comodín el
@Wildcard, estoy de acuerdo con usted, si requiere el orden de entrada, en lugar de un orden de clasificación, se puede implementar a través de una clave hash adicional, la ventaja de esto es que no tiene que ordenar la entrada, ya que el orden de clasificación se puede hacer al final con una salida más pequeña;)
Moises Najar
@Wildcard si necesita preservar el pedido, mencione eso en la pregunta. Este enfoque también fue mi primer pensamiento y no mencionas el orden que no sea decir que podemos suponer que el archivo está ordenado. Por supuesto, si el archivo está ordenado, siempre puede pasar la salida de esta solución sort.
terdon
@terdon, por supuesto que tienes razón; la salida se puede ordenar nuevamente. Buen punto. También vale la pena señalar que !=0está implícito en cómo awkconvierte los números en valores verdadero / falso, lo que hace que esto se reduzca aawk '{a[$0]++}END{for(x in a)if(a[x]%2)print x}'
comodín
1

Si la entrada está ordenada, ¿qué pasa con esto awk?

awk '{ x[$0]++; if (prev != $0 && x[prev] % 2 == 1) { print prev; } prev = $0; } END { if (x[prev] % 2 == 1) print prev; }' sorted
taliezin
fuente
1

con perl:

uniq -c file | perl -lne 'if (m(^\s*(\d+) (.*)$)) {print $2 if $1 % 2 == 1}'
xx4h
fuente
1

Usando construcciones de shell,

uniq -c file | while read a b; do if (( $a & 1 == 1 )); then echo $b; fi done
Guido
fuente
1
Eso rompe con líneas que comienzan o terminan con espacios en blanco (o más, porque olvidó citar $b).
Gilles 'SO- deja de ser malvado'
1

Divertido rompecabezas!

En perl:

#! /usr/bin/env perl

use strict;
use warnings;

my $prev;
while (<>) {
  $prev = $_, next unless defined $prev;  # prime the pump

  if ($prev ne $_) {
    print $prev;
    $prev = $_;                           # first half of a new pair
  }
  else {
    undef $prev;                          # discard and unprime the pump
  }
}

print $prev if defined $prev;             # possible trailing odd line

Verbosamente en Haskell:

main :: IO ()
main = interact removePairs
  where removePairs = unlines . go . lines
        go [] = []
        go [a] = [a]
        go (a:b:rest)
          | a == b = go rest
          | otherwise = a : go (b:rest)

Tersely en Haskell:

import Data.List (group)
main = interact $ unlines . map head . filter (odd . length) . group . lines
Greg Bacon
fuente
0

una versión: uso "delimitadores" para simplificar el bucle interno (se supone que la primera línea no es __unlikely_beginning__ y se supone que el texto no termina con la línea: __unlikely_ending__y agregue esa línea especial del delimitador al final de las líneas introducidas. El algoritmo puede asumir ambos:)

{ cat INPUTFILE_or_just_-  ; echo "__unlikely_ending__" ; } | awk '
  BEGIN {mem="__unlikely_beginning__"; occured=0; }  

    ($0 == mem)            { occured++ ; next } 

    ( occured%2 )           { print mem ;} 
                            { mem=$0; occured=1; }
'

Entonces :

  • recordamos el patrón que estamos viendo actualmente, incrementándolo en uno cada vez que vuelve a ocurrir. [y si volviera a ocurrir, omitimos las siguientes 2 acciones, que son para el caso cuando el patrón cambia]
  • Cuando el patrón cambia:
    • si no es un múltiplo de 2, imprimimos una ocurrencia del patrón memorizado
    • y en todos los casos cuando el patrón ha cambiado: el nuevo patrón memorizado es el patrón actual, y solo lo vimos una vez.
Olivier Dulac
fuente