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.
19 réponses
head
et pipe avec tail
seront lents pour un gros fichier. Je suggérerais sed
comme ceci:
sed 'NUMq;d' file
Où 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
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:
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
- 00:01:12.705 (-00:00:02.616 = -3.47%)
sed
- 00:01:13.146 (-00:00:02.175 = -2,89%)
perl
- 00:01:15.321 (+00:00:00.000 = +0.00%)
head|tail
- 00:01:16.583 (+00:00:01.262 = +1.68%)
awk
- 00:05:12.156 (+00:03:56.835 = +314.43%)
cut
ligne 500,000,000
- 00:12:07.050 (-00:00:26.160)
sed
- 00: 12:11.460 (-00:00:21.750)
perl
- 00:12:33.210 (+00:00:00.000)
head|tail
- 00:12:45.830 (+00:00:12.620)
awk
- 00:52:01.560 (+00:40:31.650)
cut
ligne 3,338,559,320
- 01:20:54.599 (-00:03:05.327)
sed
- 01:21:24.045 (-00:02:25.227)
perl
- 01:23:49.273 (+00:00:00.000)
head|tail
- 01:25:13.548 (+00:02:35.735)
awk
- 05:47:23.026 (+04:24:26.246)
cut
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
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.
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!
, Vous pouvez également utilisé sed d'impression et quitter:
sed -n '10{p;q;}' file # print line 10
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
vous pouvez également utiliser Perl pour ce faire:
perl -wnl -e '$.== NUM && print && exit;' some.file
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
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!
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.
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!
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.
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
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.
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
echo <filename> | head <n>
où n est le numéro de ligne que nous voulons imprimer.