web-dev-qa-db-fra.com

Outil Bash pour obtenir la nième ligne d'un fichier

Y a-t-il une manière "canonique" de faire cela? J'ai utilisé head -n | tail -1 qui fait l'affaire, mais je me demandais s'il existe un outil Bash qui extrait spécifiquement une ligne (ou une plage de lignes) d'un fichier.

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

451
Vlad Vivdovitch

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

sed 'NUMq;d' file

NUM est le numéro de la ligne à imprimer; Ainsi, par exemple, sed '10q;d' file pour imprimer la 10ème ligne de file.

Explication:

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

d supprimera la ligne au lieu de l’imprimer; ceci est inhibé sur la dernière ligne parce que q fait que le reste du script est ignoré lors de la fermeture.

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

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

imprimera la 2ème ligne

sed -n '2011p' < file.txt

2011ème ligne

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

ligne 10 jusqu'à ligne 33

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

1ère et 3ème ligne

etc...

Pour ajouter des lignes avec sed, vous pouvez vérifier ceci:

sed: insère une ligne dans une certaine position

242
jm666

J'ai une situation unique dans laquelle je peux comparer les solutions proposées sur cette page. J'écris donc cette réponse comme une consolidation des solutions proposées avec des durées d'exécution incluses pour chacune.

Installer

J'ai un fichier de données texte de 3,261 gigaoctets ASCII avec une paire clé-valeur par ligne. Le fichier contient 3 339 550 320 lignes au total et ne s'ouvre pas dans n'importe quel éditeur que j'ai essayé, y compris mon lecteur Vim. Je dois créer un sous-ensemble de ce fichier afin d’enquêter sur certaines des valeurs que j’ai découvertes ne commencent qu’à environ 500 000 000 lignes.

Parce que le fichier a tellement de lignes:

  • Je n'ai besoin d'extraire qu'un sous-ensemble de lignes pour pouvoir utiliser les données.
  • La lecture de chaque ligne menant aux valeurs qui me tiennent à coeur prendra beaucoup de temps.
  • Si la solution lit au-delà des lignes qui me tiennent à cœur et continue à lire le reste du fichier, elle perdra du temps à lire près de 3 milliards de lignes non pertinentes et prendra 6 fois plus de temps que nécessaire.

Mon meilleur scénario est une solution qui extrait une seule ligne du fichier sans lire aucune des autres lignes du fichier, mais je ne vois pas comment je pourrais réaliser cela dans Bash.

Pour des raisons de santé mentale, je ne vais pas essayer de lire les 500 000 000 lignes dont j'ai besoin pour résoudre mon propre problème. Au lieu de cela, je vais essayer d'extraire la ligne 50 000 000 sur 3 339 550 320 (ce qui signifie que la lecture du fichier complet prendra 60 fois plus longtemps que nécessaire).

J'utiliserai la variable time intégrée pour évaluer chaque commande.

Baseline

Voyons d'abord comment la solution headtail:

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

real    1m15.321s

La ligne de base pour la rangée 50 millions est 00: 01: 15.321. Si je passais directement à la rangée 500 millions, il faudrait probablement environ 12,5 minutes.

Couper

Je doute 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 fonctionner, ce qui est beaucoup plus lent que la ligne de base! Je ne sais pas s'il lit l'intégralité du fichier ou juste jusqu'à la ligne 50 millions avant de s'arrêter, mais peu importe, cela ne semble pas être une solution viable au problème.

AWK

J'ai uniquement exécuté la solution avec la variable exit car je n'allais pas attendre que le fichier complet soit exécuté:

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

real    1m16.583s

Ce code a été exécuté en 00: 01: 16.583, ce qui n’est que ~ 1 seconde plus lent, mais ne constitue toujours pas une amélioration par rapport à la ligne de base. À ce rythme, si la commande exit avait été exclue, il aurait probablement fallu environ 76 minutes environ pour lire le fichier entier!

Perl

J'ai aussi utilisé la solution Perl existante:

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

real    1m13.146s

Ce code a été exécuté en 00: 01: 13.146, ce qui est environ 2 secondes plus rapide que la ligne de base. Si je l'utilisais au complet, cela prendrait probablement environ 12 minutes.

sed

La réponse principale sur le tableau, voici mon résultat:

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

