Elimine eficientemente las dos últimas líneas de un archivo de texto extremadamente grande


Respuestas:


31

No he probado esto en un archivo grande para ver qué tan rápido es, pero debería ser bastante rápido.

Para usar el script para eliminar líneas del final de un archivo:

./shorten.py 2 large_file.txt

Busca hasta el final del archivo, verifica para asegurarse de que el último carácter sea una nueva línea, luego lee cada carácter uno a la vez retrocediendo hasta encontrar tres nuevas líneas y trunca el archivo justo después de ese punto. El cambio se realiza en su lugar.

Editar: he agregado una versión de Python 2.4 en la parte inferior.

Aquí hay una versión para Python 2.5 / 2.6:

#!/usr/bin/env python2.5
from __future__ import with_statement
# also tested with Python 2.6

import os, sys

if len(sys.argv) != 3:
    print sys.argv[0] + ": Invalid number of arguments."
    print "Usage: " + sys.argv[0] + " linecount filename"
    print "to remove linecount lines from the end of the file"
    exit(2)

number = int(sys.argv[1])
file = sys.argv[2]
count = 0

with open(file,'r+b') as f:
    f.seek(0, os.SEEK_END)
    end = f.tell()
    while f.tell() > 0:
        f.seek(-1, os.SEEK_CUR)
        char = f.read(1)
        if char != '\n' and f.tell() == end:
            print "No change: file does not end with a newline"
            exit(1)
        if char == '\n':
            count += 1
        if count == number + 1:
            f.truncate()
            print "Removed " + str(number) + " lines from end of file"
            exit(0)
        f.seek(-1, os.SEEK_CUR)

if count < number + 1:
    print "No change: requested removal would leave empty file"
    exit(3)

Aquí hay una versión de Python 3:

#!/usr/bin/env python3.0

import os, sys

if len(sys.argv) != 3:
    print(sys.argv[0] + ": Invalid number of arguments.")
    print ("Usage: " + sys.argv[0] + " linecount filename")
    print ("to remove linecount lines from the end of the file")
    exit(2)

number = int(sys.argv[1])
file = sys.argv[2]
count = 0

with open(file,'r+b', buffering=0) as f:
    f.seek(0, os.SEEK_END)
    end = f.tell()
    while f.tell() > 0:
        f.seek(-1, os.SEEK_CUR)
        print(f.tell())
        char = f.read(1)
        if char != b'\n' and f.tell() == end:
            print ("No change: file does not end with a newline")
            exit(1)
        if char == b'\n':
            count += 1
        if count == number + 1:
            f.truncate()
            print ("Removed " + str(number) + " lines from end of file")
            exit(0)
        f.seek(-1, os.SEEK_CUR)

if count < number + 1:
    print("No change: requested removal would leave empty file")
    exit(3)

Aquí hay una versión de Python 2.4:

#!/usr/bin/env python2.4

import sys

if len(sys.argv) != 3:
    print sys.argv[0] + ": Invalid number of arguments."
    print "Usage: " + sys.argv[0] + " linecount filename"
    print "to remove linecount lines from the end of the file"
    sys.exit(2)

number = int(sys.argv[1])
file = sys.argv[2]
count = 0
SEEK_CUR = 1
SEEK_END = 2

f = open(file,'r+b')
f.seek(0, SEEK_END)
end = f.tell()

while f.tell() > 0:
    f.seek(-1, SEEK_CUR)
    char = f.read(1)
    if char != '\n' and f.tell() == end:
        print "No change: file does not end with a newline"
        f.close()
        sys.exit(1)
    if char == '\n':
        count += 1
    if count == number + 1:
        f.truncate()
        print "Removed " + str(number) + " lines from end of file"
        f.close()
        sys.exit(0)
    f.seek(-1, SEEK_CUR)

if count < number + 1:
    print "No change: requested removal would leave empty file"
    f.close()
    sys.exit(3)

nuestro sistema ejecuta python 2.4, y no estoy seguro de si alguno de nuestros servicios depende de él, ¿funcionará esto?
Russ Bradberry el

@Russ: agregué una versión para Python 2.4.
Pausado hasta nuevo aviso.

1
¡absolutamente increible! funcionó a las mil maravillas y en menos de un segundo!
Russ Bradberry el

12

puedes probar GNU head

head -n -2 file

Es la mejor solución ya que es simple.
xiao

1
Esto le mostrará las dos últimas líneas del archivo, pero no las eliminará de su archivo ... ni siquiera funciona en mi sistemahead: illegal line count -- -2
SooDesuNe

2
@SooDesuNe: No, imprimirá todas las líneas desde el principio hasta 2 líneas desde el final, según el manual. Sin embargo, esto debería ser redirigido a un archivo, y luego está el problema de que este archivo sea gigante, por lo que no es la solución perfecta para este problema.
Daniel Andersson

+1 ¿Por qué no se acepta esto como la respuesta correcta? Es rápido, simple y funciona como se espera.
aefxx

