4 October 2019

SAX-парсер python vs DOM-парсер python. Парсим ФИАС-houses

Python
В предыдущей статье был рассмотрен подход к созданию 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 datetime import datetime
import re
from unidecode import unidecode

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])                      
                c1=c[0][:-3]+c[0][-2]
                print(c1) 
                b=b.replace(c[0],c1) # замена в строке        

                #сохраняем результат
                f1= open('out.xml', 'w',encoding='ANSI')
                f1.write(b)
                f1.close()

f.close()
print(datetime.now()- start)


Код переводит в xml-файле non_ascii символы в нормальные и затем удаляет лишние "" в наименованиях строений и домов.

SAX-парсер


Для старта возьмем небольшой xml файл (58,8 Мб), на выходе планируем получить txt или csv, удобный для дальнейшей обработки в pandas или excel.

Код
import xml.sax
import csv
from datetime import datetime

start = datetime.now()

class EventHandler(xml.sax.ContentHandler):
    def __init__(self,target):
        self.target = target
    def startElement(self,name,attrs):
        self.target.send(attrs._attrs.values())          
    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,file=f)
                

        xml.sax.parse("out.xml", EventHandler(printer()))

print(datetime.now()- start)


Выполнив программу получим значения словаря python:



Время выполнения: 5-6 сек.

DOM-парсер


Обработаем тот же файл, предварительно загрузив его целиком в память. Именно такой метод использует DOM-парсер.

Код
import codecs,os
import xml.etree.ElementTree as ET
import csv
from datetime import datetime

parser = ET.XMLParser(encoding="ANSI")
tree = ET.parse("out.xml",parser=parser)
root = tree.getroot()

Resident_data = open('AS_HOUSE.0001.csv', 'a',encoding='ANSI')
csvwriter = csv.writer(Resident_data)

attr_names = [
    'HOUSEID', 'HOUSEGUID', 'AOGUID', 'HOUSENUM', 'STRUCNUM', 
    'STRSTATUS', 'ESTSTATUS', 'STATSTATUS', 'IFNSFL', 'IFNSUL', 
    'TERRIFNSFL', 'TERRIFNSUL', 'OKATO', 'OKTMO', 'POSTALCODE', 
    'STARTDATE', 'ENDDATE', 'UPDATEDATE', 'COUNTER', 'NORMDOC', 
    'DIVTYPE', 'REGIONCODE'
]
start = datetime.now()
object = []
for member in root.findall('House'):    
    object = [member.attrib.get(attr_name, None) for attr_name in attr_names]
    csvwriter.writerow(object)    
Resident_data.close()
print(datetime.now()- start)


Время выолнения 2-3 сек.
Победа DOM-парсера?

Файлы побольше


Файлы небольшого размера не отражают действительности в полной мере. Возьмем файл побольше 353 Мб (предварительно почистив, как было указано выше).

Результаты погона:

SAX-парсер: 0:00:32.090836 — 32 сек
DOM-парсер: 0:00:16.630951 — 16 сек

Разница в 2 раза по скорости. Однако это не умаляет главного достоинства SAX-парсера — возможность обрабатывать файлы большого размера без предварительной загрузки в память.
Остается сожалеть, что данное достоинство не применимо к БД ФИАС, так как требуется предварительная работа с кодировками.

Файл для предварительной очистки кодировок:
— 353 Мб в архиве.

Очищенный файл БД для тестов парсеров:
— 353 Мб в архиве.
Tags:pythonфиаспарсеры БДxml-to-csv
Hubs: Python
+2
2.5k 28
Comments 29