¿Cómo puedo usar bash (grep / sed / etc) para tomar una sección de un archivo de registro entre 2 marcas de tiempo?

StackOverflow https://stackoverflow.com/questions/827930

Pregunta

Tengo un conjunto de registros de correo: mail.log mail.log.0 mail.log.1.gz mail.log.2.gz

cada uno de estos archivos contiene líneas ordenadas cronológicamente que comienzan con marcas de tiempo como:

3 de mayo 13:21:12 ...

¿Cómo puedo obtener fácilmente cada entrada de registro después de una determinada fecha / hora y antes de otra fecha / hora usando bash (y herramientas de línea de comando relacionadas) sin comparar cada línea? Tenga en cuenta que mis fechas anteriores y posteriores pueden no coincidir exactamente con ninguna entrada en los archivos de registro.

Me parece que necesito determinar el desplazamiento de la primera línea mayor que la marca de tiempo inicial, y el desplazamiento de la última línea menor que la marca de tiempo final, y cortar esa sección de alguna manera.

¿Fue útil?

Solución 2

Aquí una idea básica de cómo hacerlo:

  1. Examine el sello de fecha en el archivo para ver si es irrelevante
  2. Si podría ser relevante, descomprima si es necesario y examine las primeras y últimas líneas del archivo para ver si contiene el tiempo de inicio o finalización.
  3. Si lo hace, use una función recursiva para determinar si contiene la hora de inicio en la primera o segunda mitad del archivo. Usando una función recursiva, creo que podría encontrar cualquier fecha en un archivo de registro de un millón de líneas con alrededor de 20 comparaciones.
  4. echo los archivos de registro en orden desde el desplazamiento de la primera entrada hasta el desplazamiento de la última entrada (no más comparaciones)

Lo que no sé es: cómo leer mejor la enésima línea de un archivo (qué tan eficiente es usar tail n + ** n | head 1 **?)

¿Alguna ayuda?

Otros consejos

Convierta sus fechas mín. / máx. en " segundos desde la época " ;,

MIN=`date --date="$1" +%s`
MAX=`date --date="$2" +%s`

Convierta las primeras palabras n en cada línea de registro a la misma,

L_DATE=`echo $LINE | awk '{print $1 $2 ... $n}'`
L_DATE=`date --date="$L_DATE" +%s`

Compare y deseche las líneas hasta llegar a MIN ,

if (( $MIN > $L_DATE )) ; then continue ; fi

Compare e imprima líneas hasta llegar a MAX ,

if (( $L_DATE <= $MAX )) ; then echo $LINE ; fi

Salga cuando exceda MAX .

if (( $L_DATE > $MAX )) ; then exit 0 ; fi

El script completo minmaxlog.sh tiene este aspecto,

#!/usr/bin/env bash

MIN=`date --date="$1" +%s`
MAX=`date --date="$2" +%s`

while true ; do
    read LINE
    if [ "$LINE" = "" ] ; then break ; fi

    L_DATE=`echo $LINE | awk '{print $1 " " $2 " " $3 " " $4}'`
    L_DATE=`date --date="$L_DATE" +%s`

    if (( $MIN > $L_DATE  )) ; then continue ; fi
    if (( $L_DATE <= $MAX )) ; then echo $LINE ; fi
    if (( $L_DATE >  $MAX )) ; then break ; fi
done

Lo ejecuté en este archivo minmaxlog.input ,

May 5 12:23:45 2009 first line
May 6 12:23:45 2009 second line
May 7 12:23:45 2009 third line
May 9 12:23:45 2009 fourth line
June 1 12:23:45 2009 fifth line
June 3 12:23:45 2009 sixth line

así,

./minmaxlog.sh "May 6" "May 8" < minmaxlog.input

Tienes que mirar cada línea en el rango que deseas (para saber si está en el rango que deseas), así que supongo que te refieres a no todas las líneas del archivo. Como mínimo, tendrá que mirar cada línea del archivo, incluida la primera fuera de su rango (supongo que las líneas están en orden de fecha / hora).

Este es un patrón bastante simple:

state = preprint
for every line in file:
    if line.date >= startdate:
        state = print
    if line.date > enddate:
        exit for loop
    if state == print:
        print line

Puede escribir esto en awk, Perl, Python, incluso COBOL si debe hacerlo, pero la lógica es siempre la misma.

