Strumento Bash per ottenere l’ennesima riga da un file

C’è un modo “canonico” per farlo? Ho usato head -n | tail -1 head -n | tail -1 che fa il trucco, ma mi sono chiesto se c’è uno strumento Bash che estrae in modo specifico una linea (o un intervallo di linee) da un file.

Per “canonico” intendo un programma la cui funzione principale è quella di farlo.

head e il tubo con la tail saranno lenti per un file enorme. Suggerirei sed come questo:

 sed 'NUMq;d' file 

Dove NUM è il numero della linea che si desidera stampare; quindi, ad esempio, sed '10q;d' file per stampare la decima riga di file .

Spiegazione:

NUMq si NUMq immediatamente quando il numero di riga è NUM .

d cancellerà la linea invece di stamparla; questo è inibito sull’ultima riga perché q fa saltare il resto dello script quando si esce.

Se hai NUM in una variabile, dovrai utilizzare le virgolette anziché singole:

 sed "${NUM}q;d" file 
 sed -n '2p' < file.txt 

stamperà 2a riga

 sed -n '2011p' < file.txt 

Linea 2011th

 sed -n '10,33p' < file.txt 

linea 10 fino alla linea 33

 sed -n '1p;3p' < file.txt 

1a e 3a linea

e così via...

Per aggiungere linee con sed, puoi controllare questo:

sed: inserire una linea in una determinata posizione

Ho una situazione unica in cui posso confrontare le soluzioni proposte in questa pagina, quindi scrivo questa risposta come un consolidamento delle soluzioni proposte con tempi di esecuzione inclusi per ciascuna.

Impostare

Ho un file di dati di testo ASCII di 3.261 gigabyte con una coppia chiave-valore per riga. Il file contiene 3.339.550.320 righe in totale e non riesce ad aprirsi in nessun editor che ho provato, incluso il mio go-to Vim. Ho bisogno di suddividere questo file per indagare su alcuni dei valori che ho scoperto che iniziano solo intorno a ~ 500.000.000 di righe.

Perché il file ha così tante righe:

  • Ho bisogno di estrarre solo un sottoinsieme delle righe per fare qualcosa di utile con i dati.
  • Leggere tutte le righe che portano ai valori che mi stanno a cuore richiederà molto tempo.
  • Se la soluzione legge oltre le righe a cui tengo e continua a leggere il resto del file, perderà tempo a leggere quasi 3 miliardi di righe irrilevanti e impiegherà 6 volte più del necessario.

Il mio caso migliore è una soluzione che estrae solo una singola riga dal file senza leggere alcuna delle altre righe nel file, ma non riesco a pensare a come realizzerei questa operazione in Bash.

Ai fini della mia sanità mentale, non cercherò di leggere le 500.000.000 linee complete di cui avrei bisogno per il mio problema. Invece cercherò di estrarre la riga 50.000.000 su 3.339.550.320 (il che significa che la lettura dell’intero file richiederà 60 volte più del necessario).

Userò il time integrato per valutare ogni comando.

Baseline

Per prima cosa vediamo come la soluzione di tail cavallo:

 $ time head -50000000 myfile.ascii | tail -1 pgm_icnt = 0 real 1m15.321s 

La linea di base per la riga 50 milioni è 00: 01: 15.321, se fossi andato dritto per fila 500 milioni sarebbe probabilmente ~ 12,5 minuti.

tagliare

Sono dubbioso su questo, ma ne vale la pena:

 $ time cut -f50000000 -d$'\n' myfile.ascii pgm_icnt = 0 real 5m12.156s 

Questo ha richiesto di eseguire 00: 05: 12.156, che è molto più lento della linea di base! Non sono sicuro che abbia letto l’intero file o solo fino alla linea 50 milioni prima di fermarsi, ma a prescindere da ciò non sembra una soluzione praticabile al problema.

AWK

