Bash herramienta para obtener nth línea de un archivo

¿Hay una manera “canónica” de hacer eso? He estado usando head -n | tail -1 head -n | tail -1 que hace el truco, pero me he estado preguntando si hay una herramienta Bash que extraiga específicamente una línea (o un rango de líneas) de un archivo.

Por “canónico” me refiero a un progtwig cuya función principal es hacer eso.

head y la tubería con la tail serán lentas para un archivo enorme. Sugeriría sed así:

 sed 'NUMq;d' file 

Donde NUM es el número de la línea que desea imprimir; entonces, por ejemplo, sed '10q;d' file para imprimir la décima línea de file .

Explicación:

NUMq saldrá inmediatamente cuando el número de línea sea NUM .

d eliminará la línea en lugar de imprimirla; esto está inhibido en la última línea porque q hace que el rest del guión se omita al salir.

Si tiene NUM en una variable, querrá usar comillas dobles en lugar de una sola:

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

imprimirá la 2da línea

 sed -n '2011p' < file.txt 

2011a línea

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

línea 10 hasta la línea 33

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

1ª y 3ª línea

y así...

Para agregar líneas con sed, puedes verificar esto:

sed: inserta una línea en una posición determinada

Tengo una situación única en la que puedo comparar las soluciones propuestas en esta página, por lo que estoy escribiendo esta respuesta como una consolidación de las soluciones propuestas con tiempos de ejecución incluidos para cada una.

Preparar

Tengo un archivo de datos de texto ASCII de 3.261 gigabytes con un par de clave-valor por fila. El archivo contiene 3,339,550,320 filas en total y desafía la apertura en cualquier editor que haya intentado, incluido mi go-to Vim. Necesito subconjuntar este archivo para investigar algunos de los valores que descubrí que comienzan alrededor de la fila ~ 500,000,000.

Porque el archivo tiene tantas filas:

  • Necesito extraer solo un subconjunto de las filas para hacer algo útil con los datos.
  • Leer cada fila que conduce a los valores que me interesan tomará mucho tiempo.
  • Si la solución lee más allá de las filas que me interesan y continúa leyendo el rest del archivo, perderá tiempo leyendo casi 3 mil millones de filas irrelevantes y tardará 6 veces más de lo necesario.

Mi mejor escenario es una solución que extrae una sola línea del archivo sin leer ninguna de las otras filas en el archivo, pero no puedo pensar en cómo lograr esto en Bash.

Para los propósitos de mi cordura, no voy a tratar de leer las 500,000,000 líneas completas que necesitaría para mi propio problema. En cambio, intentaré extraer la fila 50,000,000 de 3,339,550,320 (lo que significa que leer el archivo completo tomará 60 veces más de lo necesario).

Usaré el time incorporado para comparar cada comando.

Base

Primero veamos cómo la solución de la tail la head :

 $ time head -50000000 myfile.ascii | tail -1 pgm_icnt = 0 real 1m15.321s 

La línea de base para la fila 50 millones es 00: 01: 15.321, si hubiera ido directamente a la fila 500 millones, probablemente sería ~ 12.5 minutos.

cortar

Tengo dudas sobre esto, pero vale la pena intentarlo:

 $ time cut -f50000000 -d$'\n' myfile.ascii pgm_icnt = 0 real 5m12.156s 

Este tomó 00: 05: 12.156 para ejecutarse, ¡que es mucho más lento que la línea de base! No estoy seguro de si se lee todo el archivo o solo hasta la línea de 50 millones antes de parar, pero a pesar de esto no parece una solución viable para el problema.

AWK

Solo ejecuté la solución con la exit porque no iba a esperar a que se ejecutara el archivo completo:

 $ time awk 'NR == 50000000 {print; exit}' myfile.ascii pgm_icnt = 0 real 1m16.583s 

Este código se ejecutó en 00: 01: 16.583, que es solo ~ 1 segundo más lento, pero aún no es una mejora en la línea de base. A este ritmo, si se hubiera excluido el comando de salida, probablemente habría tomado alrededor de ~ 76 minutos leer el archivo completo.

Perl

Ejecuté la solución existente de Perl también:

 $ time perl -wnl -e '$.== 50000000 && print && exit;' myfile.ascii pgm_icnt = 0 real 1m13.146s 

Este código se ejecutó en 00: 01: 13.146, que es ~ 2 segundos más rápido que la línea de base. Si lo hubiera ejecutado en los 500,000,000 completos, probablemente tomaría ~ 12 minutos.

sed

La respuesta más importante en el foro, aquí está mi resultado:

 $ time sed "50000000q;d" myfile.ascii pgm_icnt = 0 real 1m12.705s 

