cat line X to line Y sur un gros fichier


132

Dire que j'ai un énorme fichier texte (> 2 Go) et je veux juste catles lignes Xà Y(par exemple , de 57.890.000 à 57.890.010).

D'après ce que je comprends que je peux le faire par la tuyauterie headdans tailou vice - versa, à savoir

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

Ou bien

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

A, B, Cet Dpeut être calculé à partir du nombre de lignes dans le fichier, Xet Y.

Mais il y a deux problèmes avec cette approche:

  1. Vous devez calculer A, B, Cet D.
  2. Les commandes peuvent pipetransmettre beaucoup plus de lignes que ce qui m’intéresse (par exemple, si je ne lis que quelques lignes au milieu d’un fichier volumineux)

Existe-t-il un moyen de faire en sorte que le shell travaille et génère les lignes que je veux? (en ne fournissant que Xet Y)?


1
FYI, comparaison de test de vitesse réelle de 6 méthodes ajoutées à ma réponse.
Kevin

Réponses:


119

Je suggère la sedsolution, mais dans un souci de complétude,

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

Pour couper après la dernière ligne:

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

Test de rapidité:

  • Fichier de 100 000 000 lignes généré par seq 100000000 > test.in
  • Lignes de lecture 50 000 000 à 50 000 010
  • Tests sans ordre particulier
  • realtemps tel que rapporté par bash« s builtintime
 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

Ce ne sont en aucun cas des points de repère précis, mais la différence est claire et suffisamment répétable * pour donner une bonne idée de la vitesse relative de chacune de ces commandes.

*: Sauf entre les deux premiers sed -n p;qet head|tailqui semblent être essentiellement les mêmes.


11
Par curiosité: comment avez-vous vidé le cache disque entre les tests?
Paweł Rumian le

2
Qu'en est-il de tail -n +50000000 test.in | head -n10qui, contrairement tail -n-50000000 test.in | head -n10, donnerait le résultat correct?
Gilles le

4
Ok, je suis allé et fait quelques repères. tail | head est bien plus rapide que sed, la différence est beaucoup plus grande que ce à quoi je m'attendais.
Gilles le

3
@ Gilles tu as raison, mon mauvais. tail+|headest plus rapide de 10-15% que sed, j'ai ajouté cette référence.
Kevin

1
Je me rends compte que la question demande des lignes, mais si vous utilisez les -cpour sauter des caractères, tail+|headc'est instantané. Bien sûr, vous ne pouvez pas dire "50000000" et vous devrez peut-être rechercher manuellement le début de la section que vous recherchez.
Danny Kirchmeier

51

Si vous voulez les lignes X à Y inclus (en commençant par la numérotation à 1), utilisez

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

taillit et supprime les premières lignes X-1 (il n’ya aucun moyen de contourner cela), puis lit et affiche les lignes suivantes. headlira et imprimera le nombre de lignes demandé, puis quittera. Lorsqu’il headquitte, tailreçoit un signal SIGPIPE et meurt. Ainsi, il n’aura pas lu plus de la taille d’une mémoire tampon (généralement quelques kilo-octets) de lignes à partir du fichier d’entrée.

Sinon, comme suggéré par gorkypl , utilisez sed:

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

