habrahabr

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

  • суббота, 5 октября 2019 г. в 00:28:43
https://habr.com/ru/post/469995/
  • 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 __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 Мб в архиве.