66
@PetrMarek y otros: el problema era que se refería a un archivo gigante . Esta solución requeriría que todo el archivo se alimente a través de una tubería y reescriba todos los datos en una nueva ubicación, y el objetivo de la pregunta es evitar eso. Se necesita una solución in situ, como la de la respuesta aceptada.
Daniel Andersson

7

Veo que mis sistemas Debian Squeeze / testing (pero no Lenny / stable) incluyen un comando "truncar" como parte del paquete "coreutils".

Con él simplemente podrías hacer algo como

truncate --size=-160 myfile

para eliminar 160 bytes del final del archivo (obviamente, necesita averiguar exactamente cuántos caracteres necesita eliminar).


Esta será la ruta más rápida ya que modifica el archivo en el lugar y, por lo tanto, no requiere ni copiar ni analizar el archivo. Sin embargo, aún deberá verificar cuántos bytes eliminar ... Supongo / que un ddscript simple lo hará (debe especificar el desplazamiento de entrada para obtener el último kilobyte y luego usar tail -2 | LANG= wc -c, o algo así).
liori

Estoy usando CentOS, así que no, no tengo truncar. Sin embargo, esto es exactamente lo que estoy buscando.
Russ Bradberry el

tailtambién es eficiente para archivos grandes; puede usarse tail | wc -cpara calcular el número de bytes que se van a recortar.
krlmlr

6

El problema con sed es que es un editor de flujo: procesará todo el archivo incluso si solo desea realizar modificaciones cerca del final. Así que no importa qué, está creando un nuevo archivo de 400 GB, línea por línea. Cualquier editor que opere en todo el archivo probablemente tendrá este problema.

Si conoce el número de líneas, puede usar head, pero nuevamente esto crea un nuevo archivo en lugar de alterar el existente en su lugar. Supongo que puede obtener ganancias de velocidad por la simplicidad de la acción.

Es posible que tenga más suerte usando splitpara dividir el archivo en partes más pequeñas, editando el último y luego usando catpara combinarlas nuevamente, pero no estoy seguro de si será mejor. Usaría recuentos de bytes en lugar de líneas, de lo contrario, probablemente no será más rápido en absoluto: todavía va a crear un nuevo archivo de 400 GB.


2

Pruebe VIM ... No estoy seguro de si funcionará o no, ya que nunca lo he usado en un archivo tan grande, pero lo he usado en archivos más pequeños y más grandes en el pasado, inténtelo.


Creo que vim solo carga lo que está inmediatamente alrededor del búfer cuando edita , sin embargo, no tengo idea de cómo se guarda.
Phoshi

vim se cuelga mientras intenta cargar el archivo
Russ Bradberry

Bueno, si se cuelga, ah espera. Comience a cargar, vaya a trabajar, regrese a casa, vea si está listo.
leeand00


1

¿Qué tipo de archivo y en qué formato? Puede ser más fácil usar algo como Perl dependiendo de qué tipo de archivo sea: ¿texto, gráficos, binario? Cómo se formatea: CSV, TSV ...


se le da formato de texto tubería delimeted, sin embargo las últimas 2 líneas son cada una columna que romperá mi importación, así que necesito los quitaron
Russ Bradberry

¿está solucionando lo que hace la "importación" para tratar este caso una opción?
2010

no, la importación es el "archivo de datos de carga" de infobright
Russ Bradberry

1

Si conoce el tamaño del archivo hasta el byte (por ejemplo, 400000000160) y sabe que necesita eliminar exactamente 160 caracteres para quitar las dos últimas líneas, entonces algo como

dd if=originalfile of=truncatedfile ibs=1 count=400000000000

debería hacer el truco. Sin embargo, han pasado años desde que usé dd con ira; Parece que recuerdo que las cosas van más rápido si usas un tamaño de bloque más grande, pero si puedes hacerlo depende de si las líneas que quieres soltar son un buen múltiplo.

dd tiene algunas otras opciones para rellenar registros de texto a un tamaño fijo que podría ser útil como pase preliminar.


Intenté esto, pero iba a la misma velocidad que sed. Había escrito aproximadamente 200 MB en 10 minutos, a este ritmo, literalmente, tardaría cientos de horas en completarse.
Russ Bradberry el

1

Si el comando "truncar" no está disponible en su sistema (vea mi otra respuesta), mire el "truncamiento de man 2" para la llamada del sistema para truncar un archivo a una longitud especificada.

Obviamente, necesita saber cuántos caracteres necesita para truncar el archivo (tamaño menos la longitud del problema dos líneas; no olvide contar los caracteres cr / lf).

¡Y haga una copia de seguridad del archivo antes de intentar esto!


1

Si prefiere soluciones de estilo unix, puede guardar y truncar líneas interactivas utilizando tres líneas de código (Probado en Mac y Linux).

truncamiento de línea de estilo Unix pequeño + seguro (pide confirmación):

