Come posso rimuovere la prima riga di un file di testo usando lo script bash / sed?


555

Devo rimuovere ripetutamente la prima riga da un enorme file di testo usando uno script bash.

In questo momento sto usando sed -i -e "1d" $FILE- ma ci vuole circa un minuto per fare la cancellazione.

C'è un modo più efficiente per raggiungere questo obiettivo?


cosa significa -i?
cikatomo,

4
@cikatomo: sta per modifica in linea - modifica il file con qualunque cosa tu generi.
Drewrockshard,

4
la coda è MOLTO PIÙ LENTO di sed. la coda ha bisogno di 13,5 secondi, sed ha bisogno di 0,85 secondi. Il mio file ha ~ 1 MB di righe, ~ 100 MB. MacBook Air 2013 con SSD.
jcsahnwaldt dice GoFundMonica il

Risposte:


1030

Prova la coda :

tail -n +2 "$FILE"

-n x: Basta stampare le ultime xrighe. tail -n 5ti darebbe le ultime 5 righe dell'input. Il +tipo di segno inverte l'argomento e fa tailstampare qualsiasi cosa tranne le prime x-1righe. tail -n +1stamperebbe l'intero file, tail -n +2tutto tranne la prima riga, ecc.

GNU tailè molto più veloce di sed. tailè disponibile anche su BSD e la -n +2bandiera è coerente su entrambi gli strumenti. Controlla le pagine man di FreeBSD o OS X per ulteriori informazioni.

La versione BSD può essere molto più lenta di sed, però. Mi chiedo come ci siano riusciti; taildovrebbe semplicemente leggere un file riga per riga mentre sedfa operazioni piuttosto complesse che coinvolgono l'interpretazione di uno script, l'applicazione di espressioni regolari e simili.

Nota: potresti essere tentato di usarlo

# THIS WILL GIVE YOU AN EMPTY FILE!
tail -n +2 "$FILE" > "$FILE"

ma questo ti darà un file vuoto . Il motivo è che il reindirizzamento ( >) si verifica prima tailè invocato dalla shell:

  1. Shell tronca il file $FILE
  2. Shell crea un nuovo processo per tail
  3. Shell reindirizza lo stdout del tailprocesso a$FILE
  4. tail legge dal ormai vuoto $FILE

Se si desidera rimuovere la prima riga all'interno del file, è necessario utilizzare:

tail -n +2 "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE"

Si &&assicurerà che il file non venga sovrascritto in caso di problemi.


3
Secondo questo ss64.com/bash/tail.html il buffer tipico viene impostato automaticamente a 32k quando si utilizza BSD 'tail' con l' -ropzione. Forse c'è un'impostazione del buffer da qualche parte nel sistema? O -nè un numero con segno a 32 bit?
Yzmir Ramirez,

41
@Eddie: user869097 ha detto che non funziona quando una singola linea è di almeno 15 Mb . Finché le linee sono più corte, tailfunzionerà per qualsiasi dimensione di file.
Aaron Digulla,

6
potresti spiegare questi argomenti?
Dreampuf,

17
@Dreampuf - dalla pagina man:-n N means output the last N lines, instead of the last 10; or use +N to output lines starting with the Nth
Will Sheppard,

11
Stavo per concordare con @JonaChristopherSahnwaldt - la coda è molto, molto più lenta della variante sed, di un ordine di grandezza. Lo sto testando su un file di 500.000 K di righe (non più di 50 caratteri per riga). Comunque, mi sono reso conto che stavo usando la versione di coda di FreeBSD (che viene fornita di default con OS X). Quando sono passato alla coda GNU, la chiamata in coda era 10 volte più veloce della chiamata sed (e anche della chiamata sed GNU). AaronDigulla è corretto qui, se stai usando GNU.
Dan Nguyen,

179

Puoi usare -i per aggiornare il file senza usare l'operatore '>'. Il seguente comando eliminerà la prima riga dal file e la salverà nel file.

sed -i '1d' filename

1
Ottengo l'errore:unterminated transform source string
Daniel Kobe il

10
funziona sempre e dovrebbe davvero essere la risposta migliore!
xtheking

4
Solo per ricordare, Mac richiede un suffisso da fornire quando si utilizza sed con modifiche sul posto. Quindi esegui quanto sopra con -i.bak
mjp

3
Solo una nota - per rimuovere diverse righe utilizzaresed -i '1,2d' filename
Il padrino

4
Questa versione è davvero molto più leggibile e più universale di tail -n +2. Non sono sicuro del perché non sia la risposta migliore.
Luke Davis,

74

Per coloro che sono su SunOS che non è GNU, il seguente codice aiuterà:

sed '1d' test.dat > tmp.dat 

18
Interessante demografica
capitano

17

No, è più efficiente di quello che otterrai. Potresti scrivere un programma C che potrebbe fare il lavoro un po 'più veloce (meno tempo di avvio e argomenti di elaborazione) ma probabilmente tenderà alla stessa velocità di sed man mano che i file diventano grandi (e presumo siano grandi se impiega un minuto ).

