Outil Bash pour obtenir nth ligne à partir d'un fichier

y a-t-il une façon" canonique " de faire ça? J'ai utilisé head -n | tail -1 ce qui fait l'affaire, mais je me demandais s'il y avait un outil Bash qui extrait spécifiquement une ligne (ou une gamme de lignes) d'un fichier.

par "canonique", j'entends un programme dont la fonction principale est de faire cela.

423
demandé sur Peter Mortensen 2011-05-16 23:33:42

19 réponses

head et pipe avec tail seront lents pour un gros fichier. Je suggérerais sed comme ceci:

sed 'NUMq;d' file

NUM est le numéro de la ligne que vous voulez imprimer; ainsi, par exemple, sed '10q;d' file pour imprimer la ligne 10 de file .

explication:

NUMq cessera immédiatement lorsque le numéro de ligne est NUM .

d va supprimer la ligne au lieu de l'imprimer; ceci est inhibé sur la dernière ligne parce que le q provoque le reste du script à sauter lors de quitter.

si vous avez NUM dans une variable, vous voudrez utiliser des guillemets au lieu de simples:

sed "${NUM}q;d" file
563
répondu anubhava 2016-02-16 04:22:29
sed -n '2p' < file.txt

affichera la deuxième ligne

sed -n '2011p' < file.txt

2011e ligne

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

ligne 10 jusqu'à la ligne 33

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

1ère et 3ème lignes

et ainsi de suite...

pour ajouter des lignes avec sed, vous pouvez cocher cette case:

sed: insérer une ligne dans une certaine position

223
répondu jm666 2017-05-23 10:31:16

j'ai une situation unique où je peux comparer les solutions proposées sur cette page, et donc j'écris cette réponse comme une consolidation des solutions proposées avec des temps d'exécution inclus pour chacun.

Set Up

j'ai un fichier texte ASCII de 3.261 gigaoctets avec une paire de clés par ligne. Le fichier contient 3,339,550,320 lignes au total et défie l'ouverture dans n'importe quel éditeur, j'ai essayé, y compris de ma Vim. J'ai besoin de sous-traiter ce fichier afin d'enquêter sur certaines des valeurs que j'ai découvert ne commencent autour de la rangée ~500 000 000.

parce que le fichier a tellement de lignes:

  • je dois extraire seulement un sous-ensemble des lignes pour faire quelque chose d'utile avec les données.
  • lire à travers chaque ligne menant aux valeurs auxquelles je tiens va prendre du temps.
  • si la solution lit au-delà de les lignes que je me soucie et continue à lire le reste du fichier il perdra du temps à lire près de 3 milliards de lignes non pertinentes et prendra 6x plus de temps que nécessaire.

mon meilleur scénario est une solution qui n'extrait qu'une seule ligne du fichier sans lire les autres lignes du fichier, mais je ne vois pas comment je pourrais accomplir cela à Bash.

pour des raisons de santé mentale, Je ne vais pas essayer de lire la suite 500 millions de lignes dont j'aurais besoin pour mon propre problème. Au lieu de cela, je vais essayer d'extraire la rangée 50.000.000 sur 3.339.550.320 (ce qui signifie que la lecture du dossier complet prendra 60x plus de temps que nécessaire).

j'utiliserai le time intégré pour comparer chaque commande.

de Base

voyons D'abord comment la head tail solution:

$ time head -50000000 myfile.ascii | tail -1
pgm_icnt = 0

real    1m15.321s

le la ligne de base pour une rangée de 50 millions Est 00: 01: 15.321, si j'avais été direct pour une rangée de 500 millions, ça aurait dû prendre environ 12,5 minutes.

couper

je suis dubitatif de celui-ci, mais ça vaut le coup:

$ time cut -f50000000 -d$'\n' myfile.ascii
pgm_icnt = 0

real    5m12.156s

