bash - tutorial - awk variables shell
Herramienta Bash para obtener la lĂnea nth de un archivo (19)
¿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 extrae específicamente una línea (o un rango de líneas) de un archivo.
Por "canónico" me refiero a un programa cuya función principal es hacer eso.
Como seguimiento de la muy útil respuesta de evaluación comparativa de CaffeineConnoisseur ... Tenía curiosidad por lo rápido que se comparaba el método ''mapfile'' con otros (ya que no estaba probado), así que intenté una comparación de velocidad rápida y sucia como Tengo bash 4 a la mano. Hice una prueba del método de "cola | cabeza" (en lugar de cabeza | cola) mencionado en uno de los comentarios en la respuesta principal mientras estaba en ello, ya que la gente está cantando sus alabanzas. No tengo nada del tamaño del archivo de prueba usado; Lo mejor que pude encontrar en poco tiempo fue un archivo de pedigrí de 14M (líneas largas que están separadas por espacios en blanco, justo 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!
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 resulta ser enorme, es mejor que exit
después de leer la línea requerida. De esta manera ahorras tiempo de CPU.
awk ''NR == num_line {print; exit}'' file
Si desea dar el número de línea de una variable bash puede usar:
awk ''NR == n'' n=$num file
awk -v n=$num ''NR == n'' file # equivalent
Esta pregunta se etiqueta como Bash, aquí está la forma de hacer Bash (≥4): usar mapfile
con las mapfile
-s
(saltar) y -n
(contar).
Si necesita obtener la línea 42 de un file
:
mapfile -s 41 -n 1 ary < file
En este punto, tendrá una matriz en los campos que contienen las líneas del file
(incluida la nueva línea final), donde hemos omitido las primeras 41 líneas ( -s 41
) y se detuvo después de leer una línea ( -n 1
). Así que esa es realmente la línea 42. Para imprimirlo:
printf ''%s'' "${ary[0]}"
Si necesita un rango de líneas, diga el rango 42–666 (inclusive), y diga que no quiere hacer las cuentas usted mismo, e imprímalas en la salida estándar:
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
(recortar):
mapfile -t -s $((42-1)) -n $((666-42+1)) ary < file
# do stuff
printf ''%s/n'' "${ary[@]}"
Puedes hacer que una función haga eso por ti:
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[@]}"
}
No hay comandos externos, solo Bash builtins!
La solución más rápida para archivos grandes es siempre la cabeza, siempre que las dos distancias:
- Desde el inicio del archivo hasta la línea de inicio. Llamémoslo
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"
Cuántos son solo el recuento de líneas requeridas.
Algunos detalles más en https://unix.stackexchange.com/a/216614/79743
Muchas buenas respuestas ya. Yo personalmente voy con awk. Para mayor 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 función "nth" para canalizar sus archivos.
Ejecute esto o colóquelo 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 canalizarlo. P.ej,:
$ yes line | cat -n | nth 5 5 line
Para imprimir la línea n usando sed con una variable como número de línea:
a=4
sed -e $a''q:d'' file
Aquí el distintivo ''-e'' es para agregar un script a un comando para ser ejecutado.
Según 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 a partir de 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 quizás 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.
Es interesante saber cuál es el sed ''NUMq;d''
más votado, pero yo diría que lo entenderán menos personas de la caja que la solución cabeza / cola y también es más lento que la cola / cabeza.
En mis pruebas, ambas versiones de cola / cabeza superaron a sed ''NUMq;d''
consistentemente. Eso está en línea con los otros puntos de referencia que se publicaron. Es difícil encontrar un caso donde las colas / cabezas fueran realmente malas. Tampoco es sorprendente, ya que estas son operaciones que usted espera que se optimicen en gran medida en un sistema Unix moderno.
Para tener una idea de las diferencias de rendimiento, este es el número que obtengo para un archivo enorme (9.3G):
-
tail -n+N | head -1
tail -n+N | head -1
: 3.7 segundos -
head -N | tail -1
head -N | tail -1
: 4.6 seg -
sed Nq;d
: 18.8 seg
Los resultados pueden diferir, pero la head | tail
rendimiento head | tail
head | tail
y tail | head
tail | head
es, en general, comparable para insumos más pequeños, y sed
siempre es más lento por un factor significativo (alrededor de 5x o menos).
Para reproducir mi punto de referencia, puede intentar lo siguiente, pero se le advierte 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 un SSD y 16G de memoria). Supongo que en la ejecución final todo vendrá de la 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
Si tienes varias líneas delimitadas por / n (normalmente una línea nueva). Puedes usar ''cortar'' también:
echo "$data" | cut -f2 -d$''/n''
Obtendrá la segunda línea del archivo. -f3
te da la tercera línea.
También puedes usar Perl para esto:
perl -wnl -e ''$.== NUM && print && exit;'' some.file
También puedes usar sed print y dejar de fumar:
sed -n ''10{p;q;}'' file # print line 10
Tengo una situación única en la que puedo comparar las soluciones propuestas en esta página, por lo que escribo 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 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, incluyendo mi acceso a Vim. Necesito subcontratar este archivo para investigar algunos de los valores que he descubierto 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.
- La lectura de cada fila que conduce a los valores que me importan llevará mucho tiempo.
- Si la solución supera las filas que me importan y continúa leyendo el resto del archivo, perderá tiempo leyendo casi 3 mil millones de filas irrelevantes y tomará 6 veces más de lo necesario.
Mi mejor de los casos es una solución que extrae solo una línea del archivo sin leer ninguna de las otras filas del archivo, pero no puedo pensar en cómo lograría esto en Bash.
Para los propósitos de mi cordura, no voy a intentar leer las 500,000,000 de líneas completas que necesitaría para mi propio problema. En su lugar, estaré intentando 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 evaluar cada comando.
Base
Primero veamos cómo la solución de tail
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ían ~ 12.5 minutos.
cortar
Tengo dudas de 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 correr, ¡que es mucho más lento que la línea de base! No estoy seguro de si leyó todo el archivo o solo hasta 50 millones antes de parar, pero a pesar de esto no parece ser una solución viable al 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 esta velocidad, si se hubiera excluido el comando de salida, ¡probablemente hubiera tomado alrededor de ~ 76 minutos leer todo el archivo!
Perl
También corrí la solución Perl existente:
$ 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 ejecutara en el total de 500,000,000 probablemente tomaría unos 12 minutos.
sed
La respuesta principal en la pizarra, 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 de base y ~ 0.4 segundos más rápido que Perl. Si lo hubiera ejecutado en las 500,000,000 filas completas, probablemente hubiera tomado ~ 12 minutos.
archivo de mapas
Tengo bash 3.1 y, por lo tanto, no puedo probar la solución de 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 aumento del ~ 3% en la eficiencia.
(porcentajes calculados con la fórmula % = (runtime/baseline - 1) * 100
)
Fila 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
Fila 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
Fila 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
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.
Dado que las longitudes de las líneas son arbitrarias, todos los bytes del archivo antes de la línea n deben ser leídos. Si tiene un archivo enorme o necesita repetir esta tarea muchas veces, y este proceso lleva mucho tiempo, entonces debería pensar seriamente si debería almacenar sus datos de una manera diferente en primer lugar.
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 inicio del archivo. También puede crear un archivo de índice separado para acompañar su archivo de texto grande.
por ejemplo, puede crear una lista de posiciones de caracteres 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 en realidad 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
- Es posible que esto no funcione con caracteres de 2 bytes / multibyte, ya que awk es "consciente de los caracteres" pero la cola no.
- No he probado esto contra un archivo grande.
- También vea esta respuesta .
- Alternativamente, ¡divide tu archivo en archivos más pequeños!
Una de las formas posibles:
sed -n ''NUM{p;q}''
Tenga en cuenta que sin el comando q
, si el archivo es grande, sed continúa trabajando, lo que ralentiza el cálculo.
Usando lo que otros mencionaron, quería que esta fuera una función rápida y elegante en mi shell bash.
Crear un archivo: ~/.functions
Añade a ello los contenidos:
getline() { line=$1 sed $line''q;d'' $2 }
Luego agrega esto a tu ~/.bash_profile
:
source ~/.functions
Ahora, cuando abre una nueva ventana de bash, puede llamar a la función así:
getline 441 myfile.txt
Wow, todas las posibilidades!
Prueba esto:
sed -n "${lineNum}p" $file
o uno de estos dependiendo de tu 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 tenga 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 fácil de usar.
head
y el tubo con la tail
serán lentos para un archivo enorme. Yo sugeriría que sea así:
sed ''NUMq;d'' file
Donde NUM
es el número de la línea que desea imprimir; así, por ejemplo, sed ''10q;d'' file
para imprimir la décima línea de file
.
Explicación:
NUMq
se NUMq
inmediatamente cuando el número de línea sea NUM
.
d
borrará la línea en lugar de imprimirla; esto se inhibe en la última línea porque q
hace que el resto del script se omita al salir.
Si tiene NUM
en una variable, querrá usar comillas dobles en lugar de una sola:
sed "${NUM}q;d" file
# print line number 52
sed ''52!d'' file
echo <filename> | head <n>
Donde n es el número de línea que queremos imprimir.
sed -n ''2p'' < file.txt
imprimirá 2ª línea
sed -n ''2011p'' < file.txt
Línea 2011
sed -n ''10,33p'' < file.txt
línea 10 hasta línea 33
sed -n ''1p;3p'' < file.txt
1ª y 3ª línea
y así...
Para añadir líneas con sed, puedes verificar esto:
sed: insertar una línea en una posición determinada