Ho solo eseguito la soluzione con l’ exit perché non avrei aspettato l’esecuzione del file completo:

 $ time awk 'NR == 50000000 {print; exit}' myfile.ascii pgm_icnt = 0 real 1m16.583s 

Questo codice è stato eseguito in 00: 01: 16.583, che è solo ~ 1 secondo più lento, ma non rappresenta un miglioramento sulla linea di base. A questo ritmo, se il comando di uscita fosse stato escluso, probabilmente ci sarebbero voluti circa ~ 76 minuti per leggere l’intero file!

Perl

Ho eseguito anche la soluzione Perl esistente:

 $ time perl -wnl -e '$.== 50000000 && print && exit;' myfile.ascii pgm_icnt = 0 real 1m13.146s 

Questo codice è stato eseguito in 00: 01: 13.146, che è ~ 2 secondi più veloce della linea di base. Se lo eseguissi per un totale di 500.000.000 probabilmente ci vorranno ~ 12 minuti.

sed

La migliore risposta alla lavagna, ecco il mio risultato:

 $ time sed "50000000q;d" myfile.ascii pgm_icnt = 0 real 1m12.705s 

Questo codice ha funzionato in 00: 01: 12.705, che è 3 secondi più veloce della linea di base e ~ 0.4 secondi più veloce di Perl. Se lo avessi eseguito su tutte le 500.000.000 righe probabilmente ci sarebbero voluti ~ 12 minuti.

mapfile

Ho bash 3.1 e quindi non posso testare la soluzione di mapfile.

Conclusione

Sembra che, per la maggior parte, sia difficile migliorare la soluzione della tail . Nel migliore dei casi la soluzione sed fornisce un aumento di efficienza del ~ 3%.

(percentuali calcolate con la formula % = (runtime/baseline - 1) * 100 )

Fila 50.000.000

  1. 00: 01: 12.705 (-00: 00: 02.616 = -3.47%) sed
  2. 00: 01: 13.146 (-00: 00: 02.175 = -2.89%) perl
  3. 00: 01: 15.321 (+00: 00: 00.000 = + 0.00%) head|tail
  4. 00: 01: 16.583 (+00: 00: 01.262 = + 1.68%) awk
  5. 00: 05: 12,156 (+00: 03: 56,835 = + 314,43%) cut

Fila 500.000.000

  1. 00: 12: 07.050 (-00: 00: 26.160) sed
  2. 00: 12: 11.460 (-00: 00: 21.750) perl
  3. 00: 12: 33.210 (+00: 00: 00.000) head|tail
  4. 00: 12: 45.830 (+00: 00: 12.620) awk
  5. 00: 52: 01.560 (+00: 40: 31.650) cut

Riga 3,338,559,320

  1. 01: 20: 54.599 (-00: 03: 05.327) sed
  2. 01: 21: 24.045 (-00: 02: 25.227) perl
  3. 01: 23: 49.273 (+00: 00: 00.000) head|tail
  4. 01: 25: 13.548 (+00: 02: 35.735) awk
  5. 05: 47: 23.026 (+04: 24: 26.246) cut

Con awk è piuttosto veloce:

 awk 'NR == num_line' file 

Quando questo è vero, viene eseguito il comportamento predefinito di awk : {print $0} .


Versioni alternative

Se il tuo file sembra essere enorme, è meglio exit dopo aver letto la riga richiesta. In questo modo risparmierai il tempo della CPU.

 awk 'NR == num_line {print; exit}' file 

Se vuoi dare il numero di linea da una variabile bash puoi usare:

 awk 'NR == n' n=$num file awk -vn=$num 'NR == n' file # equivalent 

Wow, tutte le possibilità!

Prova questo:

 sed -n "${lineNum}p" $file 

o uno di questi dipende dalla tua versione di Awk:

 awk -vlineNum=$lineNum 'NR == lineNum {print $0}' $file awk -v lineNum=4 '{if (NR == lineNum) {print $0}}' $file awk '{if (NR == lineNum) {print $0}}' lineNum=$lineNum $file 