Este código se ejecutó en 00: 01: 12.705, que es 3 segundos más rápido que la línea base, y ~ 0.4 segundos más rápido que Perl. Si lo hubiese ejecutado en las 500,000,000 hileras completas, probablemente hubiera tomado ~ 12 minutos.

mapfile

Tengo bash 3.1 y, por lo tanto, no puedo probar la solución mapfile.

Conclusión

Parece que, en su mayor parte, es difícil mejorar la solución de la tail la head . En el mejor de los casos, la solución sed proporciona un ~ 3% de aumento en la eficiencia.

(porcentajes calculados con la fórmula % = (runtime/baseline - 1) * 100 )

Fila 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

Fila 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

Fila 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

Con awk es bastante rápido:

 awk 'NR == num_line' file 

Cuando esto es cierto, se realiza el comportamiento predeterminado de awk : {print $0} .


Versiones alternativas

Si su archivo es enorme, será mejor que exit después de leer la línea requerida. De esta forma ahorras tiempo de CPU.

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

Si quiere dar el número de línea de una variable bash, puede usar:

 awk 'NR == n' n=$num file awk -vn=$num 'NR == n' file # equivalent 

Wow, todas las posibilidades!

Prueba esto:

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

o uno de estos dependiendo de su versión de 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 

( Puede que tengas que probar el nawk o gawk ).

¿Hay alguna herramienta que solo imprima esa línea en particular? No es una de las herramientas estándar. Sin embargo, sed es probablemente el más cercano y el más simple de usar.

 # print line number 52 sed '52!d' file 

Útiles guiones de una línea para sed

Esta pregunta está etiquetada Bash, aquí está la forma Bash (≥4) de hacer: use mapfile con la mapfile -s (omisión) y -n (conteo).

Si necesita obtener la línea 42 de un file :

 mapfile -s 41 -n 1 ary < file 

En este punto, tendrá una matriz cuyos campos contengan las líneas de file (incluida la línea nueva posterior), donde nos hayamos saltado las primeras 41 líneas ( -s 41 ), y parado después de leer una línea ( -n 1 ). Entonces esa es realmente la línea 42. Para imprimirlo:

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

Si necesita un rango de líneas, digamos el rango 42-666 (inclusive), y diga que no quiere hacer las matemáticas usted mismo, e imprima en stdout:

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

Si necesita procesar estas líneas también, no es realmente conveniente almacenar la nueva línea final. En este caso, use la opción -t (recorte):

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

Puede hacer que una función lo haga por usted:

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

Sin comandos externos, ¡solo Bash builtins!

También puede usar sed print y salir:

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

De acuerdo con mis pruebas, en términos de rendimiento y legibilidad mi recomendación es:

tail -n+N | head -1

N es el número de línea que desea. Por ejemplo, tail -n+7 input.txt | head -1 tail -n+7 input.txt | head -1 imprimirá la séptima línea del archivo.

tail -n+N imprimirá todo comenzando desde la línea N , y head -1 hará que se detenga después de una línea.


La head -N | tail -1 alternativa head -N | tail -1 head -N | tail -1 es tal vez un poco más legible. Por ejemplo, esto imprimirá la séptima línea:

head -7 input.txt | tail -1

Cuando se trata de rendimiento, no hay mucha diferencia para tamaños más pequeños, pero será superado por la tail | head tail | head (desde arriba) cuando los archivos se vuelven enormes.

El sed 'NUMq;d' votado sed 'NUMq;d' es interesante de saber, pero yo diría que será comprendido por menos gente fuera de la caja que la solución de cabeza / cola y también es más lento que la cola / cabeza.

En mis pruebas, ambas versiones de colas / cabezas superaron a sed 'NUMq;d' consistentemente. Eso está en consonancia con los otros puntos de referencia que se publicaron. Es difícil encontrar un caso en el que las colas / cabezas fueran realmente malas. Tampoco es sorprendente, ya que estas son operaciones que esperarías estar muy optimizadas en un sistema Unix moderno.

Para tener una idea acerca de las diferencias de rendimiento, estas son las cifras que obtengo de un archivo enorme (9.3G):

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

Los resultados pueden diferir, pero el head | tail rendimiento head | tail head | tail y tail | head tail | head es, en general, comparable para entradas más pequeñas, y sed es siempre más lento por un factor significativo (alrededor de 5x o menos).

Para reproducir mi punto de referencia, puede intentar lo siguiente, pero tenga en cuenta que creará un archivo 9.3G en el directorio de trabajo actual:

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

