it-swarm-eu.dev

Strumento Bash per ottenere l'ennesima riga da un file

C'è un modo "canonico" per farlo? Ho usato 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.

451
Vlad Vivdovitch

head e pipe con tail sarà lento per un file enorme. Vorrei suggerire sed in questo modo:

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 chiuderà immediatamente quando il numero di riga è NUM.

d cancellerà la linea invece di stamparla; questo è inibito sull'ultima riga perché q fa sì che il resto dello script venga ignorato quando esce.

Se hai NUM in una variabile, ti consigliamo di utilizzare le virgolette anziché quelle singole:

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

stamperà la seconda 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: inserisci una linea in una certa posizione

242
jm666

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 ciascuno.

Impostare

Ho un file di dati di testo di 3.261 gigabyte ASCII 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 iniziare 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 realizzarlo 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 built-in per confrontare ogni comando.

Linea di base

Per prima cosa vediamo come la soluzione headtail:

$ 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.

taglio

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 legga 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 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 ha funzionato 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 headtail. 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
71

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 -v n=$num 'NR == n' file   # equivalent
42
fedorqui

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.

26
David W.
# print line number 52
sed '52!d' file

Utili script one-line per sed

20
Steven Penny

Questa domanda viene taggata come Bash, ecco come funziona Bash (≥4): usa mapfile con l'opzione -s (skip) e -n (count).

Se è necessario ottenere la quarantaduesima riga di un file file:

mapfile -s 41 -n 1 ary < file

A questo punto, avrai un array ary i cui campi contenenti le righe di file (inclusa la newline finale), dove abbiamo saltato le prime 41 righe (-s 41), e si sono fermati 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 il range 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 (trim):

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!

19
gniourf_gniourf

Puoi anche usare la stampa sed e uscire:

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

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

tail -n+N | head -1

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

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


L'alternativa 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 da tail | head (dall'alto) quando i file diventano enormi.

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

Nei miei test, entrambe le versioni di tails/heads hanno sovraperformato sed 'NUMq;d' in modo coerente. 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: 3.7 sec
  • head -N | tail -1: 4.6 sec
  • sed Nq;d: 18,8 sec

I risultati possono essere diversi, ma la performance head | tail e tail | head è, in generale, comparabile per gli 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 tieni presente 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
9
Philipp Claßen

Puoi anche usare Perl per questo:

Perl -wnl -e '$.== NUM && print && exit;' some.file
7
Timofey Stolbov

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

  • dall'inizio del file alla riga di partenza. Lo chiamo S
  • la distanza dall'ultima riga alla fine del file. Sia 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

6
user2350426

Come seguito alla risposta molto utile di benchmark 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!

4

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, devono essere letti tutti i byte del file prima dell'nth line need. 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 es. all'inizio del file, indicando le posizioni in cui iniziano le linee. È possibile utilizzare un formato di database o semplicemente aggiungere una tabella all'inizio del file. In alternativa, crea un file indice separato per accompagnare il tuo file di testo di grandi dimensioni. 

per esempio. potresti creare un elenco di posizioni dei caratteri per le nuove linee:

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

quindi leggi con tail, che in realtà seeks direttamente al punto appropriato nel file!

per esempio. per ottenere la riga 1000:

tail -c +$(awk 'NR=1000' file.idx) file.txt | head -1
  • Questo potrebbe non funzionare con i caratteri 2 byte/multibyte, dal momento che 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!
4
Sanjay Manohar

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.

3
danger89

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

2
JJC

Usando ciò che altri menzionavano, volevo che questa fosse una funzione rapida e dandy nella mia shell di 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

1
Mark Shust

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.

1
aliasav

Ho inserito alcune delle risposte precedenti in un breve script di bash che puoi inserire in un file chiamato get.sh e link a /usr/local/bin/get (o qualunque altro nome tu preferisca).

#!/bin/bash
if [ "${1}" == "" ]; then
    echo "error: blank line number";
    exit 1
fi
re='^[0-9]+$'
if ! [[ $1 =~ $re ]] ; then
    echo "error: line number arg not a number";
    exit 1
fi
if [ "${2}" == "" ]; then
    echo "error: blank file name";
    exit 1
fi
sed "${1}q;d" $2;
exit 0

Assicurati che sia eseguibile con 

$ chmod +x get

Collegalo per renderlo disponibile su PATH con

$ ln -s get.sh /usr/local/bin/get

Consuma responsabilmente!

P

0
polarise