Ma la tua domanda soffre dello stesso problema di tanti altri in quanto presuppone la soluzione. Se dovessi dirci in dettaglio cosa stai cercando di fare piuttosto che come , potremmo essere in grado di suggerire un'opzione migliore.

Ad esempio, se si tratta di un file A che viene elaborato da un altro programma B, una soluzione sarebbe quella di non eliminare la prima riga, ma modificare il programma B per elaborarlo in modo diverso.

Supponiamo che tutti i programmi aggiunti a questo file A e che il programma B attualmente legga ed elabori la prima riga prima di eliminarlo.

È possibile riprogettare il programma B in modo che non provi a eliminare la prima riga ma mantiene un offset persistente (probabilmente basato su file) nel file A in modo che, alla successiva esecuzione, possa cercare tale offset, elaborare la linea lì e aggiorna l'offset.

Quindi, in un momento di quiete (mezzanotte?), Potrebbe eseguire un'elaborazione speciale del file A per eliminare tutte le linee attualmente elaborate e riportare l'offset a 0.

Sarà certamente più veloce per un programma aprire e cercare un file piuttosto che aprirlo e riscriverlo. Questa discussione presuppone che tu abbia il controllo sul programma B, ovviamente. Non so se sia così, ma potrebbero esserci altre soluzioni se fornisci ulteriori informazioni.


Penso che l'OP stia cercando di ottenere ciò che mi ha fatto trovare questa domanda. Ho 10 file CSV con 500k linee in ciascuno. Ogni file ha la stessa riga di intestazione della prima riga. Sono un gatto: inglobando questi file in un unico file e quindi importandoli in un DB che consente al DB di creare i nomi delle colonne dalla prima riga. Ovviamente non voglio che quella riga venga ripetuta nel file 2-10.
db

1
@db In tal caso, awk FNR-1 *.csvè probabilmente più veloce.
Jinawee,

10

È possibile modificare i file al suo posto: utilizzare un solo Perl -ibandiera, in questo modo:

perl -ni -e 'print unless $. == 1' filename.txt

Questo fa scomparire la prima riga, come chiedi. Perl dovrà leggere e copiare l'intero file, ma organizza il salvataggio dell'output con il nome del file originale.


10

Puoi farlo facilmente con:

cat filename | sed 1d > filename_without_first_line

dalla riga di comando; o per rimuovere definitivamente la prima riga di un file, utilizzare la modalità sul posto di sed con il -iflag:

sed -i 1d <filename>

9

Come ha detto Pax, probabilmente non otterrai più velocemente di così. Il motivo è che non ci sono quasi filesystem che supportano il troncamento dall'inizio del file, quindi questa sarà un'operazione O ( n) dove nè la dimensione del file. Quello che puoi fare molto più velocemente è sovrascrivere la prima riga con lo stesso numero di byte (forse con spazi o un commento) che potrebbe funzionare per te a seconda di esattamente cosa stai cercando di fare (che cos'è comunque?).


Ri "... quasi nessun filesystem che supporti il ​​troncamento ..." : è interessante; si prega di considerare di includere una nota tra parentesi che nomina tale file system.
agc,

1
@agc: ora irrilevante, ma il mio primo lavoro negli anni '70 è stato con Quadex, una piccola startup (ora scomparsa e non correlata alle due società che ora usano quel nome). Avevano un filesystem che permetteva di aggiungere o rimuovere all'inizio o alla fine di un file, usato principalmente per implementare l'editing in meno di 3 KB inserendo file sopra la finestra e sotto la finestra. Non aveva un nome proprio, faceva solo parte di QMOS, il sistema operativo multiutente Quadex. ("Multi" era in genere 2-3 su un LSI-11/02 con RAM inferiore a 64 KB e di solito alcuni dischi floppy da 8 "di tipo RX01 ogni
250 KB

9

L' spongeutility evita la necessità di manipolare un file temporaneo:

tail -n +2 "$FILE" | sponge "$FILE"

spongeè davvero molto più pulito e robusto della soluzione accettata ( tail -n +2 "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE")
Jealie,

1
Dovrebbe essere chiaro che 'sponge' richiede l'installazione del pacchetto 'moreutils'.
FedFranzoni,

Questa è l'unica soluzione che ha funzionato per me per modificare un file di sistema (su un'immagine docker Debian). Altre soluzioni non riuscite a causa dell'errore "Dispositivo o risorsa occupata" durante il tentativo di scrivere il file.
FedFranzoni,

Ma esegue il spongebuffer dell'intero file in memoria? Non funzionerà se sono centinaia di GB.
OrangeDog,

@OrangeDog, Fintanto che il file system può memorizzarlo, spongelo assorbirà, poiché utilizza un file / tmp come passaggio intermedio, che viene quindi utilizzato per sostituire l'originale in seguito.
agc,

