Парсинг лог-файлов

Я пытаюсь написать сценарий, чтобы упростить процесс поиска определенной информации в файлах журналов определенных приложений. Так что я подумал, может быть, есть способ преобразовать их в XML-дерево, и я начал с достойного начала... но проблема в том, что файлы журнала приложений представляют собой абсолютный беспорядок, если вы спросите меня.

Некоторые записи просты

2014/04/09 11:27:03 INFO  Some.code.function - Doing stuff

В идеале я хотел бы превратить вышеизложенное во что-то вроде этого

    <Message>
    <Date>2014/04/09</Date>
    <Time>11:48:38</Time>
    <Type>INFO</Type>
    <Source>Some.code.function</Source>
    <Sub>Doing stuff</Sub>
    </Message>

Другие записи примерно такие, где есть дополнительная информация и разрывы строк.

2014/04/09 11:27:04 INFO  Some.code.function - Something happens

changes: 
this stuff happened

Я хотел бы превратить этот последний фрагмент во что-то вроде приведенного выше, но добавить дополнительную информацию в раздел

    <Message>
    <Date>2014/04/09</Date>
    <Time>11:48:38</Time>
    <Type>INFO</Type>
    <Source>Some.code.function</Source>
    <Sub>Doing stuff</Sub>
    <details>changes: 
this stuff happened</details>
    </Message>

а потом другие сообщения, ошибки будут в виде

2014/04/09 11:27:03 ERROR  Some.code.function - Something didn't work right
Log Entry: LONGARSEDGUID
Error Code: E3145
Application: Name
Details:
message information etc etc and more line breaks, this part of the message may add up to an unknown number of lines before the next entry

Этот последний фрагмент я хотел бы преобразовать как последний из приведенных выше примеров, но добавив узлы XML для записи журнала, кода ошибки, приложения и снова, таких деталей, как так.

    <Message>
    <Date>2014/04/09</Date>
    <Time>11:48:38</Time>
    <Type>ERROR  </Type>
    <Source>Some.code.function</Source>
    <Sub>Something didn't work right</Sub>
    <Entry>LONGARSEDGUID</Entry>
    <Code>E3145</Code>
    <Application>Name</Application>
    <details>message information etc etc and more line breaks, this part of the message may add up to an unknown number of lines before the next entry</details>
    </Message>

Теперь я знаю, что Select-String имеет параметр контекста, который позволяет мне выбрать количество строк после отфильтрованной строки, проблема в том, что это не постоянное число.

Я думаю, что регулярное выражение также позволит мне выбрать фрагмент абзаца перед строкой даты, но регулярные выражения не являются моей сильной стороной, и я подумал, что может быть лучший способ, потому что одна константа заключается в том, что новые записи начинаются с строка даты

идея, однако, состоит в том, чтобы либо разбить их на xml или таблицы, а затем оттуда, я надеюсь, это может занять последнее или отфильтровать нерелевантные или повторяющиеся сообщения немного проще

У меня есть образец, который я только что бросил на pastebin после удаления/замены нескольких битов информации из соображений конфиденциальности.

http://pastebin.com/raw.php?i=M9iShyT2


person Serge111    schedule 09.04.2014    source источник
comment
Насколько велики эти файлы? Проще, если файлы достаточно малы, чтобы весь файл мог быть прочитан в память, но вы можете столкнуться с проблемами памяти, если они превысят несколько сотен мегабайт, в зависимости от доступных ресурсов.   -  person mjolinor    schedule 10.04.2014
comment
максимальный размер журнала по умолчанию составляет 5 МБ, и он будет прокручиваться при этом размере и сохранять 5 предыдущих журналов (в отдельных файлах), поэтому максимум 30 МБ, если я хочу передать их все сразу. Некоторые из подробных записей могут содержать целые SQL-запросы или от 5 до 50 (плюс-минус) строк дополнительной информации.   -  person Serge111    schedule 11.04.2014


Ответы (2)


Извините, что немного поздно, я немного завязал с работой (чертова работа, ожидающая, что я буду продуктивным, пока они на их копейке). В итоге я получил что-то похожее на решение Ansgar Wiechers, но отформатировал вещи в объекты и собрал их в массив. Он не управляет XML-файлом, который вы добавили позже, но дает хороший массив объектов для работы с другими записями. Я объясню основную строку RegEx здесь, я прокомментирую там, где это практично.

'(^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) [\d+?] (\w+?) {1 ,2}(.+?) - (.+)$' — это регулярное выражение, определяющее начало новой записи. Я начал объяснять это, но, вероятно, для вас есть лучшие ресурсы для изучения RegEx, чем я, объясняющий это мне. См. ссылку на RegEx101.com для полной разбивки и примеров.