( Potrebbe essere necessario provare il comando nawk o gawk ).

C’è uno strumento che stampa solo quella particolare linea? Non uno degli strumenti standard. Tuttavia, sed è probabilmente il più vicino e il più semplice da usare.

 # print line number 52 sed '52!d' file 

Utili script one-line per sed

Questa domanda viene taggata come Bash, ecco come funziona Bash (≥4): usa mapfile con l’ mapfile -s (salta) e -n (conta).

Se è necessario ottenere la quarantaduesima riga di un file :

 mapfile -s 41 -n 1 ary < file 

A questo punto, avrai una matrice in cui i campi contenenti le righe di file (inclusa la nuova riga finale), dove abbiamo saltato le prime 41 righe ( -s 41 ), e si sono fermate dopo aver letto una riga ( -n 1 ). Quindi questa è davvero la quarantaduesima linea. Per stamparlo:

 printf '%s' "${ary[0]}" 

Se hai bisogno di un intervallo di linee, pronuncia l'intervallo 42-666 (incluso), e di 'che non vuoi fare il calcolo tu stesso e stampale su stdout:

 mapfile -s $((42-1)) -n $((666-42+1)) ary < file printf '%s' "${ary[@]}" 

Se è necessario elaborare anche queste righe, non è molto comodo memorizzare la nuova riga finale. In questo caso usa l'opzione -t (assetto):

 mapfile -t -s $((42-1)) -n $((666-42+1)) ary < file # do stuff printf '%s\n' "${ary[@]}" 