real    1m12.705s

Ce code a été exécuté en 00: 01: 12.705, soit 3 secondes plus rapide que la ligne de base et environ 0,4 seconde plus rapide que Perl. Si j'avais parcouru 500 000 000 de lignes, cela aurait probablement pris environ 12 minutes.

mapfile

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

Conclusion

Il semble que dans l’ensemble, il est difficile d’améliorer la solution headtail. Au mieux, la solution sed fournit une efficacité d'environ 3%.

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

Rangée 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

Rangée 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

Rangée 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
CaffeineConnoisseur

Avec awk c'est assez rapide:

awk 'NR == num_line' file

Lorsque cela est vrai, le comportement par défaut de awk est exécuté: {print $0}.


Versions alternatives

Si votre fichier est énorme, vous feriez mieux de changer exit après avoir lu la ligne requise. De cette façon, vous économisez du temps processeur.

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

Si vous voulez donner le numéro de ligne d'une variable bash, vous pouvez utiliser:

awk 'NR == n' n=$num file
awk -v n=$num 'NR == n' file   # equivalent
42
fedorqui

Wow, toutes les possibilités!

Essaye ça:

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

ou un de ceux-ci en fonction de votre version d'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

( Vous devrez peut-être essayer la commande nawk ou gawk).

Existe-t-il un outil permettant uniquement d’imprimer cette ligne? Pas l'un des outils standard. Cependant, sed est probablement le plus proche et le plus simple à utiliser.

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

Scripts utiles d'une ligne pour sed

20
Steven Penny

Cette question étant étiquetée Bash, voici la façon de procéder de Bash (≥4): utilisez mapfile avec les options -s (ignorer) et -n (compter).

Si vous devez obtenir la 42ème ligne d'un fichier file:

mapfile -s 41 -n 1 ary < file

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

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

Si vous avez besoin d'une plage de lignes, dites la plage 42–666 (incluse), et dites que vous ne voulez pas faire le calcul vous-même, puis imprimez-la sur la sortie standard:

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

Si vous devez également traiter ces lignes, il n'est pas très pratique de stocker le retour à la ligne suivant. Dans ce cas, utilisez l'option -t (trim):

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

Vous pouvez avoir une fonction qui le fait pour vous:

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[@]}"
}

Aucune commande externe, seulement les commandes intégrées Bash!

19
gniourf_gniourf

Vous pouvez également utiliser sed print et quitter:

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

D'après mes tests, en termes de performances et de lisibilité, ma recommandation est la suivante:

tail -n+N | head -1

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

tail -n+N imprimera tout à partir de la ligne N, et head -1 l'arrêtera après une ligne.


L'alternative head -N | tail -1 est peut-être un peu plus lisible. Par exemple, ceci imprimera la 7ème ligne:

head -7 input.txt | tail -1

En ce qui concerne les performances, il n’ya pas beaucoup de différence pour les tailles plus petites, mais le tail | head (en haut) l’aura surperformé lorsque les fichiers deviendront volumineux.

Le sed 'NUMq;d' avec les votes les plus élevés est intéressant à connaître, mais je dirais qu'il sera compris par moins de personnes prêtes à l'emploi que la solution tête/queue et qu'il est également plus lent que la queue/tête.

Lors de mes tests, les deux versions de tail/heads ont surperformé sed 'NUMq;d' de manière cohérente. Cela correspond aux autres repères qui ont été publiés. Il est difficile de trouver un cas où les queues/têtes étaient vraiment mauvaises. Cela n’est pas surprenant non plus, car il s’agit là d’opérations que vous vous attendriez à être fortement optimisées dans un système Unix moderne.

Pour avoir une idée des différences de performances, voici le nombre que je reçois pour un gros fichier (9.3G):

  • 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 les performances head | tail et tail | head sont, en général, comparables pour des entrées plus petites, et sed est toujours plus lent d'un facteur significatif (environ 5 fois plus).

Pour reproduire mon test, vous pouvez essayer ce qui suit, mais sachez qu'il créera un fichier 9.3G dans le répertoire de travail en cours:

#!/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 exécution sur ma machine (ThinkPad X1 Carbon avec un SSD et 16G de mémoire). Je suppose que lors de la dernière exécution, tout proviendra 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
9
Philipp Claßen

