Если вам нужно только извлечь данные из XML-файла и вам не нужно выполнять какие-либо специфичные для XML операции, такие как преобразования XSL и т. Д., Подход с очень малым объемом памяти заключается в определении собственного TreeBuilder
. Пример:
import pathlib
from pprint import pprint
from xml.etree import ElementTree as ET
class ManagedObjectsCollector:
def __init__(self):
self.item_count = 0
self.items = []
self.curr_item = None
self.attr_name = None
self.list_name = None
self.list_entry = False
def start(self, tag, attr):
if tag == '{raml20.xsd}managedObject':
self.curr_item = dict()
self.curr_item.update(**attr)
elif tag == '{raml20.xsd}p':
if self.list_name is None:
self.attr_name = attr.get('name', None)
self.list_entry = self.list_name is not None
elif tag == '{raml20.xsd}list':
self.list_name = attr.get('name', None)
if self.list_name is not None:
self.curr_item[self.list_name] = []
def end(self, tag):
if tag == '{raml20.xsd}managedObject':
self.items.append(self.curr_item)
self.curr_item = None
elif tag == '{raml20.xsd}p':
self.attr_name = None
self.list_entry = False
elif tag == '{raml20.xsd}list':
self.list_name = None
def data(self, data):
if self.curr_item is None:
return
if self.attr_name is not None:
self.curr_item[self.attr_name] = data
elif self.list_entry:
self.curr_item[self.list_name].append(data)
def close(self):
return self.items
if __name__ == '__main__':
file = pathlib.Path('data.xml')
with file.open(encoding='utf-8') as stream:
collector = ManagedObjectsCollector()
parser = ET.XMLParser(target=collector)
ET.parse(stream, parser=parser)
items = collector.items
print('total:', len(items))
pprint(items)
Запуск приведенного выше кода с данными вашего примера приведет к выводу:
total: 3
[{'PiOptions': ['0', '5', '2', '6', '7', '3', '9', '10'],
'btsName': '4251',
'class': 'MRBTS',
'distName': 'PL/M-1',
'id': '366',
'linkedMrsiteDN': 'PL/TE-2',
'name': 'Name of street',
'spareInUse': '1',
'version': 'MRBTS17A_1701_003'},
{'btsName': '748',
'class': 'MRBTS',
'distName': 'PL/M10',
'id': '958078',
'linkedMrsiteDN': 'PLMN-PLMN/MRSITE-138',
'name': 'Street 2',
'spareInUse': '3',
'version': 'MRBTS17A_1701_003'},
{'btsName': '529',
'class': 'MRBTS',
'distName': 'PL/M21',
'id': '1482118',
'name': 'Stree 3',
'spareInUse': '4',
'version': 'MRBTS17A_1701_003'}]
Поскольку мы не создаем дерево XML в ManagedObjectsCollector
и не храним в памяти больше, чем текущая строка файла, выделение парсером памяти минимально, и на использование памяти во многом влияет список collector.items
. В приведенном выше примере анализируются все данные из каждого элемента managedObject
, поэтому список может быть довольно большим. Вы можете проверить это, закомментировав строку self.items.append(self.curr_item)
- если список не увеличивается, использование памяти остается постоянным (примерно 20-30 МБ, в зависимости от версии Python).
Если вам нужны только части данных, вы получите более простую реализацию TreeBuilder
. Например, вот TreeBuilder
, который собирает только атрибуты версии, игнорируя остальные теги:
class VersionCollector:
def __init__(self):
self.items = []
def start(self, tag, attr):
if tag == '{raml20.xsd}managedObject':
self.items.append(attr['version'])
def close(self):
return self.items
Бонус
Вот автономный скрипт, расширенный измерениями использования памяти. Для установки вам понадобятся дополнительные пакеты:
$ pip install humanize psutil tqdm
Необязательно: используйте lxml
для более быстрого анализа:
$ pip install lxml
Запустить скрипт с именем файла в качестве параметра. Пример вывода XML-файла размером 40 МБ:
$ python parse.py data_39M.xml
mem usage: 1%|▏ | 174641152/16483663872 [00:01<03:05, 87764892.80it/s, mem=174.6 MB]
total items memory size: 145.9 MB
total items count: 150603
[{'PiOptions': ['0', '5', '2', '6', '7', '3', '9', '10'],
'btsName': '4251',
'class': 'MRBTS',
'distName': 'PL/M-1',
'id': '366',
'linkedMrsiteDN': 'PL/TE-2',
'name': 'Name of street',
'spareInUse': '1',
'version': 'MRBTS17A_1701_003'},
...
Обратите внимание, что для файла XML размером 40 МБ пиковое использование памяти составляет ~ 174 МБ, а выделение памяти для списка items
составляет ~ 146 МБ; остальное - накладные расходы Python и остается постоянным независимо от размера файла. Это должно дать вам приблизительную оценку того, сколько памяти вам понадобится для чтения больших файлов.
Исходный код:
from collections import deque
import itertools
import pathlib
from pprint import pprint
import os
import sys
import humanize
import psutil
import tqdm
try:
from lxml import etree as ET
except ImportError:
from xml.etree import ElementTree as ET
def total_size(o, handlers={}, verbose=False):
"""https://code.activestate.com/recipes/577504/"""
dict_handler = lambda d: itertools.chain.from_iterable(d.items())
all_handlers = {
tuple: iter,
list: iter,
deque: iter,
dict: dict_handler,
set: iter,
frozenset: iter,
}
all_handlers.update(handlers)
seen = set()
default_size = sys.getsizeof(0)
def sizeof(o):
if id(o) in seen:
return 0
seen.add(id(o))
s = sys.getsizeof(o, default_size)
if verbose:
print(s, type(o), repr(o), file=sys.stderr)
for typ, handler in all_handlers.items():
if isinstance(o, typ):
s += sum(map(sizeof, handler(o)))
break
return s
return sizeof(o)
class ManagedObjectsCollector:
def __init__(self, mem_pbar):
self.item_count = 0
self.items = []
self.curr_item = None
self.attr_name = None
self.list_name = None
self.list_entry = False
self.mem_pbar = mem_pbar
self.mem_pbar.set_description('mem usage')
def update_mem_usage(self):
proc_mem = psutil.Process(os.getpid()).memory_info().rss
self.mem_pbar.n = 0
self.mem_pbar.update(proc_mem)
self.mem_pbar.set_postfix(mem=humanize.naturalsize(proc_mem))
def start(self, tag, attr):
if tag == '{raml20.xsd}managedObject':
self.curr_item = dict()
self.curr_item.update(**attr)
elif tag == '{raml20.xsd}p':
if self.list_name is None:
self.attr_name = attr.get('name', None)
self.list_entry = self.list_name is not None
elif tag == '{raml20.xsd}list':
self.list_name = attr.get('name', None)
if self.list_name is not None:
self.curr_item[self.list_name] = []
def end(self, tag):
if tag == '{raml20.xsd}managedObject':
self.items.append(self.curr_item)
self.curr_item = None
elif tag == '{raml20.xsd}p':
self.attr_name = None
self.list_entry = False
elif tag == '{raml20.xsd}list':
self.list_name = None
# Updating progress bar costs resources, don't do it
# on each item parsed or it will slow down the parsing
self.item_count += 1
if self.item_count % 10000 == 0:
self.update_mem_usage()
def data(self, data):
if self.curr_item is None:
return
if self.attr_name is not None:
self.curr_item[self.attr_name] = data
elif self.list_entry:
self.curr_item[self.list_name].append(data)
def close(self):
return self.items
if __name__ == '__main__':
file = pathlib.Path(sys.argv[1])
total_mem = psutil.virtual_memory().total
with file.open(encoding='utf-8') as stream, tqdm.tqdm(total=total_mem, position=0) as pbar_total_mem:
collector = ManagedObjectsCollector(pbar_total_mem)
parser = ET.XMLParser(target=collector)
ET.parse(stream, parser=parser)
items = collector.items
print('total:', len(items))
print('total items memory size:', humanize.naturalsize(total_size(items)))
pprint(items)