Puoi avere una funzione che fa per te:

 print_file_range() { # $1-$2 is the range of file $3 to be printed to stdout local ary mapfile -s $(($1-1)) -n $(($2-$1+1)) ary < "$3" printf '%s' "${ary[@]}" } 

Nessun comando esterno, solo builtin di Bash!

Puoi anche usare la stampa sed e uscire:

 sed -n '10{p;q;}' file # print line 10 

Secondo i miei test, in termini di prestazioni e leggibilità la mia raccomandazione è:

tail -n+N | head -1

N è il numero di riga che desideri. Ad esempio, tail -n+7 input.txt | head -1 tail -n+7 input.txt | head -1 stamperà la settima riga del file.

tail -n+N stamperà tutto a partire dalla riga N , e head -1 lo farà fermare dopo una riga.


La head -N | tail -1 alternativa head -N | tail -1 head -N | tail -1 è forse leggermente più leggibile. Ad esempio, questo stamperà la settima riga:

head -7 input.txt | tail -1

Quando si tratta di prestazioni, non c’è molta differenza per le taglie più piccole, ma sarà sovraperformato dalla tail | head tail | head (dall’alto) quando i file diventano enormi.

sed 'NUMq;d' top-votato sed 'NUMq;d' è interessante da sapere, ma direi che sarà compreso da un minor numero di persone fuori dagli schemi rispetto alla soluzione testa / coda ed è anche più lento della coda / testa.

Nei miei test, entrambe le versioni di tails / heads hanno sovraperformato sed 'NUMq;d' consistentemente. Questo è in linea con gli altri benchmark che sono stati pubblicati. È difficile trovare un caso in cui le code / teste fossero davvero pessime. Inoltre, non è sorprendente, poiché si tratta di operazioni che ci si aspetterebbe di essere fortemente ottimizzate in un moderno sistema Unix.

Per avere un’idea delle differenze di prestazioni, queste sono le cifre che ottengo per un file enorme (9.3G):

  • tail -n+N | head -1 tail -n+N | head -1 : 3.7 sec
  • head -N | tail -1 head -N | tail -1 : 4.6 sec
  • sed Nq;d : 18,8 sec

I risultati possono essere diversi, ma la head | tail della prestazione head | tail head | tail e tail | head tail | head è, in generale, comparabile per input più piccoli, e sed è sempre più lento di un fattore significativo (circa 5x circa).

Per riprodurre il mio benchmark, puoi provare quanto segue, ma ti avverto che creerà un file 9.3G nella directory di lavoro corrente:

 #!/bin/bash readonly file=tmp-input.txt readonly size=1000000000 readonly pos=500000000 readonly retries=3 seq 1 $size > $file echo "*** head -N | tail -1 ***" for i in $(seq 1 $retries) ; do time head "-$pos" $file | tail -1 done echo "-------------------------" echo echo "*** tail -n+N | head -1 ***" echo seq 1 $size > $file ls -alhg $file for i in $(seq 1 $retries) ; do time tail -n+$pos $file | head -1 done echo "-------------------------" echo echo "*** sed Nq;d ***" echo seq 1 $size > $file ls -alhg $file for i in $(seq 1 $retries) ; do time sed $pos'q;d' $file done /bin/rm $file 

Ecco l’output di una corsa sulla mia macchina (ThinkPad X1 Carbon con un SSD e 16G di memoria). Presumo che nella corsa finale tutto verrà dalla cache, non dal disco:

 *** head -N | tail -1 *** 500000000 real 0m9,800s user 0m7,328s sys 0m4,081s 500000000 real 0m4,231s user 0m5,415s sys 0m2,789s 500000000 real 0m4,636s user 0m5,935s sys 0m2,684s ------------------------- *** tail -n+N | head -1 *** -rw-r--r-- 1 phil 9,3G Jan 19 19:49 tmp-input.txt 500000000 real 0m6,452s user 0m3,367s sys 0m1,498s 500000000 real 0m3,890s user 0m2,921s sys 0m0,952s 500000000 real 0m3,763s user 0m3,004s sys 0m0,760s ------------------------- *** sed Nq;d *** -rw-r--r-- 1 phil 9,3G Jan 19 19:50 tmp-input.txt 500000000 real 0m23,675s user 0m21,557s sys 0m1,523s 500000000 real 0m20,328s user 0m18,971s sys 0m1,308s 500000000 real 0m19,835s user 0m18,830s sys 0m1,004s 

Puoi anche usare Perl per questo:

 perl -wnl -e '$.== NUM && print && exit;' some.file 

La soluzione più veloce per i file di grandi dimensioni è sempre tail | head, a condizione che le due distanze:

  • dall’inizio del file alla riga di partenza. Lo chiamiamo S
  • la distanza dall’ultima riga alla fine del file. Che si E

sono conosciuti. Quindi, potremmo usare questo:

 mycount="$E"; (( E > S )) && mycount="+$S" howmany="$(( endline - startline + 1 ))" tail -n "$mycount"| head -n "$howmany" 

quant’è solo il numero di linee richieste.

Qualche altro dettaglio in https://unix.stackexchange.com/a/216614/79743

Come seguito alla risposta di benchmark molto utile di CaffeineConnoisseur … Ero curioso di sapere quanto velocemente il metodo ‘mapfile’ fosse paragonato ad altri (come quello non testato), quindi ho provato un confronto veloce e sporco di me stesso come Ho bash 4 a portata di mano. Ho fatto un test del metodo “tail | head” (piuttosto che head | tail) menzionato in uno dei commenti sulla risposta principale mentre ci stavo lavorando, mentre la gente cantava le sue lodi. Non ho nulla di simile alla dimensione del file di prova utilizzato; il meglio che ho potuto trovare in breve tempo era un file genealogico di 14 milioni (linee lunghe separate da spazi bianchi, poco meno di 12000 righe).

Versione breve: mapfile appare più veloce del metodo di taglio, ma più lento di qualsiasi altra cosa, quindi lo definirei un vero disastro. coda | head, OTOH, sembra che potrebbe essere il più veloce, anche se con un file di queste dimensioni la differenza non è poi così sostanziale rispetto a sed.

 $ time head -11000 [filename] | tail -1 [output redacted] real 0m0.117s $ time cut -f11000 -d$'\n' [filename] [output redacted] real 0m1.081s $ time awk 'NR == 11000 {print; exit}' [filename] [output redacted] real 0m0.058s $ time perl -wnl -e '$.== 11000 && print && exit;' [filename] [output redacted] real 0m0.085s $ time sed "11000q;d" [filename] [output redacted] real 0m0.031s $ time (mapfile -s 11000 -n 1 ary < [filename]; echo ${ary[0]}) [output redacted] real 0m0.309s $ time tail -n+11000 [filename] | head -n1 [output redacted] real 0m0.028s 

Spero che questo ti aiuti!

Se hai più linee delimitate da \ n (normalmente una nuova riga). Puoi anche usare ‘cut’:

 echo "$data" | cut -f2 -d$'\n' 

Otterrai la seconda linea dal file. -f3 ti dà la 3a riga.

Tutte le risposte di cui sopra rispondono direttamente alla domanda. Ma ecco una soluzione meno diretta ma un’idea potenzialmente più importante, per provocare pensieri.

Poiché le lunghezze delle righe sono arbitrarie, è necessario leggere tutti i byte del file prima dell’ennesima riga. Se hai un file enorme o hai bisogno di ripetere questa attività molte volte e questo processo richiede molto tempo, allora dovresti seriamente pensare se dovresti archiviare i tuoi dati in un modo diverso in primo luogo.

La vera soluzione è avere un indice, ad esempio all’inizio del file, che indica le posizioni in cui iniziano le linee. È ansible utilizzare un formato di database o semplicemente aggiungere una tabella all’inizio del file. In alternativa, crea un file di indice separato per accompagnare il tuo file di testo di grandi dimensioni.

ad esempio potresti creare un elenco di posizioni di carattere per le nuove linee:

 awk 'BEGIN{c=0;print(c)}{c+=length()+1;print(c+1)}' file.txt > file.idx 

quindi leggi con la tail , che in realtà seek s direttamente nel punto appropriato nel file!

ad es. per ottenere la linea 1000:

 tail -c +$(awk 'NR=1000' file.idx) file.txt | head -1 
  • Questo potrebbe non funzionare con caratteri a 2 byte / multibyte, poiché awk è “character-aware” ma tail non lo è.
  • Non ho provato questo contro un file di grandi dimensioni.
  • Vedi anche questa risposta .
  • In alternativa, dividi il tuo file in file più piccoli!

Uno dei modi possibili:

 sed -n 'NUM{p;q}' 

Notare che senza il comando q , se il file è grande, sed continua a funzionare, il che rallenta il calcolo.

Molte buone risposte già. Io personalmente vado con awk. Per comodità, se usi bash, aggiungi il sotto al tuo ~/.bash_profile . E la prossima volta che effettui il login (o se apri il tuo .bash_profile dopo questo aggiornamento), avrai una nuova “nth” nifty funzione disponibile per il pipe dei tuoi file.

Esegui questo o mettilo nel tuo ~ / .bash_profile (se usi bash) e riapri bash (o esegui source ~/.bach_profile )

# print just the nth piped in line nth () { awk -vlnum=${1} 'NR==lnum {print; exit}'; }

Quindi, per usarlo, passa semplicemente attraverso di esso. Per esempio,:

$ yes line | cat -n | nth 5 5 line

Per stampare l’ennesima riga usando sed con una variabile come numero di riga:

 a=4 sed -e $a'q:d' file 

Qui il flag ‘-e’ serve per aggiungere script al comando da eseguire.

Usando ciò che altri hanno menzionato, volevo che questa fosse una funzione rapida e dandy nella mia shell bash.

Crea un file: ~/.functions

Aggiungi ad esso il contenuto:

getline() { line=$1 sed $line'q;d' $2 }

Quindi aggiungi questo al tuo ~/.bash_profile :

source ~/.functions

Ora quando apri una nuova finestra bash, puoi semplicemente chiamare la funzione in questo modo:

getline 441 myfile.txt

 echo  | head  

Dove n è il numero di linea che vogliamo stampare.