¿Cómo eliminar líneas duplicadas dentro de un archivo de texto?


126

Un enorme archivo de texto (hasta 2 GiB) contiene aproximadamente 100 duplicados exactos de cada línea (inútil en mi caso, ya que el archivo es una tabla de datos similar a CSV).

Lo que necesito es eliminar todas las repeticiones mientras (preferiblemente, pero esto se puede sacrificar por un aumento significativo del rendimiento) mantener el orden de secuencia original. En el resultado, cada línea debe ser única. Si hubiera 100 líneas iguales (generalmente los duplicados se extienden por el archivo y no serán vecinos), solo quedará uno del tipo.

He escrito un programa en Scala (considérelo Java si no sabe sobre Scala) para implementar esto. ¿Pero tal vez hay herramientas nativas escritas en C más rápidas capaces de hacerlo más rápido?

ACTUALIZACIÓN: la awk '!seen[$0]++' filenamesolución parecía funcionar bien para mí, siempre y cuando los archivos estuvieran cerca de 2 GiB o menos, pero ahora que tengo que limpiar un archivo de 8 GiB ya no funciona. Parece tener infinito en una Mac con 4 GiB RAM y una PC con Windows 7 de 64 bits con 4 GiB RAM y 6 GiB swap simplemente se queda sin memoria. Y no me entusiasma probarlo en Linux con 4 GiB RAM dada esta experiencia.


esto destruirá su pedido, pero, ¿ha intentado sort -u, no tengo idea de cómo o si puede ejecutarse en un archivo tan masivo
0x7c0

55
C a menudo no es significativamente más rápido que Java, y si lo está ejecutando (en orden) ahora, hay una buena posibilidad de que termine antes de obtener una respuesta aquí, implementarla y termine de ejecutarse; fuera de servicio, sort -uprobablemente será más rápido.
Kevin

Respuestas:


215

Una awksolución vista en #bash (Freenode):

awk '!seen[$0]++' filename

1
Solo probé esto en un archivo 2G y tardé tres minutos en mi computadora portátil. No está mal. También probé uniq filename | awk '! visto [$ 0] ++', pero no fue más rápido.
mgjk

Esto es sorprendentemente más rápido que una awkversión más detallada que utiliza 2 búsquedas de matriz (se muestra como una explicación ampliada en la respuesta de Gilles): 0m36.132s vs 0m49.958s ... para 50 millones de líneas ... Pensé que el cuello de botella sería la E / S, pero la búsqueda de matriz adicional es ... 1 millón de elementos en la matriz parece hacer una abolladura bastante significativa ...
Peter.O

Pero, ¿cómo se compara eso con sort -u ...?
HashWizard

1
@HashWizard: este comando no ordena, pero elimina cada próxima aparición de la misma línea
enzotib

1
@MaxWilliams sí, funciona si se distribuyen al azar.
Setholopolus

47

Hay un método simple (que no quiere decir obvio) que utiliza utilidades estándar que no requiere una gran memoria, excepto para ejecutarse sort, que en la mayoría de las implementaciones tiene optimizaciones específicas para archivos de gran tamaño (un buen algoritmo de ordenamiento externo). Una ventaja de este método es que solo recorre todas las líneas dentro de las utilidades especiales, nunca dentro de los lenguajes interpretados.

<input nl -b a -s : |           # number the lines
sort -t : -k 2 -u |             # sort and uniquify ignoring the line numbers
sort -t : -k 1n |               # sort according to the line numbers
cut -d : -f 2- >output          # remove the line numbers

Si todas las líneas comienzan con un carácter que no sea un espacio en blanco, puede prescindir de algunas de las opciones:

<input nl | sort -k 2 -u | sort -k 1n | cut -f 2- >output

Para una gran cantidad de duplicación, un método que solo requiere almacenar una sola copia de cada línea en la memoria funcionará mejor. Con un poco de interpretación general, hay un script awk muy conciso para eso (ya publicado por enzotib ):

<input awk '!seen[$0]++'

De manera menos concisa: !seen[$0] {print} {seen[$0] += 1}es decir, imprima la línea actual si aún no se ha visto, luego incremente el seencontador para esta línea (las variables no inicializadas o los elementos de la matriz tienen el valor numérico 0).

Para líneas largas, puede ahorrar memoria manteniendo solo una suma de comprobación no falsificable (por ejemplo, un resumen criptográfico) de cada línea. Por ejemplo, con SHA-1, solo necesita 20 bytes más una sobrecarga constante por línea. Pero calcular los resúmenes es bastante lento; este método solo ganará si tiene una CPU rápida (especialmente una con un acelerador de hardware para calcular los resúmenes) y no hay mucha memoria en relación con el tamaño del archivo y líneas suficientemente largas. Ninguna utilidad básica le permite calcular una suma de verificación para cada línea; tendría que soportar la sobrecarga de interpretación de Perl / Python / Ruby / ... o escribir un programa compilado dedicado.

<input perl -MDigest::MD5 -ne '$seen{Digest::MD5::md5($_)}++ or print' >output

@Gilles Según su explicación de awk '!seen[$0]++', ¿significa que si awk ve 2 líneas duplicadas, mantendrá la siempre primera e ignorará todas las siguientes? (¿O se quedará con el último?)
user779159

1
@ user779159 Conserva la primera: cada línea de entrada se imprime inmediatamente (primera aparición) o no se imprime (aparición repetida).
Gilles

¿Pero cómo se compara eso con sort -u ...?
HashWizard

@HashWizard A simple sort -ucambia el orden. Mi respuesta muestra soluciones que preservan el orden (el orden de las primeras ocurrencias, para ser precisos).
Gilles