celui-ci a pris 00:05:12.156 pour courir, ce qui est beaucoup plus lent que la ligne de base! Je ne sais pas s'il a lu tout le dossier ou jusqu'à la ligne 50 millions avant de s'arrêter, mais quoi qu'il en soit ne semble pas être une solution viable à ce problème.

AWK

Je n'ai couru la solution qu'avec le exit parce que je n'allais pas attendre que le fichier complet s'exécute:

$ time awk 'NR == 50000000 {print; exit}' myfile.ascii
pgm_icnt = 0

real    1m16.583s

ce code a couru dans 00:01:16.583, qui est seulement ~1 seconde plus lent, mais toujours pas une amélioration sur la ligne de base. À ce rythme, si la commande de sortie avait été exclue, il aurait probablement fallu environ ~76 minutes pour lire le dossier complet!

Perl

j'ai aussi exécuté la solution Perl existante:

$ time perl -wnl -e '$.== 50000000 && print && exit;' myfile.ascii
pgm_icnt = 0

real    1m13.146s

ce code s'est déroulé à 00:01:13.146, ce qui est environ 2 secondes plus rapide que la ligne de base. Si je l'avais lancé sur les 500 millions, ça prendrait 12 minutes.

sed

la meilleure réponse sur le tableau, voici mon résultat:

$ time sed "50000000q;d" myfile.ascii
pgm_icnt = 0

real    1m12.705s

Ce code a couru dans 00:01:12.705, qui est de 3 secondes plus rapide que la ligne de base, et ~0,4 secondes plus rapide que Perl. Si je l'avais mis sur les 500 millions de rangs, ça aurait pris 12 minutes.

mapfile

j'ai bash 3.1 et ne peux donc pas tester la solution mapfile.

Conclusion

It on dirait, pour la plupart, qu'il est difficile d'améliorer la solution head tail . Au mieux, la solution sed offre une augmentation d'environ 3% de l'efficacité.

(pourcentages calculés avec la formule % = (runtime/baseline - 1) * 100 )

ligne 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

ligne 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

ligne 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
56
répondu CaffeineConnoisseur 2017-06-04 22:11:46

avec awk c'est assez rapide:

awk 'NR == num_line' file

Lorsque cela est vrai, le comportement par défaut de awk : {print "151950920"} .


variantes

si votre fichier est énorme, vous feriez mieux de exit après avoir lu la ligne requise. De cette façon, vous gagnez du temps en CPU.

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

si vous voulez donner le numéro de ligne d'une bash variable à utiliser:

awk 'NR == n' n=$num file
awk -v n=$num 'NR == n' file   # equivalent
40
répondu fedorqui 2015-07-06 07:59:09

Wow, toutes les possibilités!

essayez ceci:

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

ou l'un de ceux-ci selon votre version de Awk:

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

( Vous pourriez avoir à essayer le nawk ou gawk commande ).

Est-il un outil qui ne fait qu'imprimer cette ligne? Pas l'un des outils standard. Cependant, sed est probablement le plus proche et le plus simple à utiliser.

24
répondu David W. 2013-08-08 14:06:40
# print line number 52
sed '52!d' file

scripts utiles d'une ligne pour sed

18
répondu Steven Penny 2014-01-10 15:27:29

cette question étant marquée Bash, voici la façon de faire Bash (≥4): Utilisez mapfile avec l'option -s (skip) et -n (count).

si vous devez obtenir la 42e ligne d'un fichier file :

mapfile -s 41 -n 1 ary < file

à ce point, vous aurez un tableau ary dont les champs contiennent les lignes de file (y compris la nouvelle ligne arrière), où nous avons sauté les premières 41 lignes ( -s 41 ), et s'est arrêté après avoir lu une ligne ( -n 1 ). Donc c'est vraiment la 42e ligne. Pour l'imprimer:

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

Si vous avez besoin d'un ensemble de lignes, de dire la gamme 42-666 (inclus), et de dire que vous ne voulez pas faire le calcul vous-même, et de les imprimer sur la sortie standard:

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

