cat line X to line Y su un file enorme


132

Dire che ho un file di testo enorme (> 2 GB) e voglio solo catle linee Xa Y(ad es 57.890.000-57.890.010).

Da quello che ho capito posso farlo eseguendo il piping headin tailo viceversa, vale a dire

head -A /path/to/file | tail -B

o in alternativa

tail -C /path/to/file | head -D

dove A, B, Ce Dpuò essere calcolata dal numero di righe nel file, Xe Y.

Ma ci sono due problemi con questo approccio:

  1. Bisogna calcolare A, B, Ce D.
  2. I comandi potrebbero pipescambiarsi molte più righe di quelle che mi interessa leggere (ad es. Se sto leggendo solo poche righe nel mezzo di un file enorme)

C'è un modo per far funzionare la shell e produrre le linee che desidero? (pur fornendo solo Xe Y)?


1
Cordiali saluti, confronto test di velocità reale di 6 metodi aggiunti alla mia risposta.
Kevin,

Risposte:


119

Suggerisco la sedsoluzione, ma per completezza,

awk 'NR >= 57890000 && NR <= 57890010' /path/to/file

Per ritagliare dopo l'ultima riga:

awk 'NR < 57890000 { next } { print } NR == 57890010 { exit }' /path/to/file

Test di velocità:

  • File da 100.000.000 di righe generato da seq 100000000 > test.in
  • Linee di lettura 50.000.000-50.000.010
  • Test in nessun ordine particolare
  • realil tempo come riportato da bash's incorporatotime
 4.373  4.418  4.395    tail -n+50000000 test.in | head -n10
 5.210  5.179  6.181    sed -n '50000000,50000010p;57890010q' test.in
 5.525  5.475  5.488    head -n50000010 test.in | tail -n10
 8.497  8.352  8.438    sed -n '50000000,50000010p' test.in
22.826 23.154 23.195    tail -n50000001 test.in | head -n10
25.694 25.908 27.638    ed -s test.in <<<"50000000,50000010p"
31.348 28.140 30.574    awk 'NR<57890000{next}1;NR==57890010{exit}' test.in
51.359 50.919 51.127    awk 'NR >= 57890000 && NR <= 57890010' test.in

Questi non sono affatto parametri precisi, ma la differenza è abbastanza chiara e ripetibile * da dare un buon senso della velocità relativa di ciascuno di questi comandi.

*: Tranne tra i primi due sed -n p;qe head|tail, che sembrano essere sostanzialmente gli stessi.


11
Per curiosità: come hai svuotato la cache del disco tra i test?
Paweł Rumian,

2
Che dire tail -n +50000000 test.in | head -n10, a differenza tail -n-50000000 test.in | head -n10che darebbe il risultato corretto?
Gilles,

4
Ok, sono andato e ho fatto alcuni benchmark. tail | head è molto più veloce di sed, la differenza è molto più di quanto mi aspettassi.
Gilles,

3
@Gilles hai ragione, mia cattiva. tail+|headè più veloce del 10-15% rispetto a sed, ho aggiunto quel benchmark.
Kevin,

1
Mi rendo conto che la domanda richiede delle righe, ma se si utilizza -cper saltare i caratteri, tail+|headè istantaneo. Ovviamente, non puoi dire "50000000" e potrebbe essere necessario cercare manualmente l'inizio della sezione che stai cercando.
Danny Kirchmeier,

51

Se si desidera includere le linee da X a Y (iniziando la numerazione da 1), utilizzare

tail -n +$X /path/to/file | head -n $((Y-X+1))