n=2; file=test.csv; tail -n $n $file &&
read -p "truncate? (y/N)" -n1 key && [ "$key" == "y" ] &&
perl -e "truncate('$file', `wc -c <$file` - `tail -n $n $file | wc -c` )"

Esta solución se basa en algunas herramientas comunes de Unix, pero aún se utiliza perl -e "truncate(file,length)"como el reemplazo más cercano para truncate(1), que no está disponible en todos los sistemas.

También puede usar el siguiente programa integral de shell reutilizable, que proporciona información de uso y cuenta con confirmación de truncamiento, análisis de opciones y manejo de errores.

secuencia de comandos de truncamiento de línea integral :

#!/usr/bin/env bash

usage(){
cat <<-EOF
  Usage:   $0 [-n NUM] [-h] FILE
  Options:
  -n NUM      number of lines to remove (default:1) from end of FILE
  -h          show this help
EOF
exit 1
}

num=1

for opt in $*; do case $opt in
  -n) num=$2;                 shift;;
  -h) usage;                  break;;
  *)  [ -f "$1" ] && file=$1; shift;;
esac done

[ -f "$file" ] || usage

bytes=`wc -c <$file`
size=`tail -n $num $file | wc -c`

echo "using perl 'truncate' to remove last $size of $bytes bytes:"
tail -n $num $file
read -p "truncate these lines? (y/N)" -n1 key && [ "$key" == "y" ] &&
perl -e "truncate('$file', $bytes - $size )"; echo ""
echo "new tail is:"; tail $file

Aquí hay un ejemplo de uso:

$ cat data/test.csv
1 nice data
2 cool data
3 just data

GARBAGE to be removed (incl. empty lines above and below)

$ ./rmtail.sh -n 3 data/test.csv
using perl 'truncate' to remove last 60 of 96 bytes:

GARBAGE to be removed (incl. empty lines above and below)

truncate these lines? (y/N)y
new tail is:
1 nice data
2 cool data
3 just data
$ cat data/test.csv
1 nice data
2 cool data
3 just data

0
#! / bin / sh

ed "$ 1" << AQUÍ
PS
re
re
w
AQUÍ

Los cambios se realizan en su lugar. Esto es más simple y más eficiente que el script python.


En mi sistema, usar un archivo de texto que consta de un millón de líneas y más de 57 MB, edtardó 100 veces más en ejecutarse que mi script Python. Solo puedo imaginar cuánto más sería la diferencia para el archivo del OP, que es 7000 veces más grande.
Pausado hasta nuevo aviso.

0

Modificó la respuesta aceptada para resolver un problema similar. Podría modificarse un poco para eliminar n líneas.

import os

def clean_up_last_line(file_path):
    """
    cleanup last incomplete line from a file
    helps with an unclean shutdown of a program that appends to a file
    if \n is not the last character, remove the line
    """
    with open(file_path, 'r+b') as f:
        f.seek(0, os.SEEK_END)

        while f.tell() > 0: ## current position is greater than zero
            f.seek(-1, os.SEEK_CUR)

            if f.read(1) == '\n':
                f.truncate()
                break

            f.seek(-1, os.SEEK_CUR) ## don't quite understand why this has to be called again, but it doesn't work without it

Y la prueba correspondiente:

import unittest

class CommonUtilsTest(unittest.TestCase):

    def test_clean_up_last_line(self):
        """
        remove the last incomplete line from a huge file
        a line is incomplete if it does not end with a line feed
        """
        file_path = '/tmp/test_remove_last_line.txt'

        def compare_output(file_path, file_data, expected_output):
            """
            run the same test on each input output pair
            """
            with open(file_path, 'w') as f:
                f.write(file_data)

            utils.clean_up_last_line(file_path)

            with open(file_path, 'r') as f:
                file_data = f.read()
                self.assertTrue(file_data == expected_output, file_data)        

        ## test a multiline file
        file_data = """1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b
1362358458954466,2013-03-03 16:54:18,34.5,3.0,b
1362358630923094,2013-03-03 16:57:10,34.5,50.0,b
136235"""

        expected_output = """1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b
1362358458954466,2013-03-03 16:54:18,34.5,3.0,b
1362358630923094,2013-03-03 16:57:10,34.5,50.0,b
"""        
        compare_output(file_path, file_data, expected_output)

        ## test a file with no line break
        file_data = u"""1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b"""
        expected_output = "1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b"
        compare_output(file_path, file_data, expected_output)

        ## test a file a leading line break
        file_data = u"""\n1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b"""
        expected_output = "\n"
        compare_output(file_path, file_data, expected_output)

        ## test a file with one line break
        file_data = u"""1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b\n""" 
        expected_output = """1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b\n""" 
        compare_output(file_path, file_data, expected_output)

        os.remove(file_path)


if __name__ == '__main__':
    unittest.main()

0

Puede usar Vim en modo Ex:

ex -sc '-,d|x' file
  1. -, seleccione las últimas 2 líneas

  2. d borrar

  3. x guardar y cerrar

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.