Rimuovi in ​​modo efficiente le ultime due righe di un file di testo estremamente grande


Risposte:


31

Non ho provato questo su un file di grandi dimensioni per vedere quanto è veloce, ma dovrebbe essere abbastanza veloce.

Per utilizzare lo script per rimuovere le righe dalla fine di un file:

./shorten.py 2 large_file.txt

Cerca la fine del file, verifica che l'ultimo carattere sia una nuova riga, quindi legge ogni carattere uno alla volta andando indietro fino a quando non trova tre nuove righe e tronca il file subito dopo quel punto. La modifica è stata effettuata.

Modifica: ho aggiunto una versione di Python 2.4 in basso.

Ecco una versione per 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)

Ecco una versione di 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)

Ecco una versione di 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)

il nostro sistema esegue python 2.4 e non sono sicuro che alcuni dei nostri servizi si basino su di esso, funzionerà in questo?
Russ Bradberry,

@Russ: ho aggiunto una versione per Python 2.4.
In pausa fino a nuovo avviso.

1
assolutamente fantastico! ha funzionato come un fascino e in meno di un secondo!
Russ Bradberry,

12

puoi provare GNU head

head -n -2 file

È la soluzione migliore poiché è semplice.
xiao,

1
Questo gli mostrerà le ultime due righe del file, ma non le rimuoverà dal suo file ... non funziona nemmeno sul mio sistemahead: illegal line count -- -2
SooDesuNe

2
@SooDesuNe: No stamperà tutte le righe dall'inizio a 2 righe dalla fine, come da manuale. Tuttavia, questo dovrebbe essere reindirizzato a un file e quindi c'è il problema che questo file è gigante, quindi non è la soluzione perfetta per questo problema.
Daniel Andersson,

+1 Perché questo non viene accettato come risposta corretta? È veloce, semplice e funziona come previsto.
aefxx,

6
@PetrMarek e altri: il problema era che riguardava un file gigante . Questa soluzione richiederebbe che l'intero file venisse inviato attraverso una pipe e riscritto tutti i dati in una nuova posizione, e il punto centrale della domanda è evitarlo. È necessaria una soluzione sul posto, come quella nella risposta accettata.
Daniel Andersson,

7

Vedo che i miei sistemi Debian Squeeze / testing (ma non Lenny / stable) includono un comando "truncate" come parte del pacchetto "coreutils".

Con esso potresti semplicemente fare qualcosa del genere

truncate --size=-160 myfile

per rimuovere 160 byte dalla fine del file (ovviamente è necessario capire esattamente quanti caratteri è necessario rimuovere).


Questo sarà il percorso più veloce poiché modifica il file sul posto e quindi non richiede né la copia né l'analisi del file. Tuttavia, dovrai comunque controllare quanti byte rimuovere ... I / suppongo / che un semplice ddscript lo farà (devi specificare l'offset di input per ottenere l'ultimo kilobyte e quindi usare tail -2 | LANG= wc -c, o sth).
liori,

Sto usando CentOS, quindi no non ho troncato. Tuttavia, questo è esattamente quello che sto cercando.
Russ Bradberry,

tailè efficace anche per file di grandi dimensioni - può essere utilizzato tail | wc -cper calcolare il numero di byte da tagliare.
krlmlr,

6

Il problema con sed è che si tratta di un editor di stream: elaborerà l'intero file anche se si desidera apportare modifiche solo alla fine. Quindi, indipendentemente da cosa, stai creando un nuovo file da 400 GB, riga per riga. Qualsiasi editor che opera sull'intero file avrà probabilmente questo problema.

Se conosci il numero di righe, puoi usarlo head, ma di nuovo questo crea un nuovo file invece di alterare quello esistente in atto. Potresti ottenere guadagni di velocità dalla semplicità dell'azione, immagino.

Si potrebbe avere più fortuna usando splitper rompere il file in pezzi più piccoli, modificando l'ultimo, e quindi utilizzando catper combinare di nuovo, ma non sono sicuro se sarà meglio. Vorrei usare i conteggi dei byte piuttosto che le linee, altrimenti probabilmente non sarà affatto più veloce - stai ancora creando un nuovo file da 400 GB.


