web-dev-qa-db-fra.com

ligne de chat X à ligne Y sur un énorme fichier

Disons que j'ai un énorme fichier texte (> 2 Go) et je veux juste cat les lignes X à Y (par exemple 57890000 à 57890010).

D'après ce que je comprends, je peux le faire en canalisant head dans tail ou vice-versa, c'est-à-dire.

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

ou bien

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

A, B, C et D peuvent être calculés à partir du nombre de lignes du fichier, X et Y.

Mais cette approche pose deux problèmes:

  1. Vous devez calculer A, B, C et D.
  2. Les commandes pourraient pipe entre elles beaucoup plus lignes que ce que je suis intéressé à lire (par exemple si je ne lis que quelques lignes au milieu d'un énorme fichier)

Existe-t-il un moyen de faire fonctionner le Shell avec et de sortir les lignes que je veux? (tout en ne fournissant que X et Y)?

142

Je suggère la solution sed, mais par souci d'exhaustivité,

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 vitesse (ici sur macOS, YMMV sur d'autres systèmes):

  • 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
  • real heure telle que rapportée par bash's builtin time
 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 repères 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;q et head|tail, qui semblent être essentiellement les mêmes.

128
Kevin

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

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

tail lira et supprimera les premières lignes X-1 (il n'y a aucun moyen de contourner cela), puis lira et imprimera les lignes suivantes. head lira et imprimera le nombre de lignes demandé, puis quittera. Lorsque head se ferme, tail reçoit un signal SIGPIPE et meurt, il n'aura donc pas lu plus qu'un tampon la valeur (généralement quelques kilo-octets) de lignes du fichier d'entrée.

Alternativement, comme gorkypl suggéré, utilisez sed:

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

La solution sed est cependant beaucoup plus lente (au moins pour les utilitaires GNU et Busybox; sed pourrait être plus compétitif si vous extrayez une grande partie du fichier sur un OS où la tuyauterie est lente et sed est 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, /tmp est tmpfs et la machine est sinon inactive et n'échange pas.

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 sautant 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

Le head | tail L'approche est l'un des moyens les meilleurs et les plus "idiomatiques" de le faire:

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

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

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

La raison pour laquelle cela est plus rapide est la première X - 1 les lignes n'ont pas besoin de passer par le tuyau par rapport au head | tail approche.

Votre question, telle qu'elle est formulée, est un peu trompeuse et explique probablement certaines de vos réticences non fondées à l'égard de cette approche.

  • Vous dites que vous devez calculer A, B, C, D mais comme vous pouvez le voir, le nombre de lignes du fichier n'est pas nécessaire et à la plupart des calculs sont nécessaires, ce que Shell peut faire pour vous de toute façon.

  • Vous craignez que la tuyauterie ne lise plus de lignes que nécessaire. En fait, ce n'est pas vrai: tail | head est à peu près aussi efficace que possible en termes d'E/S de fichiers. Tout d'abord, considérez la quantité minimale de travail nécessaire: pour trouver la X 'ème ligne dans un fichier, la seule façon générale de le faire est de lire chaque octet et arrêtez quand vous comptez X symboles de nouvelle ligne car il n'y a aucun moyen de deviner le décalage de fichier du [[# #]] x [~ # ~] 'ème ligne. Une fois que vous atteignez la * X * ème ligne, vous devez lire toutes les lignes afin de les imprimer, en vous arrêtant à la Y 'ème ligne. Ainsi, aucune approche ne peut s'en tirer avec une lecture inférieure à Y lignes. Maintenant, head -n $Y ne lit pas plus de Y lignes (arrondies à l'unité tampon la plus proche, mais les tampons utilisés correctement améliorent les performances, donc pas besoin de s'inquiéter de cette surcharge) . De plus, tail ne lira pas plus que head, nous avons donc montré que head | tail lit le moins de lignes possible (encore une fois, plus une mise en mémoire tampon négligeable que nous ignorons). Le seul avantage d'efficacité d'une approche à outil unique qui n'utilise pas de tuyaux est moins de processus (et donc moins de frais généraux).

24
jw013

La façon la plus orthodoxe (mais pas la 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'option -n Implique que seules les lignes pertinentes sont imprimées sur stdout.

Le p à la fin du numéro de ligne d'arrivée signifie imprimer les 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.

16
Paweł Rumian

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

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

Si nous connaissons le nombre total de lignes: lAll nous pourrions également 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: tailnumber comme ceci:

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

Nous permet d'utiliser la commande d'exécution la plus rapide:

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

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

La seule mise en garde est que nous avons besoin du nombre total de lignes, et cela peut prendre un peu plus de temps à trouver.
Comme d'habitude avec:

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

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

7
user79743

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
1
Doolan