8

Se si desidera modificare il file al suo posto, si può sempre utilizzare l'originale edinvece del suo s successore treaming sed:

ed "$FILE" <<<$'1d\nwq\n'

Il edcomando era l'editor di testo UNIX originale, prima ancora che ci fossero persino terminali a schermo intero, molto meno stazioni di lavoro grafiche. L' exeditore, meglio conosciuto come quello che si sta utilizzando, quando digita al prompt della nel colon vi, è un ex versione teso di ed, tanti della stessa opera comandi. Sebbene edsia pensato per essere usato in modo interattivo, può anche essere utilizzato in modalità batch inviando una serie di comandi, che è ciò che fa questa soluzione.

La sequenza <<<$'1d\nwq\n'si avvale del sostegno di Bash per stringhe here ( <<<) e citazioni POSIX ( $'... ') all'ingresso di alimentazione per il edcomando costituito da due linee: 1dche d eletes linea 1 , e poi wq, che w riti il file indietro per disco e poi q UITS la sessione di modifica.


questo è elegante. +1
Armin,

Ma devi leggere l'intero file in memoria, che non funzionerà se è centinaia di GB.
OrangeDog,

5

dovrebbe mostrare le righe tranne la prima riga:

cat textfile.txt | tail -n +2

4
- dovresti fare "tail -n +2 textfile.txt"
niglesias

5
@niglesiais Non sono d'accordo con l '"uso inutile di cat", poiché chiarisce che questa soluzione va bene per il contenuto in pipe e non solo per i file.
Titou,

5

Potrebbe usare vim per fare questo:

vim -u NONE +'1d' +'wq!' /tmp/test.txt

Questo dovrebbe essere più veloce, poiché vim non legge l'intero file durante il processo.


Potrebbe essere necessario citare +wq!se la tua shell è bash. Probabilmente non dal momento che non !è all'inizio di una parola, ma prendere l'abitudine di citare le cose è probabilmente buono tutto intorno. (E se stai andando per la super efficienza non citando inutilmente, non hai bisogno delle virgolette intorno a 1dentrambi.)
Mark Reed,

vim ha bisogno di leggere l'intero file. Infatti se il file è più grande della memoria, come richiesto in questa Q, vim legge l'intero file e lo scrive (o la maggior parte di esso) in un file temporaneo, e dopo la modifica lo riscrive (nel file permanente). Non so come pensi che possa funzionare senza questo.
dave_thompson_085

4

Che ne dici di usare csplit?

man csplit
csplit -k file 1 '{1}'

Questa sintassi sarebbe anche il lavoro, ma solo generare due file di output invece di tre: csplit file /^.*$/1. O più semplicemente: csplit file //1. O ancora più semplicemente: csplit file 2.
Marco Roy,

1

Dato che non riesco ad accelerare la cancellazione, penso che un buon approccio potrebbe essere quello di elaborare il file in batch come questo:

While file1 not empty
  file2 = head -n1000 file1
  process file2
  sed -i -e "1000d" file1
end

Lo svantaggio di questo è che se il programma viene ucciso nel mezzo (o se c'è qualche sql cattivo lì dentro - che fa morire o bloccare la parte "process"), ci saranno linee che verranno saltate o processate due volte .

(file1 contiene righe di codice sql)


Cosa contiene la prima riga? Puoi semplicemente sovrascriverlo con un commento sql come ho suggerito nel mio post?
Robert Gamble,

0

Se quello che stai cercando di fare è recuperare dopo un errore, potresti semplicemente creare un file che ha quello che hai fatto finora.

if [[ -f $tmpf ]] ; then
    rm -f $tmpf
fi
cat $srcf |
    while read line ; do
        # process line
        echo "$line" >> $tmpf
    done

0

Questa fodera farà:

echo "$(tail -n +2 "$FILE")" > "$FILE"

Funziona, poiché tailviene eseguito prima echoe quindi il file è sbloccato, quindi non è necessario un file temporaneo.


-1

Usando tail su linee N-1 e indirizzandolo in un file, seguito rimuovendo il vecchio file e rinominando il nuovo file con il vecchio nome farebbe il lavoro?

Se lo facessi in modo programmatico, leggerei il file e ricorderei l'offset del file, dopo aver letto ogni riga, in modo da poter tornare in quella posizione per leggere il file con una riga in meno.


La prima soluzione è sostanzialmente identica a quella che sta facendo Brent. Non capisco il tuo approccio programmatico, solo la prima riga deve essere eliminata, devi solo leggere e scartare la prima riga e copiare il resto in un altro file che è di nuovo uguale agli approcci sed e tail.
Robert Gamble,

La seconda soluzione implica che il file non viene ridotto ogni volta dalla prima riga. Il programma semplicemente lo elabora, come se fosse stato ridotto, ma a partire dalla riga successiva ogni volta
EvilTeach

Non capisco ancora quale sia la tua seconda soluzione.
Robert Gamble,
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.