si vous avez besoin de traiter ces lignes aussi, il n'est pas vraiment pratique de stocker la nouvelle ligne arrière. Dans ce cas, utilisez l'option -t (garniture):

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

vous pouvez avoir une fonction faire que pour vous:

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

pas de commandes externes, seulement Bash builtins!

18
répondu gniourf_gniourf 2014-05-17 20:35:42

, Vous pouvez également utilisé sed d'impression et quitter:

sed -n '10{p;q;}' file   # print line 10
8
répondu bernd 2015-10-23 22:57:28

selon mes tests, en termes de performance et de lisibilité, ma recommandation est:

tail -n+N | head -1

N est le numéro de ligne que vous voulez. Par exemple, tail -n+7 input.txt | head -1 affichera la 7ème ligne du fichier.

tail -n+N imprimera tout à partir de la ligne N , et head -1 le fera arrêter après une ligne.


le la variante head -N | tail -1 est peut-être un peu plus lisible. Par exemple, cela affichera la 7ème ligne:

head -7 input.txt | tail -1

quand il s'agit de la performance, il n'y a pas beaucoup de différence pour les petites tailles, mais il sera surpassé par le tail | head (d'en haut) quand les fichiers deviennent énormes.

le Top-voté sed 'NUMq;d' est intéressant à savoir, mais je dirais qu'il sera compris par moins de gens sur le zone de la tête/queue de solution et il est plus lent que la queue de la tête.

dans mes tests, les deux versions pile/heads ont constamment dépassé sed 'NUMq;d' . Cela est conforme aux autres points de repère affichés. Il est difficile de trouver un cas où tails/heads était vraiment mauvais. Ce n'est pas non plus surprenant, car il s'agit d'opérations qui devraient être fortement optimisées dans un système Unix moderne.

pour se faire une idée de la performance différences, voici le nombre que je reçois pour un énorme fichier (9.3 G):

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

les résultats peuvent différer, mais la performance head | tail et tail | head est, en général, comparable pour des entrées plus petites, et sed est toujours plus lente d'un facteur significatif (environ 5x).

pour reproduire mon benchmark, vous pouvez essayer ce qui suit, mais soyez averti qu'il va créer un fichier 9.3 G dans le répertoire de travail actuel:

#!/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

Voici la sortie d'une course sur ma machine (ThinkPad X1 Carbon avec un SSD et 16G de mémoire). Je suppose que dans l'exécution finale tout viendra du cache, pas du disque:

*** 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
7
répondu Philipp Claßen 2018-01-20 02:19:49

vous pouvez également utiliser Perl pour ce faire:

perl -wnl -e '$.== NUM && print && exit;' some.file
6
répondu Timofey Stolbov 2014-10-21 22:07:04

la solution la plus rapide pour les gros fichiers est toujours tail|head, à condition que les deux distances:

  • depuis le début du fichier jusqu'à la ligne de départ. Appelons-le S
  • la distance entre la dernière ligne et la fin du fichier. Être E

sont connus. Nous pourrions alors utiliser ceci:

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

combien est juste le nombre de lignes nécessaires.

plus de détails dans https://unix.stackexchange.com/a/216614/79743

5
répondu Community 2017-04-13 12:36:30

pour donner suite à la réponse très utile de Caffeineconnoisieur... J'étais curieux de savoir à quelle vitesse la méthode' mapfile ' était comparée aux autres (car elle n'a pas été testée), donc j'ai essayé une comparaison rapide et sale de la vitesse moi-même car j'ai bash 4 à portée de main. J'ai lancé un test de la méthode "queue / tête" (plutôt que tête | queue) mentionnée dans l'un des commentaires sur la réponse du haut pendant que j'y étais, pendant que les gens chantent ses louanges. Je n'ai pas de quoi que ce soit presque la taille de la testfile utilisé; le meilleur que j'ai pu trouver à court préavis était un fichier pedigree de 14M (de longues lignes qui sont séparées par des espaces, juste en dessous de 12000 lignes).

