Python lxml не хватает памяти при использовании iter в цикле for - PullRequest
0 голосов
/ 01 июля 2019

Я пишу решение для извлечения информации из файла. Эти файлы создаются командой Windows Event Utility с помощью другого сценария (я не вызываю, просто получаю файл для анализа):

wevtutil qe Application /q:"*[System[Provider[@Name='NameOfTheSourceApplication']]]" >> %FILE%

Эта команда сохраняет все выходные данные, относящиеся к исходному приложению, запрошенные в файл дампа, в конце концов, для каждой события в каждой строке есть XML. Все, что меня волнует, это EventData и TimeCreated SystemTime.

Пример вывода:

<?xml version="1.0" encoding="UTF-8"?>
<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
   <System>
      <Provider Name="" />
      <EventID Qualifiers="">0</EventID>
      <Level>4</Level>
      <Task>0</Task>
      <Keywords />
      <TimeCreated SystemTime="2018-10-02T11:19:41.000000000Z" />
      <EventRecordID />
      <Channel>Application</Channel>
      <Computer />
      <Security />
   </System>
   <EventData>
      DATA
      <Data />
   </EventData>
</Event>

Когда дамп файла закончен, файл может быть довольно большим (больше 6-7 ГБ). Поэтому я использую утилиту Linux iconv, чтобы изменить кодировку исходного файла с UTF-16/UCS2-LE (кодировка по умолчанию wevutil) на UTF-8, новая кодировка уменьшает почти половину размера файла. Затем я использую рецепт grouper в сочетании с простой функцией разделения файлов, чтобы разделить большой дамп-файл на файлы меньшего размера:

def grouper(n, iterable, fillvalue=None):
   """Collect data into fixed-length chunks or blocks"""
   # grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx
   args = [iter(iterable)] * n
   return zlg(fillvalue=fillvalue, *args)

def splitter(fileobj,outName,ranoutName,rencode,wencode):
    with open(fileobj,"r",encoding='UTF-8',errors='replace') as f:
        for i, g in enumerate(grouper(n, f, fillvalue=''), 1):
            with open('{0}_{1}.xml'.format(i,outName), 'w',encoding=wencode) as fout:
                fout.writelines(g)
                print("Splitting file : %s" % (fileobj))

Поскольку эти файлы на самом деле не являются файлами XML, но каждая строка отформатирована как xml с пространством имен, я добавляю корневой тег к каждому разделенному файлу один за другим для последующего анализа lxml (glst обозначает «globbed») список ").

def rooter(glst):
    for logFiles in glst:
        oFile = open(logFiles,'r',encoding='utf-8')
        rFile = oFile.read()
        wFile = open(logFiles,'w',encoding='utf-8')
        wFile.write('<root>')
        wFile.write(rFile)
        wFile.write('</root>')
        oFile.close()
        wFile.close()

        print("Rooting XML : %s" % (logFiles))

Затем я загружаю только один файл XML для анализа в lxml:

def loadXml(fileobj):
    tree = etree.parse(fileobj)
    print("Processing : %s" % (fileobj))
    return tree

Вот мое узкое место, поскольку я не нашел другого удобного метода для эффективного анализа файла, пока я ищу только Event Data и мой Event Time. После нахождения данных я добавляю свои результаты в два отдельных списка (один для данных о событии, один для времени события), которые я позже преобразовываю в простой файл CSV, чтобы продолжить мой анализ с помощью pandas.

Этот код на самом деле работает с файлами размером менее 2 ГБ, но при запуске синтаксического анализа файлов размером более 2 ГБ не хватает памяти, мое решение должно работать в системе, имеющей всего 2-3 ГБ свободной оперативной памяти (64-разрядный рабочий стол Windows).

def parser(tree,DataL,DataTimeL):
    for evts in tree.iter('{%s}EventData' % nameSpace):
        EvtData = evts.find('{%s}Data' % nameSpace).text
        DataL.append(EvtData)        
    for evtSysTime in tree.iter('{%s}System' % nameSpace):
        eSysTime = evtSysTime.find('{%s}TimeCreated' % nameSpace).attrib
        DataTimeL.append(eSysTime)
        break

