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.
head
et pipe avec tail
seront lents pour un fichier volumineux. Je suggérerais sed
comme ceci:
sed 'NUMq;d' file
Où 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
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:
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:
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 head
tail
:
$ 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 head
tail
. 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
sed
Perl
head|tail
awk
cut
Rangée 500 000 000
sed
Perl
head|tail
awk
cut
Rangée 3,338,559,320
sed
Perl
head|tail
awk
cut
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}
.
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
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.
# print line number 52
sed '52!d' file
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!
Vous pouvez également utiliser sed print et quitter:
sed -n '10{p;q;}' file # print line 10
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 sechead -N | tail -1
: 4.6 secsed Nq;d
: 18.8 secLes 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
Vous pouvez également utiliser Perl pour cela:
Perl -wnl -e '$.== NUM && print && exit;' some.file
La solution la plus rapide pour les gros fichiers est toujours la queue | tête, à condition que les deux distances:
S
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
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!
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 seek
s 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
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.
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
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
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.
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