$Records=@() #Create empty array that we will populate with custom objects later
$Event = $Null #make sure nothing in $Event to give script a clean start
Get-Content 'C:\temp\test1.txt' | #Load file, and start looping through it line-by-line.
?{![string]::IsNullOrEmpty($_)}|% { #Filter out blank lines, and then perform the following on each line
  if ($_ -match '(^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) \[\d+?] (\w+?) {1,2}(.+?) - (.+)$') { #New Record Detector line! If it finds this RegEx match, it means we're starting a new record.
    if ($Event) { #If there's already a record in progress, add it to the Array
      $Records+=$Event
    }
    $Event = New-Object PSObject -Property @{ #Create a custom PSObject object with these properties that we just got from that RegEx match
DateStamp = [datetime](get-date $Matches[1]) #We convert the date/time stamp into an actual DateTime object. That way sorting works better, and you can compare it to real dates if needed.
Type = $Matches[2]
Source = $Matches[3]
Message = $Matches[4]}

Хорошо, небольшая пауза для дела здесь. $Matches не определено мной, почему я на него ссылаюсь? . Когда PowerShell получает совпадения из выражения RegEx, он автоматически сохраняет полученные совпадения в $Matches. Таким образом, все группы, которые мы только что сопоставили в скобках, становятся $Matches[1], $Matches[2] и так далее. Да, это массив, и есть $Matches[0], но это вся строка, с которой было сопоставлено, а не только совпадающие группы. Теперь мы возвращаем вас к вашему регулярно запланированному сценарию...

  } else { #End of the 'New Record' section. If it's not a new record if does the following
    if($_ -match "^((?:[^ ^\[])(?:\w| |\.)+?):(.*)$"){ 

RegEx снова совпадает. Он начинается с утверждения, что это должно быть начало строки с символом карата (^). Затем он говорит (в группе без захвата, отмеченной форматом (?:<stuff>), что на самом деле для моих целей просто означает, что он не будет отображаться в $Matches) [^ \[]; это означает, что следующий символ не может быть пробелом или открывающей скобкой (с экранированием с помощью ), просто чтобы ускорить процесс и пропустить эти строки для этой проверки. Если у вас есть вещи в скобках [], а первый символ - карат, это означает «ничего не сопоставлять в этих скобках».

На самом деле я только что изменил эту следующую часть, включив точки, и использовал \w вместо [a-zA-Z0-9], потому что это по сути то же самое, но короче. \w — это «символ слова» в RegEx, который включает в себя буквы, цифры и знак подчеркивания. Я не уверен, почему подчеркивание считается частью слова, но я не устанавливаю правила, я просто играю в игру. Я использовал [a-zA-Z0-9], который соответствует чему-либо между «a» и «z» (нижний регистр), чему-либо между «A» и «Z» (верхний регистр) и чему-либо между «0» и «9». . Рискуя включить символ подчеркивания, \w намного короче и проще.

Затем фактическая часть захвата этого RegEx. У этого есть 2 группы, первая - буквы, цифры, символы подчеркивания, пробелы и точки (экранированные с помощью \, потому что '.' сам по себе соответствует любому символу). Потом двоеточие. Затем вторая группа, то есть все остальное до конца строки.

        $Field = $Matches[1] #Everything before the colon is the name of the field
        $Value = $Matches[2].trim() #everything after the colon is the data in that field
        $Event | Add-Member $Field $Value #Add the Field to $Event as a NoteProperty, with a value of $Value. Those two are actually positional parameters for Add-Member, so we don't have to go and specify what kind of member, specify what the name is, and what the value is. Just Add-Member <[string]name> <value can be a string, array, yeti, whatever... it's not picky>
        } #End of New Field for current record
    else{$Value = $_} #If it didn't find the regex to determine if it is a new field then this is just more data from the last field, so don't change the field, just set it all as data.

    } else { #If it didn't find the regex then this is just more data from the last field, so don't change the field, just set it all as data.the field does not 'not exist') do this:
            $Event.$Field += if(![string]::isNullOrEmpty($Event.$Field)){"`r`n$_"}else{$_}} 

Это длинное объяснение довольно короткого фрагмента кода. На самом деле все, что он делает, это добавляет данные в поле! Это имеет инвертированную (с префиксом !) проверку If, чтобы увидеть, есть ли в текущем поле какие-либо данные, если они есть, или является ли оно в настоящее время Null или Empty. Если он пуст, он добавляет новую строку, а затем добавляет данные $Value. Если у него нет данных, он пропускает бит новой строки и просто добавляет данные.

    }
  }
}
$Records+=$Event #Adds the last event to the array of records.

Извините, я не очень хорошо разбираюсь в XML. Но, по крайней мере, это принесет вам чистые записи.

Редактировать: Хорошо, теперь код помечен, надеюсь, все объяснено достаточно хорошо. Если что-то все еще сбивает с толку, возможно, я могу направить вас на сайт, который объясняет лучше, чем я. Я проверил приведенное выше для вашего примера ввода в PasteBin.

