https://habr.com/ru/post/469995/В предыдущей
статье был рассмотрен подход к созданию csv из xml на базе данных, которые публикует ФИАС. В основу парсинга был положен DOM-парсер, загружающий в память весь файл целиком перед обработкой, что приводило к необходимости дробления файлов большого размера в виду ограниченного объема оперативной памяти. В этот раз предлагается посмотреть насколько хорош SAX-парсер и сравнить его скорость работы c DOM-парсером. В качестве подопытного будет использоваться наибольший из файлов базы ФИАС — houses, размером 27,5 ГБ.
Вступление
Вынуждены сразу огорчить почтеннейшую публику — сходу скормить SAX-парсеру файл БД ФИАС houses не удастся. Парсер вылетает с ошибкой «not well-formed (invalid token)». И первоначально были подозрения, что файл БД битый. Однако после нарезки БД на несколько мелких частей было установлено, что вылеты вызваны измененной кодировкой для номеров домов и/или строений. То есть в тегах STRUCNUM либо HOUSENUM попадались дома с буквой, записанной в странной кодировке (не UTF-8 и не ANSI, в которой сформирован сам документ):
При этом, если эту кодировку выправить, прогнав файл через функцию remove_non_ascii, запись принимала вид:
Такой файл также не поглощался парсером, из-за лишних знаков.
Пришлось вспоминать регулярные выражения и чистить файл перед загрузкой в парсер.
Вопрос: почему нельзя создать нормальную БД, которая выкладывается для работы приобретает оттенок риторического.
Чтобы выровнять стартовые возможности парсеров, очистим тестовый фрагмент от вышеуказанных нестыковок.
Код для очистки файла БД перед загрузкой в парсер:
Код#from __future__ import unicode_literals
#import codecs,os
#import xml.etree.ElementTree as ET
#import csv
from datetime import datetime
#import struct
#import binascii
#import io
import re
from unidecode import unidecode
#test1=[]
#test2=[]
#os.chdir('D://python64//')
start = datetime.now()
f= open('AS_HOUSE.462.xml', 'r',encoding='ANSI')
def remove_non_ascii(text):
return unidecode(unidecode(text))
for line in f:
b=remove_non_ascii(line)
for c in re.finditer(r'\w{5}NUM="\d{1,}\"\w\"',b):
print(c[0])
#test1.append(c[0])
c1=c[0][:-3]+c[0][-2]
print(c1)
#test2.append(c1)
#print(test1)
b=b.replace(c[0],c1) # замена в строке
#сохраняем результат
f1= open('out.xml', 'w',encoding='ANSI')
f1.write(b)
f1.close()
"""
for c in re.finditer(r'STRUCNUM="\d{1,}\"\w\"',b):
print(c[0])
#test1.append(c[0])
c1=c[0][:-3]+c[0][-2]
print(c1)
#test2.append(c1)
#print(test1)
b=b.replace(c[0],c1) # замена в строке
#сохраняем результат
f1= open('out.xml', 'w',encoding='ANSI')
f1.write(b)
f1.close()
"""
f.close()
print(datetime.now()- start)
#для файла 58Мб очистка длится
# 0:00:25.884481 - 25 сек
Код переводит в xml-файле non_ascii символы в нормальные и затем удаляет лишние "" в наименованиях строений и домов.
SAX-парсер
Для старта возьмем небольшой xml файл (58,8 Мб), на выходе планируем получить txt или csv, удобный для дальнейшей обработки в pandas или excel.
Кодimport xml.sax
import csv
from datetime import datetime
start = datetime.now()
#Resident_data = open('AS_HOUSE.0001.csv', 'a',encoding='ANSI')
#csvwriter = csv.writer(Resident_data)
class EventHandler(xml.sax.ContentHandler):
def __init__(self,target):
self.target = target
def startElement(self,name,attrs):
self.target.send(attrs._attrs.values())
#self.target.send(attrs._attrs)
def characters(self,text):
self.target.send('')
def endElement(self,name):
self.target.send('')
def coroutine(func):
def start(*args,**kwargs):
cr = func(*args,**kwargs)
cr.__next__()
return cr
return start
with open('out.csv', 'a') as f:
# example use
if __name__ == '__main__':
@coroutine
def printer():
while True:
event = (yield)
#print (event)
print(event,file=f)
#csvwriter.writerow(event)
xml.sax.parse("out.xml", EventHandler(printer()))
#Resident_data.close()
print(datetime.now()- start)
#0:00:06.196355 - 6 сек
Выполнив программу получим значения словаря python:
Время выполнения: 5-6 сек.
DOM-парсер
Обработаем тот же файл, предварительно загрузив его целиком в память. Именно такой метод использует DOM-парсер.
Кодfrom __future__ import unicode_literals
import codecs,os
import xml.etree.ElementTree as ET
import csv
from datetime import datetime
import struct
import binascii
import io
import re
"""
f= open('AS_HOUSE.0028.xml', 'r',encoding='ANSI')
#parser = lxml.etree.XMLParser(recover=True)
#tree = lxml.etree.parse('AS_HOUSE.002.xml', parser)
for line in f:
result = re.sub(r'[^\x00-\x7f]', '', line)
f1= open('out.xml', 'a',encoding='ANSI')
f1.write(result)
f1.close()
f.close()
#parser = ET.XMLParser(encoding="ANSI")
#tree = ET.parse(result,parser=parser)
удалить двойные ""
удалить BUILDNUM=
#root = tree.getroot()
"""
parser = ET.XMLParser(encoding="ANSI") #либо ANSI
tree = ET.parse("out.xml",parser=parser)
root = tree.getroot()
Resident_data = open('AS_HOUSE.0001.csv', 'w',encoding='ANSI')
csvwriter = csv.writer(Resident_data)
#resident_head = []
#count = 0
start = datetime.now()
for member in root.findall('House'):
object = []
object.append(member.attrib['HOUSEID'])
object.append(member.attrib['HOUSEGUID'])
object.append(member.attrib['AOGUID'])
try:
object.append(member.attrib['HOUSENUM'])
except:
object.append(None)
try:
object.append(member.attrib['STRUCNUM'])
except:
object.append(None)
object.append(member.attrib['STRSTATUS'])
object.append(member.attrib['ESTSTATUS'])
object.append(member.attrib['STATSTATUS'])
try:
object.append(member.attrib['IFNSFL'])
except:
object.append(None)
try:
object.append(member.attrib['IFNSUL'])
except:
object.append(None)
try:
object.append(member.attrib['TERRIFNSFL'])
except:
object.append(None)
try:
object.append(member.attrib['TERRIFNSUL'])
except:
object.append(None)
try:
object.append(member.attrib['OKATO'])
except:
object.append(None)
try:
object.append(member.attrib['OKTMO'])
except:
object.append(None)
try:
object.append(member.attrib['POSTALCODE'])
except:
object.append(None)
object.append(member.attrib['STARTDATE'])
object.append(member.attrib['ENDDATE'])
object.append(member.attrib['UPDATEDATE'])
object.append(member.attrib['COUNTER'])
try:
object.append(member.attrib['NORMDOC'])
except:
object.append(None)
object.append(member.attrib['DIVTYPE'])
object.append(member.attrib['REGIONCODE'])
#print(len(object))
csvwriter.writerow(object)
Resident_data.close()
print(datetime.now()- start)
#0:00:02.972170
Время выолнения 2-3 сек.
Победа DOM-парсера?
Файлы побольше
Файлы небольшого размера не отражают действительности в полной мере. Возьмем файл побольше 353 Мб (предварительно почистив, как было указано выше).
Результаты погона:
SAX-парсер: 0:00:32.090836 — 32 сек
DOM-парсер: 0:00:16.630951 — 16 сек
Разница в 2 раза по скорости. Однако это не умаляет главного достоинства SAX-парсера — возможность обрабатывать файлы большого размера без предварительной загрузки в память.
Остается сожалеть, что данное достоинство не применимо к БД ФИАС, так как требуется предварительная работа с кодировками.
Файл для предварительной очистки кодировок:
— 353 Мб в
архиве.
Очищенный файл БД для тестов парсеров:
— 353 Мб в
архиве.