@Gilles, ¿diría que es más rápido que sort -u para archivos grandes (10G) con 50% de duplicados?
HashWizard

25
sort -u big-csv-file.csv > duplicates-removed.csv

Tenga en cuenta que el archivo de salida se ordenará.


1
¡No tan rápido como el awkcomando en otras respuestas, pero conceptualmente simple!
Johann

@Johann Estoy haciendo esto con bastante frecuencia en archivos con cientos de miles (incluso millones) de cadenas cortas terminadas en nueva línea. Obtengo los resultados bastante rápido para los experimentos que estoy haciendo. Puede ser más importante si se usa en scripts que se ejecutan una y otra vez, el ahorro de tiempo puede ser considerable.
Vladislavs Dovgalecs

1
Se usa sort -upara eliminar duplicados durante la ordenación, en lugar de después. (Y ahorra ancho de banda de memoria) canalizándolo a otro programa). Esto solo es mejor que la awkversión si desea que su salida también esté ordenada. (El OP sobre esta pregunta quiere que se conserve su orden original , por lo que esta es una buena respuesta para un caso de uso ligeramente diferente)
Peter Cordes,

Tomó alrededor de un minuto, para mí, para un archivo de línea de 5,5 millones (1,8 GB en total). Brillante.
Max Williams el

18

Suponiendo que puede permitirse mantener tanto como el archivo desduplicado en la memoria (si sus datos están duplicados por un factor de 100, eso debería ser aproximadamente 20MiB + sobrecarga), puede hacer esto muy fácilmente con Perl.

$ perl -ne 'print unless $dup{$_}++;' input_file > output_file

Esto preserva el orden también.

Si lo desea, puede extraer el número de apariciones de cada línea del %duphash, como un bono gratis adicional.

Si lo prefiere awk, esto también debería hacerlo (la misma lógica que la versión perl, el mismo orden, los mismos datos recopilados en la dupvariable):

$ awk '{if (++dup[$0] == 1) print $0;}' input_file > output_file

Esto es demasiado bueno @ Mat, estaba a punto de sorber el archivo, jajaja ;-).
Nikhil Mulley

Ahora esperando a @ManAtWork por su tejido mágico sed y awk también :-)
Nikhil Mulley

impresionante de nuevo por el consejo awk :-)
Nikhil Mulley

1
¿Es posible cambiar el script perl para eliminar solo las líneas adyacentes duplicadas?
Dumbledad

2
@dumbledad: lo uniqhace todo solo
Mat

3

Como ninguna otra respuesta proporcionó soporte in situ, aquí hay una:

gawk -i inplace '!a[$0]++' file

¿Esto preserva el orden? Por cierto, esto no funcionó para mí. Mi versión es:GNU Awk 4.0.2
Leonid

1
@Leonid sí, lo hace. Imprime la primera aparición de cualquier línea única. El soporte in situ se introdujo por primera vez en la versión 4.1, que se lanzó en 2013.
Jan Chren - rindeal

3

Puede usar uniq http://www.computerhope.com/unix/uuniq.htm

uniq informa o filtra líneas repetidas en un archivo.


Al dar una respuesta, es preferible dar una explicación de POR QUÉ su respuesta es la correcta. Entonces, ¿cómo difiere esta respuesta de varias de las respuestas anteriores?
Stephen Rauch

1
Desde la página de manual de uniq: Nota: 'uniq' does not detect repeated lines unless they are adjacent. Por lo tanto, primero debe ordenarlo y perder el orden de las líneas no duplicadas.
Vindolin

2

Forros Python One:

python -c "import sys; lines = sys.stdin.readlines(); print ''.join(sorted(set(lines)))" < InputFile

esto hace que todo el archivo se deslice en la memoria y puede que no sea una buena opción para el problema del OP. Tampoco se garantiza que retenga el pedido
iruvar

Gracias por la sugerencia, acabo de aprender Python ... solo intenté esto con el propósito de aprender ... :)
Rahul Patil

Aquí hay una versión de Python 2.7 que no es de una sola línea, pero (sucintamente) devuelve líneas únicas preservando el orden sin cargar todo el archivo en la memoria o crear una sola cadena gigantesca para alimentar para imprimir
iruvar

Gracias @ 1_CR Tengo algo que aprender hoy :)OrderedDict
Rahul Patil

0

Ninguna de las respuestas aquí funcionó para mí en mi Mac, así que escribí un simple script de Python que funciona para mí. Estoy ignorando los espacios en blanco iniciales / finales y tampoco me importa el consumo de memoria.

import sys

inputfile = sys.argv[1]
outputfile = sys.argv[2]

with open(inputfile) as f:
    content = f.readlines()

content = [x.strip() for x in content]

my_list = list(set(content))

with open(outputfile, 'w') as output:
    for item in my_list:
        output.write("%s\n" % item)

Guarde lo anterior en unique.py y ejecútelo así:

python unique.py inputfile.txt outputfile.txt

-1

Con bash 4, se puede utilizar una solución de bash puro que aprovecha las matrices asociativas . Aquí hay un ejemplo

unset llist; declare -A llist;
while read -r line; do
if [[ ${llist[$line]} ]]; then
  continue
else 
  printf '%s\n' "$line"
  llist[$line]="x"
fi
done < file.txt

2
No use readbucles para procesar archivos de texto grandes. bash tiene que leer un byte por vez para evitar sobrepasar una nueva línea. Bash tampoco es muy rápido en el procesamiento de texto en general en comparación con awk. Si usa esto, read -raevitará comer barras invertidas en su entrada. Además, no olvide unset llist después del ciclo, si pone esto en una función de shell o lo usa de forma interactiva.
Peter Cordes

2
@PeterCordes, o podría haber hecho referencia a esto :-)
iruvar
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.