Ubicar primero los números de línea (con digamos grep) y luego simplemente imprimir a ciegas ese rango de línea no ayudará, ya que grep también tiene que mirar todas las líneas ( todas , no solo hasta el primero fuera del rango, y muy probablemente dos veces , uno para la primera línea y otro para la última).

Si esto es algo que va a hacer con bastante frecuencia, puede considerar cambiar el esfuerzo de 'cada vez que lo haga' a 'una vez, cuando el archivo esté estabilizado'. Un ejemplo sería cargar las líneas del archivo de registro en una base de datos, indexadas por fecha / hora.

Eso lleva un tiempo configurarlo, pero hará que sus consultas sean mucho más rápidas. No estoy abogando necesariamente por una base de datos; probablemente podría lograr el mismo efecto dividiendo los archivos de registro en registros por hora de esta manera:

2009/
  01/
    01/
      0000.log
      0100.log
      : :
      2300.log
    02/
    : :

Luego, durante un tiempo determinado, usted sabe exactamente dónde comenzar y dejar de buscar. El rango 2009/01 / 01-15: 22 a 2009/01 / 05-09: 07 daría como resultado:

  • algunos (el último bit) del archivo 2009/01/01 / 1500.txt .
  • todos los archivos 2009/01/01/1 [6-9] *. txt .
  • todos los archivos 2009/01/01/2 * .txt .
  • todos los archivos 2009/01/0 [2-4] / *. txt .
  • todos los archivos 2009/01/05/0 [0-8] *. txt .
  • algunos (el primer bit) del archivo 2009/01/05 / 0900.txt .

Por supuesto, escribiría un script para devolver esas líneas en lugar de intentar hacerlo manualmente cada vez.

Quizás puedas probar esto:

sed -n "/BEGIN_DATE/,/END_DATE/p" logfile

Puede ser posible en un entorno Bash, pero realmente debería aprovechar las herramientas que tienen más soporte incorporado para trabajar con cadenas y fechas. Por ejemplo, Ruby parece tener la capacidad incorporada de analizar su formato de fecha. Luego puede convertirlo en una marca de tiempo de Unix fácilmente comparable (un número entero positivo que representa los segundos desde la época).

irb> require 'time'
# => true

irb> Time.parse("May 3 13:21:12").to_i
# => 1241371272  

Luego puede escribir fácilmente un script Ruby:

  • Proporcione una fecha de inicio y finalización. Convierta esos a este número de marca de tiempo de Unix.
  • Escanee los archivos de registro línea por línea, convirtiendo la Fecha en su Marca de Tiempo Unix y verifique si está dentro del rango de las fechas de inicio y finalización.

Nota: Convertir a un entero de marca de tiempo de Unix primero es bueno porque comparar enteros es muy fácil y eficiente de hacer.

Usted mencionó "sin comparar cada línea". Va a ser difícil de adivinar en donde en el archivo de registro las entradas comienzan a ser demasiado viejas o demasiado nuevas sin verificar todos los valores intermedios Sin embargo, si de hecho hay una tendencia monotónicamente creciente, sabrá de inmediato cuándo dejar de analizar las líneas, porque tan pronto como la próxima entrada sea demasiado nueva (o antigua, dependiendo del diseño de los datos), puede dejar de buscar. Aún así, existe el problema de encontrar la primera línea en el rango deseado.


Acabo de notar tu edición. Esto es lo que diría:

Si está realmente preocupado por encontrar eficientemente esa entrada inicial y final, entonces podría hacer una búsqueda binaria para cada una. O, si eso parece excesivo o demasiado difícil con las herramientas bash, podría tener una heurística de leer solo el 5% de las líneas (1 de cada 20), para obtener rápidamente una respuesta exacta y luego refinarla si lo desea. Estas son solo algunas sugerencias para mejorar el rendimiento.

Sé que este hilo es antiguo, pero me topé con él después de encontrar recientemente una solución de una línea para mis necesidades:

awk -v ts_start="2018-11-01" -v ts_end="2018-11-15" -F, '$1>=ts_start && $1<ts_end' myfile

En este caso, mi archivo tiene registros con valores separados por comas y la marca de tiempo en el primer campo. Puede usar cualquier formato de marca de tiempo válido para las marcas de tiempo de inicio y finalización, y reemplazar estas variables de shell si lo desea.

Si desea escribir en un nuevo archivo, simplemente use la redirección de salida normal ( > newfile ) adjunta al final de arriba.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top