tailleggerà e scarterà le prime righe X-1 (non c'è modo di aggirarle), quindi leggerà e stamperà le seguenti righe. headleggerà e stamperà il numero di righe richiesto, quindi uscirà. Quando headesce, tailriceve un segnale SIGPIPE e muore, quindi non avrà letto più del valore di una dimensione del buffer (in genere pochi kilobyte) di righe dal file di input.

In alternativa, come suggerito Gorkypl , usa sed:

sed -n -e "$X,$Y p" -e "$Y q" /path/to/file

La soluzione sed è significativamente più lenta (almeno per le utility GNU e le utility Busybox; sed potrebbe essere più competitiva se si estrae gran parte del file su un sistema operativo in cui le tubazioni sono lente e sed è veloce). Ecco alcuni benchmark rapidi sotto Linux; i dati sono stati generati seq 100000000 >/tmp/a, l'ambiente è Linux / amd64, /tmpè tmpfs e la macchina è altrimenti inattiva e non scambia.

real  user  sys    command
 0.47  0.32  0.12  </tmp/a tail -n +50000001 | head -n 10 #GNU
 0.86  0.64  0.21  </tmp/a tail -n +50000001 | head -n 10 #BusyBox
 3.57  3.41  0.14  sed -n -e '50000000,50000010 p' -e '50000010q' /tmp/a #GNU
11.91 11.68  0.14  sed -n -e '50000000,50000010 p' -e '50000010q' /tmp/a #BusyBox
 1.04  0.60  0.46  </tmp/a tail -n +50000001 | head -n 40000001 >/dev/null #GNU
 7.12  6.58  0.55  </tmp/a tail -n +50000001 | head -n 40000001 >/dev/null #BusyBox
 9.95  9.54  0.28  sed -n -e '50000000,90000000 p' -e '90000000q' /tmp/a >/dev/null #GNU
23.76 23.13  0.31  sed -n -e '50000000,90000000 p' -e '90000000q' /tmp/a >/dev/null #BusyBox

Se conosci l'intervallo di byte con cui vuoi lavorare, puoi estrarlo più velocemente saltando direttamente alla posizione iniziale. Ma per le righe, devi leggere dall'inizio e contare le nuove righe. Per estrarre i blocchi da x inclusivo a y esclusivo a partire da 0, con una dimensione del blocco di b:

dd bs=$b seek=$x count=$((y-x)) </path/to/file

1
Sei sicuro che non ci sia cache tra di loro? Le differenze tra tail | head e sed mi sembrano troppo grandi.
Paweł Rumian,

@gorkypl Ho fatto diverse misure e i tempi erano comparabili. Come ho scritto, tutto ciò accade nella RAM (tutto è nella cache).
Gilles,

1
@Gilles tail will read and discard the first X-1 linesembra essere evitato quando il numero di righe viene dato dalla fine, in tal caso, la coda sembra leggere all'indietro dalla fine in base ai tempi di esecuzione. Si prega di leggere: http://unix.stackexchange.com/a/216614/79743.

1
@BinaryZebra Sì, se l'input è un file normale, alcune implementazioni di tail(inclusa la coda GNU) hanno euristiche da leggere dalla fine. Ciò migliora la tail | headsoluzione rispetto ad altri metodi.
Gilles,

22

L' head | tailapproccio è uno dei modi migliori e più "idiomatici" per farlo:

X=57890000
Y=57890010
< infile.txt head -n "$Y" | tail -n +"$X"

Come sottolineato da Gilles nei commenti, è un modo più veloce

< infile.txt tail -n +"$X" | head -n "$((Y - X))"

Il motivo per cui questo è più veloce è che le prime linee X - 1 non devono passare attraverso il tubo rispetto head | tailall'approccio.

La tua domanda come formulata è un po 'fuorviante e probabilmente spiega alcuni dei tuoi dubbi infondati verso questo approccio.

  • Tu dici di avere calcolare A, B, C, Dma come si può vedere, il numero di riga del file non è necessaria e al massimo 1 calcolo è necessario, che il guscio può fare per voi in ogni modo.

  • Temi che le tubazioni leggano più righe del necessario. In realtà questo non è vero: tail | headè il più efficiente possibile in termini di I / O dei file. Innanzitutto, considera la quantità minima di lavoro necessaria: per trovare la X 'linea in un file, l'unico modo generale per farlo è leggere ogni byte e fermarti quando conti X simboli di nuova riga poiché non c'è modo di divinare il file offset della X 'linea. Una volta raggiunta la riga * X *, devi leggere tutte le righe per stamparle, fermandoti sulla riga Y. Quindi nessun approccio può cavarsela leggendo meno di Y righe. Ora head -n $Ynon legge più di Ylinee (arrotondate all'unità buffer più vicina, ma i buffer se utilizzati correttamente migliorano le prestazioni, quindi non è necessario preoccuparsi di tale sovraccarico). Inoltre, tailnon leggeremo più di head, quindi abbiamo dimostrato che head | taillegge il minor numero possibile di righe (di nuovo, più un trascurabile buffering che stiamo ignorando). L'unico vantaggio in termini di efficienza di un approccio a singolo strumento che non utilizza tubi è un minor numero di processi (e quindi un minor sovraccarico).


1
Non ho mai visto il reindirizzamento andare prima sulla linea prima. Fresco, rende più chiaro il flusso del tubo.
clacke,

14

Il modo più ortodosso (ma non il più veloce, come notato da Gilles sopra) sarebbe di usare sed.

Nel tuo caso:

X=57890000
Y=57890010
sed -n -e "$X,$Y p" -e "$Y q" filename

L' -nopzione implica che solo le righe pertinenti vengono stampate su stdout.

La p alla fine del numero del traguardo significa stampare le linee in un determinato intervallo. La q nella seconda parte dello script fa risparmiare un po 'di tempo saltando il resto del file.


1
Mi aspettavo sede tail | headper essere circa alla pari, ma si scopre che tail | headè significativamente più veloce (vedi la mia risposta ).
Gilles,

1
Non so, da quello che ho letto, tail/ headsono considerato più "ortodosso", dal momento che tagliare entrambe le estremità di un file è proprio quello per cui sono fatte. In quei materiali, sedsembra entrare nell'immagine solo quando sono necessarie sostituzioni - e di essere rapidamente espulso dall'immagine quando inizia a succedere qualcosa di molto più complesso, poiché la sua sintassi per compiti complessi è molto peggio di AWK, che quindi prende il sopravvento .
underscore_d

7

Se conosciamo l'intervallo da selezionare, dalla prima riga: lStartall'ultima riga: lEndpotremmo calcolare:

lCount="$((lEnd-lStart+1))"

Se conosciamo la quantità totale di linee: lAllpotremmo anche calcolare la distanza dalla fine del file:

toEnd="$((lAll-lStart+1))"

Quindi sapremo entrambi:

"how far from the start"            ($lStart) and
"how far from the end of the file"  ($toEnd).

Scegliendo il più piccolo di uno di quelli: tailnumbercome questo:

tailnumber="$toEnd"; (( toEnd > lStart )) && tailnumber="+$linestart"

Ci consente di utilizzare il comando di esecuzione costantemente più veloce:

tail -n"${tailnumber}" ${thefile} | head -n${lCount}

Si noti il ​​segno più ("+") aggiuntivo quando $linestartè selezionato.

L'unica avvertenza è che abbiamo bisogno del conteggio totale delle linee e che potrebbe richiedere del tempo aggiuntivo per trovare.
Come al solito con:

linesall="$(wc -l < "$thefile" )"

Alcune volte misurate sono:

lStart |500| lEnd |500| lCount |11|
real   user   sys    frac
0.002  0.000  0.000  0.00  | command == tail -n"+500" test.in | head -n1
0.002  0.000  0.000  0.00  | command == tail -n+500 test.in | head -n1
3.230  2.520  0.700  99.68 | command == tail -n99999501 test.in | head -n1
0.001  0.000  0.000  0.00  | command == head -n500 test.in | tail -n1
0.001  0.000  0.000  0.00  | command == sed -n -e "500,500p;500q" test.in
0.002  0.000  0.000  0.00  | command == awk 'NR<'500'{next}1;NR=='500'{exit}' test.in


lStart |50000000| lEnd |50000010| lCount |11|
real   user   sys    frac
0.977  0.644  0.328  99.50 | command == tail -n"+50000000" test.in | head -n11
1.069  0.756  0.308  99.58 | command == tail -n+50000000 test.in | head -n11
1.823  1.512  0.308  99.85 | command == tail -n50000001 test.in | head -n11
1.950  2.396  1.284  188.77| command == head -n50000010 test.in | tail -n11
5.477  5.116  0.348  99.76 | command == sed -n -e "50000000,50000010p;50000010q" test.in
10.124  9.669  0.448  99.92| command == awk 'NR<'50000000'{next}1;NR=='50000010'{exit}' test.in


lStart |99999000| lEnd |99999010| lCount |11|
real   user   sys    frac
0.001  0.000  0.000  0.00  | command == tail -n"1001" test.in | head -n11
1.960  1.292  0.660  99.61 | command == tail -n+99999000 test.in | head -n11
0.001  0.000  0.000  0.00  | command == tail -n1001 test.in | head -n11
4.043  4.704  2.704  183.25| command == head -n99999010 test.in | tail -n11
10.346  9.641  0.692  99.88| command == sed -n -e "99999000,99999010p;99999010q" test.in
21.653  20.873  0.744  99.83 | command == awk 'NR<'99999000'{next}1;NR=='99999010'{exit}' test.in

Si noti che i tempi cambiano drasticamente se le linee selezionate sono vicine all'inizio o alla fine. Un comando che sembra funzionare bene su un lato del file, può essere estremamente lento sull'altro lato del file.


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
terdon

@BinaryZebra - molto meglio.
Mikeserv,

0

Lo faccio abbastanza spesso e così ho scritto questo script. Non ho bisogno di trovare i numeri di riga, lo script fa tutto.

#!/bin/bash

# $1: start time
# $2: end time
# $3: log file to read
# $4: output file

# i.e. log_slice.sh 18:33 19:40 /var/log/my.log /var/log/myslice.log

if [[ $# != 4 ]] ; then 
echo 'usage: log_slice.sh <start time> <end time> <log file> <output file>'
echo
exit;
fi

if [ ! -f $3 ] ; then
echo "'$3' doesn't seem to exit."
echo 'exiting.'
exit;
fi

sline=$(grep -n " ${1}" $3|head -1|cut -d: -f1)  #what line number is first occurrance of start time
eline=$(grep -n " ${2}" $3|head -1|cut -d: -f1)  #what line number is first occurrance of end time

linediff="$((eline-sline))"

tail -n+${sline} $3|head -n$linediff > $4

2
Stai rispondendo a una domanda che non è stata posta. La tua risposta è del 10% tail|head, che è stata ampiamente discussa nella domanda e nelle altre risposte, e il 90% determina i numeri di riga in cui compaiono stringhe / schemi specificati, che non faceva parte della domanda . PS dovresti sempre citare i parametri e le variabili della shell; ad es. "$ 3" e "$ 4".
G-Man,
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.