Vous pouvez également utiliser Perl pour cela:

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

La solution la plus rapide pour les gros fichiers est toujours la queue | tête, à condition que les deux distances:

  • du début du fichier à la ligne de départ. Permet de l'appeler S
  • la distance entre la dernière ligne et la fin du fichier. Que ce soit E

sont connus. Ensuite, nous pourrions utiliser ceci:

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

combien faut-il juste compter le nombre de lignes.

Quelques détails supplémentaires dans https://unix.stackexchange.com/a/216614/79743

6
user2350426

Pour faire suite à la réponse très utile de CaffeineConnoisseur en matière d’analyse comparative ... J'étais curieux de savoir à quelle vitesse la méthode "mapfile" était comparée aux autres (car elle n’a pas été testée), j’ai donc essayé moi-même une comparaison de vitesse rapide J'ai bash 4 à portée de main. Jeté dans un test de la méthode "queue | tête" (plutôt que tête | queue) mentionné dans l'un des commentaires sur la réponse en haut pendant que j'y étais, comme les gens chantent ses louanges. Je n'ai rien de la taille du fichier de test utilisé; le meilleur que j'ai pu trouver à court préavis était un fichier généalogique de 14 millions de dollars (longues lignes séparées par des espaces, un peu moins de 12 000 lignes).

Version courte: mapfile semble plus rapide que la méthode de coupe, mais plus lent que tout le reste, alors je l'appellerais un raté. queue | OTOH, on dirait que c’est peut-être le plus rapide, mais avec un fichier de cette taille, la différence n’est pas si 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

J'espère que cela t'aides!

4
Jo Valentine-Cooper

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 pensée. 

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

La vraie solution consiste à avoir un index, par exemple au début du fichier, indiquant les positions où commencent les lignes. Vous pouvez utiliser un format de base de données ou simplement ajouter une table au début du fichier. Vous pouvez également créer un fichier d’index séparé pour accompagner votre fichier texte volumineux. 

par exemple. vous pouvez créer une liste de positions de personnage pour les nouvelles lignes:

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

ensuite, lisez avec tail, qui en fait seeks directement au point approprié dans le fichier!

par exemple. pour obtenir la ligne 1000:

tail -c +$(awk 'NR=1000' file.idx) file.txt | head -1
  • Cela peut ne pas fonctionner avec des caractères de 2 octets/multi-octets, car awk est "conscient du caractère" mais pas la queue.
  • Je n'ai pas testé cela sur un fichier volumineux. 
  • Voir aussi cette réponse .
  • Sinon, divisez votre fichier en fichiers plus petits!
4
Sanjay Manohar

Si vous avez plusieurs lignes délimitées par\n (normalement nouvelle ligne). Vous pouvez aussi utiliser 'cut':

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

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

3
danger89

Beaucoup de bonnes réponses déjà. Je vais personnellement avec awk. Pour plus de commodité, si vous utilisez bash, ajoutez simplement le texte ci-dessous à votre ~/.bash_profile. Et, la prochaine fois que vous vous connecterez (ou si vous générez votre fichier .bash_profile après cette mise à jour), vous aurez une nouvelle "nth" fonction disponible pour diriger vos fichiers. 

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

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

Ensuite, pour l'utiliser, il vous suffit de la diriger. Par exemple.,:

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

2
JJC

En utilisant ce que d’autres ont mentionné, je voulais que cela soit une fonction rapide et utile dans mon shell bash.

Créer un fichier: ~/.functions

Ajoutez-y le contenu:

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

Ajoutez ensuite ceci à votre ~/.bash_profile:

source ~/.functions

Maintenant, lorsque vous ouvrez une nouvelle fenêtre bash, vous pouvez simplement appeler la fonction ainsi:

getline 441 myfile.txt

1
Mark Shust

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, l'indicateur '-e' sert à ajouter un script à la commande à exécuter.

1
aliasav

J'ai mis certaines des réponses ci-dessus dans un court script bash que vous pouvez mettre dans un fichier appelé get.sh et un lien vers /usr/local/bin/get (ou tout autre nom de votre choix).

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

Assurez-vous qu'il est exécutable avec 

$ chmod +x get

Associez-le pour le rendre disponible sur la PATH avec

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

Profitez de façon responsable!

P

0
polarise