línea de gato X a línea Y en un archivo enorme

132

Decir que tengo un archivo de texto grande (> 2 GB) y sólo quiero que catlas líneas Xa Y(por ejemplo, 57.89 millón a 57,890,010).

Por lo que entiendo, puedo hacer esto headentrando tailo viceversa, es decir

head -A /path/to/file | tail -B

o alternativamente

tail -C /path/to/file | head -D

donde A, B, Cy Dpuede ser calculado a partir del número de líneas en el archivo, Xy Y.

Pero hay dos problemas con este enfoque:

  1. Usted tiene que calcular A, B, Cy D.
  2. Los comandos podrían pipeentre sí muchas más líneas de las que estoy interesado en leer (por ejemplo, si estoy leyendo solo unas pocas líneas en medio de un archivo enorme)

¿Hay alguna manera de que el shell simplemente funcione y genere las líneas que quiero? (mientras proporciona solo Xy Y)?

Amelio Vazquez-Reina
fuente
1
FYI, comparación de prueba de velocidad real de 6 métodos agregados a mi respuesta.
Kevin

Respuestas:

119

Sugiero la sedsolución, pero en aras de la integridad,

awk 'NR >= 57890000 && NR <= 57890010' /path/to/file

Para cortar después de la última línea:

awk 'NR < 57890000 { next } { print } NR == 57890010 { exit }' /path/to/file

Prueba de velocidad:

  • Archivo de 100,000,000 líneas generado por seq 100000000 > test.in
  • Líneas de lectura 50,000,000-50,000,010
  • Pruebas sin ningún orden en particular.
  • realtiempo según lo informado por bash's incorporadotime
 4.373  4.418  4.395    tail -n+50000000 test.in | head -n10
 5.210  5.179  6.181    sed -n '50000000,50000010p;57890010q' test.in
 5.525  5.475  5.488    head -n50000010 test.in | tail -n10
 8.497  8.352  8.438    sed -n '50000000,50000010p' test.in
22.826 23.154 23.195    tail -n50000001 test.in | head -n10
25.694 25.908 27.638    ed -s test.in <<<"50000000,50000010p"
31.348 28.140 30.574    awk 'NR<57890000{next}1;NR==57890010{exit}' test.in
51.359 50.919 51.127    awk 'NR >= 57890000 && NR <= 57890010' test.in

Estos no son puntos de referencia precisos, pero la diferencia es clara y lo suficientemente repetible * como para dar una buena idea de la velocidad relativa de cada uno de estos comandos.

*: Excepto entre los dos primeros, sed -n p;qy head|tail, que parecen ser esencialmente lo mismo.

Kevin
fuente
11
Por curiosidad: ¿cómo ha vaciado el caché del disco entre pruebas?
Paweł Rumian
2
¿Qué pasa tail -n +50000000 test.in | head -n10, que a diferencia tail -n-50000000 test.in | head -n10daría el resultado correcto?
Gilles
44
Ok, fui e hice algunos puntos de referencia. tail | head es mucho más rápido que sed, la diferencia es mucho más de lo que esperaba.
Gilles
3
@ Gilles tienes razón, mi mal. tail+|heades más rápido en un 10-15% que sed, he agregado ese punto de referencia.
Kevin
1
Me doy cuenta de que la pregunta pide líneas, pero si usa el -cpara omitir caracteres, tail+|heades instantánea. Por supuesto, no puede decir "50000000" y puede que tenga que buscar manualmente el inicio de la sección que está buscando.
Danny Kirchmeier
51

Si desea líneas X a Y inclusive (comenzando la numeración en 1), use

tail -n +$X /path/to/file | head -n $((Y-X+1))

tailleerá y descartará las primeras líneas X-1 (no hay forma de evitarlo), luego leerá e imprimirá las siguientes líneas. headleerá e imprimirá el número de líneas solicitado, luego saldrá. Cuando headsale, tailrecibe una señal SIGPIPE y muere, por lo que no habrá leído más del tamaño de un búfer (generalmente unos pocos kilobytes) de líneas del archivo de entrada.

Alternativamente, como sugirió gorkypl , use sed:

sed -n -e "$X,$Y p" -e "$Y q" /path/to/file

Sin embargo, la solución sed es significativamente más lenta (al menos para las utilidades GNU y Busybox; sed podría ser más competitiva si extrae una gran parte del archivo en un sistema operativo donde la tubería es lenta y sed es rápida). Aquí hay puntos de referencia rápidos en Linux; los datos fueron generados por seq 100000000 >/tmp/a, el entorno es Linux / amd64, /tmpes tmpfs y la máquina está inactiva y no se intercambia.

real  user  sys    command
 0.47  0.32  0.12  </tmp/a tail -n +50000001 | head -n 10 #GNU
 0.86  0.64  0.21  </tmp/a tail -n +50000001 | head -n 10 #BusyBox
 3.57  3.41  0.14  sed -n -e '50000000,50000010 p' -e '50000010q' /tmp/a #GNU