Я попытался вручную gc.collect и del объект файла после синтаксического анализа, но это, кажется, не имеет никакого эффекта, и python продолжает наращивать память, пока не происходит сбой ПК.

def initParser(glst,DataL,DataTimeL):
    for file in glst:
     root = loadXml(file)
     parser(root,DataL,DataTimeL)
     gc.collect()
     del file

Создание CSV (zlg означает itertools - zip_longest):

with open('LogOUT.csv', 'w', encoding="UTF-8", newline='') as cF:
    wr = csv.writer(cF)
    wr.writerow(("Event", "Event Time"))
    wr.writerows(zlg(EvtL,EvtTimeL))

Я пытался использовать TinyDB, ZODB, который звучит как перебор, также он слишком медленный или, возможно, я делаю это неправильно. Выгрузка событий в CSV вручную выполняется очень медленно. Поскольку функция парсера цикла for на самом деле очень эффективна для файлов размером менее 2 ГБ, я хотел бы найти способ безопасного и эффективного добавления этих больших списков без сбоев всей системы.

Заранее спасибо.

1 Ответ

0 голосов
/ 01 июля 2019

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

import csv
import itertools
import typing
from io import StringIO
from xml.etree import ElementTree
from xml.etree.ElementTree import Element


def grouper(iterable, n, fill=None) -> typing.Iterator:
    args = [iter(iterable)] * n
    return itertools.zip_longest(*args, fillvalue=fill)


def parse_event_xml(event_xml: str) -> dict:
    root: Element = ElementTree.fromstring(event_xml)
    namespaces = {'ns': 'http://schemas.microsoft.com/win/2004/08/events/event'}
    time_el = root.find('./ns:System/ns:TimeCreated', namespaces=namespaces)
    data_el = root.find('./ns:EventData', namespaces=namespaces)

    return {
        'Event Time': time_el.attrib['SystemTime'],
        'Event Data': data_el.text,
    }


def process_batch(batch: typing.Iterator[str], batch_filename: str) -> None:
    fields = ['Event Time', 'Event Data']
    with open(batch_filename, 'w', newline='') as bf:
        writer = csv.DictWriter(bf, fieldnames=fields)
        writer.writeheader()
        for item in batch:
            if not item:  # skip empty lines
                continue
            parsed = parse_event_xml(item)
            writer.writerow(parsed)


if __name__ == '__main__':
    xml_raw = '''<Event xmlns='http://schemas.microsoft.com/win/2004/08/events/event'><System><Provider Name=''/><EventID Qualifiers=''>0</EventID><Level>4</Level><Task>0</Task><Keywords></Keywords><TimeCreated SystemTime='2018-10-02T11:19:41.000000000Z'/><EventRecordID></EventRecordID><Channel>Application</Channel><Computer></Computer><Security/></System><EventData>DATA<Data></Data></EventData></Event>
    <Event xmlns='http://schemas.microsoft.com/win/2004/08/events/event'><System><Provider Name=''/><EventID Qualifiers=''>0</EventID><Level>4</Level><Task>0</Task><Keywords></Keywords><TimeCreated SystemTime='2018-10-02T11:19:41.000000000Z'/><EventRecordID></EventRecordID><Channel>Application</Channel><Computer></Computer><Security/></System><EventData>DATA<Data></Data></EventData></Event>'''

    batch_size = 10  # lines / events

    # read the event stream
    # normally you'd use `with open(filename, encoding='utf-8')`
    # but here i'm reading from a string
    with StringIO(xml_raw) as f:
        for i, batch in enumerate(grouper(f, batch_size)):
            batch_filename = f'batch_{i}.csv'
            process_batch(batch, batch_filename)

Настройте функцию parse_event_xml для извлечения нужных вам данных, здесь я использовал только EventTime и EventData

, эти выходные данныеCSV-файл, как это:

Event Time,Event Data
2018-10-02T11:19:41.000000000Z,DATA
2018-10-02T11:19:41.000000000Z,DATA
...