person TheMadTechnician    schedule 09.04.2014
comment
Спасибо за ответ. Не буду врать, мне тяжело читать. Это немного выходит за рамки моих возможностей, и, к сожалению, я не смог заставить его дать мне что-либо с моим образцом файла журнала. Однако я ценю усилия. - person Serge111; 11.04.2014
comment
Закинул на pastebin, если поможет. Возможно, это помогло бы, если бы мои предыдущие примеры были немного точнее. В строках ввода есть [2134], который я совершенно проглядел и не хочу с ним работать. Я отредактировал свою исходную тему с помощью небольшого образца pastebin, вы, вероятно, быстро поймете, почему я пытаюсь сделать их немного более удобочитаемыми. Комментарии в вашем образце кода были бы замечательными, если вы не против помочь. Я действительно ценю усилия до сих пор. - person Serge111; 11.04.2014
comment
Это отлично работает и на 50 строк короче LOL. Кроме одной мелочи. со знаком = получилось вот так 2014/04/09 12:21:04 [201] ИНФОРМАЦИЯ ActionManagers.ActionNwkScan - Сканирование завершено - затраченное время: 23 сек. 2014/04/09 12:21:04 [201] ОШИБКА Устройство - ошибка обновления DeviceID = 29 DataAccess.SqlSqlHelperException: [Sql=... в этот DateStamp: 2014/04/09 12:21:04 Сообщение: сканирование завершено - затраченное время: 23 сек. Источник: ActionManagers.ActionNwkScan Тип: INFO Подробности: 2014/04/09 12:21:04 [201] ОШИБКА Устройство — ошибка при обновлении DeviceID = 29DataAccess.Sql... - person Serge111; 11.04.2014
comment
вздох, это не сработало. Я поместил его в корзину для вставки, чтобы показать pastebin.com/raw.php?i=WCS9y3fv - person Serge111; 11.04.2014
comment
Обновленный ответ с небольшой оптимизацией, исправлением (добавлено . к возможным именам полей) и множеством объяснений. - person TheMadTechnician; 11.04.2014
comment
вау, это действительно помогает. Я преобразовал его в модуль, который я могу использовать сейчас. Это определенно облегчит мою работу теперь, когда мне приходится сортировать их почти ежедневно. Большое спасибо, вы не представляете - person Serge111; 12.04.2014
comment
Если этот ответ дал решение ваших первоначальных вопросов, отметьте его как принятый ответ. Это не только помогает будущим пользователям искать помощь с похожими вопросами, но и немного повышает мою репутацию на сайте =) - person TheMadTechnician; 12.04.2014
comment
ага, еще не проверял. все еще читаю некоторые комментарии и обозначения и играючи в ISE, чтобы помочь моему пониманию, но да, это абсолютно отвечает на мой вопрос, и я считаю, что только что отметил его ответом. - person Serge111; 12.04.2014

Один из возможных способов работы с такими файлами — обрабатывать их построчно. Каждая запись в журнале начинается с временной метки и заканчивается, когда появляется следующая строка, начинающаяся с временной метки, поэтому вы можете сделать что-то вроде этого:

Get-Content 'C:\path\to\your.log' | % {
  if ($_ -match '^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}') {
    if ($logRecord) {
      # If a current log record exists, it is complete now, so it can be added
      # to your XML or whatever, e.g.:

      $logRecord -match '^(\d{4}/\d{2}/\d{2}) (\d{2}:\d{2}:\d{2}) (\S+) ...'

      $message = $xml.CreateElement('Message')

      $date = $xml.CreateElement('Date')
      $date.InnerText = $matches[1]
      $message.AppendChild($date)

      $time = $xml.CreateElement('Time')
      $time.InnerText = $matches[2]
      $message.AppendChild($time)

      $type = $xml.CreateElement('Type')
      $type.InnerText = $matches[3]
      $message.AppendChild($type)

      ...

      $xml.SelectSingleNode('...').AppendChild($message)
    }
    $logRecord = $_          # start new record
  } else {
    $logRecord += "`r`n$_"   # append to current record
  }
}
person Ansgar Wiechers    schedule 09.04.2014
comment
Я нашел что-то в этом роде, и я ищу что-то большее. Я должен был быть более ясным в своем первом посте, который я только что исправил - person Serge111; 10.04.2014
comment
на самом деле, я играю с этим и вижу, что происходит, я думаю, что смогу адаптировать это к тому, что у меня есть. - person Serge111; 10.04.2014
comment
@ Serge111 Вы можете обрабатывать каждую запись журнала в соответствии с вашими потребностями во вложенном условном выражении. Я добавил пример кода. - person Ansgar Wiechers; 10.04.2014
comment
Мне потребовалось некоторое время, чтобы понять это. Это помогло мне переформатировать скобки, чтобы я мог правильно читать, но мне удалось заставить это работать. Я закончил тем, что он передал записи журнала в функцию, с которой я начал изначально. Я все еще добавляю последние штрихи, но это помогло мне разделить записи журнала во что-то, с чем я могу работать. Мое решение может быть не таким элегантным, но оно работает для меня - person Serge111; 11.04.2014