2

Prova VIM ... Non sono sicuro che farà il trucco o meno, dato che non l'ho mai usato su un file così grande, ma in passato lo ho usato su file più piccoli e più grandi.


Credo che vim carichi solo ciò che circonda immediatamente il buffer durante la modifica , tuttavia non ho idea di come si salva.
Phoshi,

vim si blocca mentre tenta di caricare il file
Russ Bradberry

Beh, se si blocca, ah aspetta. Inizia a caricarlo, vai al lavoro, torna a casa, vedi se è fatto.
leeand00,


1

Che tipo di file e in quale formato? Potrebbe essere più facile usare qualcosa come Perl a seconda del tipo di file: testo, grafica, binario? Come viene formattato - CSV, TSV ...


è un testo delimitato da pipe, tuttavia le ultime 2 righe sono una colonna ciascuna che interromperà la mia importazione, quindi ho bisogno che
Russ Bradberry,

sta riparando qualunque cosa l '"importazione" per affrontare questo caso sia un'opzione?
giorno

no l'importazione è il "caricamento dei dati" di infobright
Russ Bradberry,

1

Se conosci la dimensione del file al byte (ad esempio 400000000160) e sai che devi rimuovere esattamente 160 caratteri per eliminare le ultime due righe, quindi qualcosa di simile

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

dovrebbe fare il trucco. Sono passati secoli da quando ho usato dd in rabbia; Mi sembra di ricordare che le cose vanno più veloci se usi blocchi di dimensioni maggiori, ma se puoi farlo dipende dal fatto che le linee che vuoi eliminare siano in un bel multiplo.

dd ha alcune altre opzioni per riempire i record di testo a una dimensione fissa che potrebbe essere utile come passaggio preliminare.


ho provato questo, ma stava andando alla stessa velocità di sed. Aveva scritto circa 200 MB in 10 minuti, a questo ritmo occorrerebbero letteralmente centinaia di ore per il completamento.
Russ Bradberry,

1

Se il comando "tronca" non è disponibile sul tuo sistema (vedi la mia altra risposta), guarda "man 2 truncate" per la chiamata di sistema per troncare un file ad una lunghezza specificata.

Ovviamente devi sapere a quanti caratteri devi troncare il file (dimensione meno la lunghezza del problema due righe; non dimenticare di contare i caratteri cr / lf).

E fai un backup del file prima di provare questo!


1

Se preferisci soluzioni in stile unix, puoi avere il salvataggio e il troncamento interattivo delle linee usando tre righe di codice (testato su Mac e Linux).

troncamento della linea piccolo + sicuro in stile unix (richiede conferma):

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` )"

Questa soluzione si basa su alcuni strumenti unix comuni, ma utilizza ancora perl -e "truncate(file,length)"come sostituto più vicino per truncate(1), che non è disponibile su tutti i sistemi.

È inoltre possibile utilizzare il seguente programma shell riutilizzabile completo, che fornisce informazioni sull'utilizzo e funzionalità di conferma del troncamento, analisi delle opzioni e gestione degli errori.

script completo di troncamento di riga :

#!/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

Ecco un esempio di utilizzo:

$ 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" << QUI
$
d
d
w
QUI

vengono apportate modifiche. Questo è più semplice ed efficiente dello script Python.


Sul mio sistema, usando un file di testo composto da un milione di righe e oltre 57 MB, l'esecuzione di ed100 volte più lunga del mio script Python. Posso solo immaginare quanto maggiore sarebbe la differenza per il file del PO che è 7000 volte più grande.
In pausa fino a nuovo avviso.

0

Modificata la risposta accettata per risolvere un problema simile. Potrebbe essere modificato un po 'per rimuovere n linee.

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

E il test corrispondente:

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

Puoi usare Vim in modalità Ex:

ex -sc '-,d|x' file
  1. -, seleziona le ultime 2 righe

  2. d Elimina

  3. x salva e chiudi

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.