11.91 11.68  0.14  sed -n -e '50000000,50000010 p' -e '50000010q' /tmp/a #BusyBox
 1.04  0.60  0.46  </tmp/a tail -n +50000001 | head -n 40000001 >/dev/null #GNU
 7.12  6.58  0.55  </tmp/a tail -n +50000001 | head -n 40000001 >/dev/null #BusyBox
 9.95  9.54  0.28  sed -n -e '50000000,90000000 p' -e '90000000q' /tmp/a >/dev/null #GNU
23.76 23.13  0.31  sed -n -e '50000000,90000000 p' -e '90000000q' /tmp/a >/dev/null #BusyBox

Si conoce el rango de bytes con el que desea trabajar, puede extraerlo más rápido saltando directamente a la posición inicial. Pero para las líneas, debe leer desde el principio y contar las nuevas líneas. Para extraer bloques de x inclusive a y exclusivo a partir de 0, con un tamaño de bloque de b:

dd bs=$b seek=$x count=$((y-x)) </path/to/file
Gilles
fuente
1
¿Estás seguro de que no hay almacenamiento en caché en el medio? Las diferencias entre tail | head y sed me parecen demasiado grandes.
Paweł Rumian
@gorkypl Hice varias medidas y los tiempos fueron comparables. Como escribí, todo esto está sucediendo en la RAM (todo está en la caché).
Gilles
1
@Gilles tail will read and discard the first X-1 lineparece evitarse cuando se da el número de líneas desde el final. En tal caso, la cola parece leer hacia atrás desde el final de acuerdo con los tiempos de ejecución. Por favor lea: http://unix.stackexchange.com/a/216614/79743.
1
@BinaryZebra Sí, si la entrada es un archivo normal, algunas implementaciones de tail(incluida GNU tail) tienen heurísticas para leer desde el final. Eso mejora la tail | headsolución en comparación con otros métodos.
Gilles
22

El head | tailenfoque es una de las mejores y más "idiomáticas" formas de hacer esto:

X=57890000
Y=57890010
< infile.txt head -n "$Y" | tail -n +"$X"

Como señaló Gilles en los comentarios, una forma más rápida es

< infile.txt tail -n +"$X" | head -n "$((Y - X))"

La razón por la que esto es más rápido es que las primeras líneas X - 1 no necesitan pasar por la tubería en comparación con el head | tailenfoque.

Su pregunta como formulada es un poco engañosa y probablemente explica algunas de sus dudas infundadas hacia este enfoque.

  • Usted dice que usted tiene que calcular A, B, C, Dpero como se puede ver, no es necesario el número de líneas del archivo y como máximo 1 cálculo es necesario, que la cáscara puede hacer por usted de todos modos.

  • Le preocupa que la tubería lea más líneas de las necesarias. De hecho, esto no es cierto: tail | heades lo más eficiente posible en términos de E / S de archivo. Primero, considere la cantidad mínima de trabajo necesario: para encontrar la línea X 'th en un archivo, la única forma general de hacerlo es leer cada byte y detenerse cuando cuenta X símbolos de nueva línea ya que no hay forma de adivinar el archivo desplazamiento de la línea X 'th. Una vez que llegue a la línea * X *, debe leer todas las líneas para imprimirlas, deteniéndose en la línea Y '. Por lo tanto, ningún enfoque puede salirse con la lectura de menos de líneas Y Ahora, head -n $Ylee no más que Ylíneas (redondeadas a la unidad de memoria intermedia más cercana, pero las memorias intermedias si se usan correctamente mejoran el rendimiento, por lo que no hay que preocuparse por esa sobrecarga). Además, tailno leerá más que head, por lo tanto, hemos demostrado que head | taillee la menor cantidad posible de líneas (de nuevo, además de un almacenamiento intermedio insignificante que estamos ignorando). La única ventaja de eficiencia de un enfoque de herramienta única que no utiliza tuberías es menos procesos (y, por lo tanto, menos gastos generales).

jw013
fuente
1
Nunca antes había visto la redirección ir primero en la línea. Fresco, hace que la tubería fluya más clara.
clacke
14

La forma más ortodoxa (pero no la más rápida, como señaló Gilles anteriormente) sería utilizarla sed.

En tu caso:

X=57890000
Y=57890010
sed -n -e "$X,$Y p" -e "$Y q" filename

La -nopción implica que solo las líneas relevantes se imprimen en stdout.

La p al final del número de la línea de llegada significa imprimir líneas en un rango dado. La q en la segunda parte del script ahorra algo de tiempo al omitir el resto del archivo.

Paweł Rumian
fuente
1
Que esperaba sedy tail | headque se acerca a la par, pero resulta que tail | heades significativamente más rápido (véase mi respuesta ).
Gilles
1
No sé, por lo que he leído, tail/ headse consideran más "ortodoxa", ya que el recorte de los extremos de un archivo es precisamente lo que están hechos para. En esos materiales, sedsolo parece ingresar a la imagen cuando se requieren sustituciones, y se expulsa rápidamente de la imagen cuando comienza a suceder algo mucho más complejo, ya que su sintaxis para tareas complejas es mucho peor que AWK, que luego se hace cargo .
underscore_d
7