Aquí está la salida de una ejecución en mi máquina (ThinkPad X1 Carbon con SSD y 16G de memoria). Supongo que en la ejecución final todo vendrá de la memoria caché, no del disco:

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

También puedes usar Perl para esto:

 perl -wnl -e '$.== NUM && print && exit;' some.file 

La solución más rápida para archivos grandes siempre es tail | head, siempre que las dos distancias:

  • desde el inicio del archivo hasta la línea de partida. Vamos a llamarlo S
  • la distancia desde la última línea hasta el final del archivo. Ya sea E

son conocidos. Entonces, podríamos usar esto:

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

Howmany es solo el recuento de las líneas requeridas.

Más detalles en https://unix.stackexchange.com/a/216614/79743

Como seguimiento de la respuesta de benchmarking muy útil de CaffeineConnoisseur … Tenía curiosidad sobre qué tan rápido se comparó el método ‘mapfile’ con otros (ya que no se probó), así que probé una comparación de velocidad rápida y sucia como Tengo bash 4 a mano. Lancé una prueba del método “tail | head” (en lugar de head | tail) mencionado en uno de los comentarios en la respuesta principal mientras estaba en ello, mientras la gente canta sus alabanzas. No tengo casi el tamaño del archivo de prueba utilizado; lo mejor que pude encontrar a corto plazo fue un archivo de pedigrí de 14M (líneas largas que están separadas por espacios en blanco, justo por debajo de 12000 líneas).

Versión corta: mapfile parece más rápido que el método de corte, pero más lento que todo lo demás, así que lo llamaría un fracaso. cola | head, OTOH, parece que podría ser el más rápido, aunque con un archivo de este tamaño la diferencia no es tan sustancial en comparación con 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 

¡Espero que esto ayude!

Si tiene varias líneas delimitadas por \ n (normalmente nueva línea). También puedes usar ‘cortar’:

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

Obtendrá la 2da línea del archivo. -f3 te da la tercera línea.

Todas las respuestas anteriores responden directamente a la pregunta. Pero aquí hay una solución menos directa, pero una idea potencialmente más importante, para provocar el pensamiento.

Como las longitudes de línea son arbitrarias, todos los bytes del archivo antes de la enésima línea deben leerse. Si tiene un archivo enorme o necesita repetir esta tarea muchas veces, y este proceso lleva mucho tiempo, entonces, en primer lugar, debe considerar seriamente si debe almacenar sus datos de una manera diferente.

La solución real es tener un índice, por ejemplo, al comienzo del archivo, que indique las posiciones donde comienzan las líneas. Puede usar un formato de base de datos, o simplemente agregar una tabla al comienzo del archivo. También puede crear un archivo de índice separado para acompañar su archivo de texto grande.

por ejemplo, puedes crear una lista de posiciones de personaje para nuevas líneas:

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

luego, lea con tail , que realmente seek s directamente al punto apropiado en el archivo!

por ejemplo, para obtener la línea 1000:

 tail -c +$(awk 'NR=1000' file.idx) file.txt | head -1 
  • Esto puede no funcionar con caracteres de 2 bytes / multibyte, ya que awk es “consciente de los caracteres” pero la cola no.
  • No he probado esto en un archivo grande.
  • También vea esta respuesta .
  • Alternativamente, divida su archivo en archivos más pequeños.

Una de las posibles maneras:

 sed -n 'NUM{p;q}' 

Tenga en cuenta que sin el comando q , si el archivo es grande, sed continúa funcionando, lo que ralentiza el cálculo.

Muchas buenas respuestas ya. Yo personalmente voy con awk. Para su comodidad, si usa bash, simplemente agregue lo siguiente a su ~/.bash_profile . Y, la próxima vez que inicie sesión (o si obtiene su archivo .bash_profile después de esta actualización), tendrá una nueva y nítida “enésima función disponible para canalizar sus archivos.

Ejecute esto o póngalo en su ~ / .bash_profile (si usa bash) y vuelva a abrir bash (o ejecute source ~/.bach_profile )

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

Luego, para usarlo, simplemente páselo. P.ej,:

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

Para imprimir n-ésima línea usando sed con una variable como número de línea:

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

Aquí el indicador ‘-e’ es para agregar script al comando que se ejecutará.

Usando lo que otros mencionaron, quería que fuera una función rápida y excelente en mi shell bash.

Crea un archivo: ~/.functions

Añádele los contenidos:

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

A continuación, agregue esto a su ~/.bash_profile :

source ~/.functions

Ahora cuando abres una nueva ventana bash, puedes llamar a la función de la siguiente manera:

getline 441 myfile.txt

 echo  | head  

Donde n es el número de línea que queremos imprimir.