Grep desde el final de un archivo hasta el principio

39

Tengo un archivo con aproximadamente 30.000.000 de líneas (contabilidad de radio) y necesito encontrar la última coincidencia de un patrón dado.

El comando:

tac accounting.log | grep $pattern

da lo que necesito, pero es demasiado lento porque el sistema operativo primero tiene que leer todo el archivo y luego enviarlo a la tubería.

Entonces, necesito algo rápido que pueda leer el archivo desde la última línea hasta la primera.

Hábner Costa
fuente

Respuestas:

44

tacsolo ayuda si también usas grep -m 1(suponiendo GNU grep) para grepdetenerte después del primer partido:

tac accounting.log | grep -m 1 foo

De man grep:

   -m NUM, --max-count=NUM
          Stop reading a file after NUM matching lines.  

En el ejemplo de su pregunta, tanto tacy grepnecesidad de procesar el archivo completo por lo que usar taces una especie de sentido.

Entonces, a menos que lo use grep -m, no lo use tacen absoluto, solo analice la salida de greppara obtener la última coincidencia:

grep foo accounting.log | tail -n 1 

Otro enfoque sería utilizar Perl o cualquier otro lenguaje de script. Por ejemplo (donde $pattern=foo):

perl -ne '$l=$_ if /foo/; END{print $l}' file

o

awk '/foo/{k=$0}END{print k}' file
terdon
fuente
1
Estoy usando tac porque necesito encontrar la última coincidencia de un patrón dado. Usando su sugerencia "grep -m1", el tiempo de ejecución va de 0m0.597s a 0m0.007s \ o /. ¡Gracias a todos!
Hábner Costa
1
@ HábnerCosta de nada. Entiendo por qué lo está usando tac, mi punto es que no ayuda a menos que también lo use, -mya que el archivo aún necesita ser leído por dos programas. De lo contrario, podría buscar todas las ocurrencias y quedarse solo con la última como hago con tail -n 1.
terdon
66
¿Por qué dice "tac [...] necesita procesar todo el archivo"? Lo primero que hace tac es buscar hasta el final del archivo y leer un bloque desde el final. Puede verificar esto usted mismo con strace (1). Cuando se combina con grep -m, debería ser bastante eficiente.
camh
1
@camh cuando se combina con grep -mél es. El OP no estaba usando, -masí que grep y tac estaban procesando todo.
terdon
¿Podría por favor ampliar el significado de la awklínea?
Sopalajo de Arrierez
12

La razón por la cual

tac file | grep foo | head -n 1

no se detiene en el primer partido es debido al almacenamiento en búfer.

Normalmente, head -n 1sale después de leer una línea. Por greplo tanto, debe obtener un SIGPIPE y salir tan pronto como escriba su segunda línea.

Pero lo que sucede es que debido a que su salida no va a una terminal, la grepamortigua. Es decir, no lo está escribiendo hasta que se haya acumulado lo suficiente (4096 bytes en mi prueba con GNU grep).

Lo que eso significa es que grepno saldrá antes de que haya escrito 8192 bytes de datos, por lo que probablemente bastantes líneas.

Con GNU grep, puede hacer que salga antes usando --line-bufferedel comando que le dice que escriba líneas tan pronto como se encuentren, independientemente de si va a un terminal o no. Entonces grepsaldría sobre la segunda línea que encuentra.

Pero de greptodos modos, con GNU , puede usar -m 1en su lugar como ha demostrado @terdon, que es mejor ya que sale en la primera coincidencia.

Si tu grepno es el GNU grep, entonces puedes usar sedo en su awklugar. Pero al tac ser un comando GNU, dudo que encuentres un sistema con tacdonde grepno sea GNU grep.

tac file | sed "/$pattern/!d;q"                             # BRE
tac file | P=$pattern awk '$0 ~ ENVIRON["P"] {print; exit}' # ERE

Algunos sistemas tienen tail -rque hacer lo mismo que GNU tac.

Tenga en cuenta que, para archivos normales (buscables), tacy tail -rson eficientes porque leen los archivos al revés, no solo leen el archivo completamente en la memoria antes de imprimirlo hacia atrás (como lo haría el enfoque sed de @ slm o tacen archivos no regulares) .

En sistemas donde ni tacni tail -restán disponibles, las únicas opciones son implementar la lectura hacia atrás a mano con lenguajes de programación como perlo usar:

grep -e "$pattern" file | tail -n1

O:

sed "/$pattern/h;$!d;g" file

Pero eso significa encontrar todas las coincidencias y solo imprimir la última.

Stéphane Chazelas
fuente
4

Aquí hay una posible solución que encontrará la ubicación de la primera aparición del patrón desde la última:

tac -s "$pattern" -r accounting.log | head -n 1

Esto hace uso de los interruptores -sy -rde los taccuales son los siguientes:

-s, --separator=STRING
use STRING as the separator instead of newline

-r, --regex
interpret the separator as a regular expression
mkc
fuente
Excepto que perderá todo lo que se encuentre entre el inicio de la línea y el patrón.
ychaouche el
2

Usando sed

Mostrando algunos métodos alternativos a la excelente respuesta de @ Terdon usando sed:

$ sed '1!G;h;$!d' file | grep -m 1 $pattern
$ sed -n '1!G;h;$p' file | grep -m 1 $pattern

Ejemplos

$ seq 10 > file

$ sed '1!G;h;$!d' file | grep -m 1 5
5

$ sed -n '1!G;h;$p' file | grep -m 1 5
5

Usando Perl

Como beneficio adicional, aquí hay una notación un poco más fácil de recordar en Perl:

$ perl -e 'print reverse <>' file | grep -m 1 $pattern

Ejemplo

$ perl -e 'print reverse <>' file | grep -m 1 5
5
slm
fuente
1
Es sedprobable que (especialmente el uno) sea varios órdenes de magnitud más lento que grep 5 | tail -n1o sed '/5/h;$!d;g'. Potencialmente, también utilizará mucha memoria. No es mucho más portátil ya que todavía estás usando GNU grep -m.
Stéphane Chazelas