version courte: mapfile apparaît plus rapide que la méthode cut, mais plus lent que tout le reste, donc je l'appellerais un dud. la queue de la tête, otoh, que, ça pourrait être le plus rapide, mais avec un fichier de cette taille, la différence n'est pas importante par rapport à 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

Espérons que cette aide!

4
répondu Jo Valentine-Cooper 2018-01-10 14:16:21

si vous avez plusieurs lignes délimitées par \n (Normalement nouvelle ligne). Vous pouvez également utiliser 'cut':

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

, Vous obtiendrez la 2ème ligne du fichier. -f3 vous donne la 3e ligne.

3
répondu danger89 2016-01-07 16:27:53

toutes les réponses ci-dessus répondent directement à la question. Mais voici une solution moins directe mais une idée potentiellement plus importante, pour provoquer la réflexion.

puisque les longueurs de ligne sont arbitraires, tous les octets du fichier avant la nième ligne doivent être lus . Si vous avez un gros fichier ou avez besoin de répéter cette tâche plusieurs fois, et que ce processus prend beaucoup de temps, alors vous devriez sérieusement réfléchir à la question de savoir si vous devez stocker vos données dans un de manière différente, en premier lieu.

la vraie solution est d'avoir un index, par exemple au début du fichier, indiquant les positions où les lignes commencent. Vous pouvez utiliser un format de base de données, ou tout simplement ajouter une table au début du fichier. Vous pouvez aussi créer un fichier index séparé pour accompagner votre grand fichier texte.

par exemple, vous pouvez créer une liste de positions de caractères pour les nouvelles lignes:

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

alors lire avec tail , qui en fait seek s directement au point approprié dans le dossier!

p.ex. pour obtenir la ligne 1000:

tail -c +$(awk 'NR=1000' file.idx) file.txt | head -1
  • cela ne fonctionne peut-être pas avec les caractères de 2 octets / multi octets, puisque awk est "conscient des caractères" mais tail ne l'est pas.
  • Je ne l'ai pas testé contre un gros fichier.
  • voir Aussi cette réponse .
  • alternativement - divisez votre fichier en petits fichiers!
3
répondu Sanjay Manohar 2017-10-12 10:44:16

L'une des voies possibles:

sed -n 'NUM{p;q}'

notez que sans la commande q , si le fichier est grand, sed continue à fonctionner, ce qui ralentit le calcul.

2
répondu Jindra Helcl 2016-03-16 14:01:24

Beaucoup de bonnes réponses déjà. Je vais personnellement avec awk. Pour plus de commodité, si vous utilisez bash, il suffit d'ajouter ce qui suit à votre ~/.bash_profile . Et, la prochaine fois que vous vous connectez (ou si vous avez votre source .bash_profile après cette mise à jour), vous aurez une nouvelle fonction "nth" pour passer vos fichiers.

exécutez ceci ou mettez-le dans votre ~/.bash_profile (si vous utilisez bash) et rouvrir bash (ou exécuter source ~/.bach_profile )

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

ensuite, pour l'utiliser, il suffit de pipe à travers elle. Par exemple:

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

2
répondu JJC 2018-01-17 19:05:56

pour imprimer la nième ligne en utilisant sed avec une variable comme numéro de ligne:

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

ici le drapeau '- e ' est pour ajouter un script à la commande à exécuter.

1
répondu aliasav 2015-04-07 18:59:44

en utilisant ce que d'autres ont mentionné, je voulais que ce soit une fonction quick & dandy dans mon shell bash.

créer un fichier: ~/.functions

ajouter le contenu:

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

puis ajoutez ceci à votre ~/.bash_profile :

source ~/.functions

maintenant, quand vous ouvrez une nouvelle fenêtre de bash, vous pouvez simplement appeler la fonction comme suit:

getline 441 myfile.txt

1
répondu Mark Shust 2018-01-17 14:19:49
echo <filename> | head <n>

où n est le numéro de ligne que nous voulons imprimer.

-1
répondu eldhoittangeorge 2018-08-25 05:19:47