La solution sed est toutefois beaucoup plus lente (du moins pour les utilitaires GNU et Busybox; sed pourrait être plus compétitif si vous extrayez une grande partie du fichier sur un système d'exploitation où la tuyauterie est lente et sed est rapide). Voici des repères rapides sous Linux; les données ont été générées par seq 100000000 >/tmp/a, l'environnement est Linux / amd64, /tmptmpfs et la machine est sinon inactive et non permutée.

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

Si vous connaissez la plage d'octets avec laquelle vous souhaitez travailler, vous pouvez l'extraire plus rapidement en passant directement à la position de départ. Mais pour les lignes, vous devez lire depuis le début et compter les nouvelles lignes. Pour extraire des blocs de x inclus à y exclusif à partir de 0, avec une taille de bloc de b:

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

1
Etes-vous sûr qu'il n'y a pas de cache entre les deux? Les différences entre tail | head et sed me paraissent trop grandes.
Paweł Rumian

@gorkypl J'ai pris plusieurs mesures et les temps étaient comparables. Comme je l'ai écrit, tout se passe dans la RAM (tout est dans le cache).
Gilles

1
@Gilles tail will read and discard the first X-1 linesemble être évité lorsque le nombre de lignes est donné à partir de la fin. Dans ce cas, tail semble lire à partir de la fin en fonction des temps d'exécution. S'il vous plaît lire: http://unix.stackexchange.com/a/216614/79743.

1
@BinaryZebra Oui, si l'entrée est un fichier normal, certaines implémentations de tail(y compris GNU tail) ont une heuristique à lire à partir de la fin. Cela améliore la tail | headsolution par rapport aux autres méthodes.
Gilles le

22

L’ head | tailapproche est l’un des meilleurs moyens, parmi les plus "idiomatiques", de le faire:

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

Comme Gilles le souligne dans les commentaires, un moyen plus rapide est de

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

La raison pour laquelle cela est plus rapide est que les premières lignes X-1 n'ont pas besoin de passer par le tuyau par rapport à l' head | tailapproche.

Votre question, dans sa formulation, est un peu trompeuse et explique probablement certaines de vos craintes non fondées à l’égard de cette approche.

  • Vous dites que vous devez calculer A, B, C, Dmais comme vous pouvez le voir, le nombre de lignes du fichier n'est pas nécessaire et il est nécessaire de calcul au plus 1, la coque peut faire pour vous de toute façon.

  • Vous craignez que la tuyauterie lise plus de lignes que nécessaire. En fait, ce n’est pas vrai: tail | headc’est aussi efficace que possible en termes d’E / S sur fichier. Considérons d' abord le montant minimum de travail nécessaire: pour trouver le X e ligne » dans un fichier, la seule manière générale de le faire est de lire chaque octet et arrêter quand vous comptez X symboles de nouvelle ligne car il n'y a aucun moyen de deviner le fichier décalage de la X 'ème ligne. Une fois que vous avez atteint la * X * ème ligne, vous devez lire toutes les lignes afin de les imprimer, en vous arrêtant à la Y 'e ligne. Ainsi, aucune approche ne peut se permettre de lire moins de lignes Y. Maintenant, head -n $Yne lit pas plus que Ylignes (arrondies à l’unité tampon la plus proche, mais les tampons, s’ils sont utilisés correctement, améliorent les performances, évitant ainsi de s’inquiéter de cette surcharge). En outre, tailne lisons pas plus que head, nous avons donc montré que nous head | taillisions le moins de lignes possible (encore une fois, plus une mise en mémoire tampon négligeable que nous ignorons). Le seul avantage en termes d'efficacité d'une approche à un seul outil qui n'utilise pas de canalisations est la réduction du nombre de processus (et donc de la surcharge).


1
Jamais vu la redirection aller en premier sur la ligne avant. Cool, ça rend le tuyau plus clair.
clacke

14

Le moyen le plus orthodoxe (mais pas le plus rapide, comme l'a noté Gilles ci-dessus) serait d'utiliser sed.

Dans ton cas:

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

L' -noption implique que seules les lignes pertinentes sont imprimées sur stdout.

Le p à la fin du numéro de ligne d'arrivée signifie que vous imprimez des lignes dans une plage donnée. Le q dans la deuxième partie du script permet de gagner du temps en ignorant le reste du fichier.


1
Je m'y attendais sedet tail | headd'être à égalité, mais il se trouve que tail | headest nettement plus rapide (voir ma réponse ).
Gilles

1
Je ne sais pas, de ce que j'ai lu, tail/ headsont considérés comme plus « orthodoxes », depuis la coupe ou l' autre extrémité d'un fichier est précisément ce qu'ils sont faits pour. Dans ces documents, il sedne semble y avoir d’inconvénient que lorsque des substitutions sont nécessaires - et d’être rapidement exclu dès que quelque chose de beaucoup plus complexe commence à se produire, car sa syntaxe pour les tâches complexes est bien pire que AWK, qui prend ensuite le relais. .
underscore_d

7

Si nous connaissons la plage à sélectionner, de la première ligne: lStartà la dernière ligne: lEndnous pourrions calculer:

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

Si nous connaissons le nombre total de lignes: lAllnous pourrions aussi calculer la distance jusqu'à la fin du fichier:

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

Ensuite, nous connaîtrons les deux:

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

Choisir le plus petit de ceux-ci: tailnumbercomme ceci:

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

Nous permet d'utiliser la commande d'exécution la plus rapide de tous les temps:

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

Veuillez noter le signe plus ("+") supplémentaire lorsque $linestartest sélectionné.

Le seul inconvénient est que nous avons besoin du nombre total de lignes, ce qui peut prendre un peu plus de temps à trouver.
Comme d'habitude avec:

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

Quelques temps mesurés sont:

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

Notez que les temps changent radicalement si les lignes sélectionnées sont proches du début ou de la fin. Une commande qui semble bien fonctionner d'un côté du fichier peut être extrêmement lente de l'autre côté du fichier.


Les commentaires ne sont pas pour une discussion prolongée; cette conversation a été déplacée pour discuter .
terdon

@BinaryZebra - bien mieux.
mikeserv

0

Je le fais assez souvent et j'ai donc écrit ce script. Je n'ai pas besoin de trouver les numéros de ligne, le script fait tout.

#!/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
Vous répondez à une question qui n'a pas été posée. Votre réponse est 10% tail|head, ce qui a été longuement discuté dans la question et les autres réponses, et 90% déterminant les numéros de ligne où des chaînes / motifs spécifiés apparaissent, ce qui ne faisait pas partie de la question . PS vous devriez toujours citer vos paramètres de shell et vos variables; par exemple, "$ 3" et "$ 4".
G-Man
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.