Si conocemos el rango para seleccionar, desde la primera línea: lStarthasta la última línea: lEndpodríamos calcular:

lCount="$((lEnd-lStart+1))"

Si conocemos la cantidad total de líneas: lAlltambién podríamos calcular la distancia hasta el final del archivo:

toEnd="$((lAll-lStart+1))"

Entonces sabremos ambos:

"how far from the start"            ($lStart) and
"how far from the end of the file"  ($toEnd).

Elegir el más pequeño de todos: tailnumbercomo este:

tailnumber="$toEnd"; (( toEnd > lStart )) && tailnumber="+$linestart"

Nos permite usar el comando de ejecución consistentemente más rápido:

tail -n"${tailnumber}" ${thefile} | head -n${lCount}

Tenga en cuenta el signo más ("+") adicional cuando $linestartse selecciona.

La única advertencia es que necesitamos el recuento total de líneas, y eso puede tomar un tiempo adicional para encontrarlo.
Como es habitual con:

linesall="$(wc -l < "$thefile" )"

Algunas veces medidas son:

lStart |500| lEnd |500| lCount |11|
real   user   sys    frac
0.002  0.000  0.000  0.00  | command == tail -n"+500" test.in | head -n1
0.002  0.000  0.000  0.00  | command == tail -n+500 test.in | head -n1
3.230  2.520  0.700  99.68 | command == tail -n99999501 test.in | head -n1
0.001  0.000  0.000  0.00  | command == head -n500 test.in | tail -n1
0.001  0.000  0.000  0.00  | command == sed -n -e "500,500p;500q" test.in
0.002  0.000  0.000  0.00  | command == awk 'NR<'500'{next}1;NR=='500'{exit}' test.in


lStart |50000000| lEnd |50000010| lCount |11|
real   user   sys    frac
0.977  0.644  0.328  99.50 | command == tail -n"+50000000" test.in | head -n11
1.069  0.756  0.308  99.58 | command == tail -n+50000000 test.in | head -n11
1.823  1.512  0.308  99.85 | command == tail -n50000001 test.in | head -n11
1.950  2.396  1.284  188.77| command == head -n50000010 test.in | tail -n11
5.477  5.116  0.348  99.76 | command == sed -n -e "50000000,50000010p;50000010q" test.in
10.124  9.669  0.448  99.92| command == awk 'NR<'50000000'{next}1;NR=='50000010'{exit}' test.in


lStart |99999000| lEnd |99999010| lCount |11|
real   user   sys    frac
0.001  0.000  0.000  0.00  | command == tail -n"1001" test.in | head -n11
1.960  1.292  0.660  99.61 | command == tail -n+99999000 test.in | head -n11
0.001  0.000  0.000  0.00  | command == tail -n1001 test.in | head -n11
4.043  4.704  2.704  183.25| command == head -n99999010 test.in | tail -n11
10.346  9.641  0.692  99.88| command == sed -n -e "99999000,99999010p;99999010q" test.in
21.653  20.873  0.744  99.83 | command == awk 'NR<'99999000'{next}1;NR=='99999010'{exit}' test.in

Tenga en cuenta que los tiempos cambian drásticamente si las líneas seleccionadas están cerca del inicio o cerca del final. Un comando que parece funcionar bien en un lado del archivo, puede ser extremadamente lento en el otro lado del archivo.


fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
terdon
@BinaryZebra: mucho mejor.
mikeserv
0

Hago esto con bastante frecuencia y escribí este guión. No necesito encontrar los números de línea, el script lo hace todo.

#!/bin/bash

# $1: start time
# $2: end time
# $3: log file to read
# $4: output file

# i.e. log_slice.sh 18:33 19:40 /var/log/my.log /var/log/myslice.log

if [[ $# != 4 ]] ; then 
echo 'usage: log_slice.sh <start time> <end time> <log file> <output file>'
echo
exit;
fi

if [ ! -f $3 ] ; then
echo "'$3' doesn't seem to exit."
echo 'exiting.'
exit;
fi

sline=$(grep -n " ${1}" $3|head -1|cut -d: -f1)  #what line number is first occurrance of start time
eline=$(grep -n " ${2}" $3|head -1|cut -d: -f1)  #what line number is first occurrance of end time

linediff="$((eline-sline))"

tail -n+${sline} $3|head -n$linediff > $4
Doolan
fuente
2
Estás respondiendo una pregunta que no se hizo. Su respuesta es del 10% tail|head, que se ha discutido ampliamente en la pregunta y las otras respuestas, y el 90% determina los números de línea donde aparecen las cadenas / patrones especificados, lo que no era parte de la pregunta . PD: siempre debe citar los parámetros y variables de su shell; por ejemplo, "$ 3" y "$ 4".
G-Man