Pull to refresh

Самый быстрый SAX-парсер для python

Reading time2 min
Views20K
Внезапно захотелось пересчитать все xml-теги в 240 тысячах xml-файлов общим весом 180 GB. Питоном — и побыстрее.

Задача


На самом деле захотелось прикинуть — насколько реально перегнать Библиотеку, Чье Имя Нельзя Произносить Вслух, из fb2 в docbook. В связи со «специфичностью» FB2 надо прикинуть — какие теги можно просто пропустить ввиду редкости. Т.е. просто пересчитать количество вхождения каждого тега во все файлы.
По дороге планировалось сравнить разные sax-парсеры. К сожалению — тестирования не получилось, т.к. и xml.sax и lxml на первом же fb2 поломались. В итоге остался xml.parsers.expat.
Да, и еще — файлы *.fb2 упакованы в zip-архивы.

Исходные данные


Исходными данными является снапшот Библиотеки по состоянию на 2013.02.01, цельнотянутый из тор Интернетов: 242525 файла *.fb2 общим весом 183909288096 байт, упакованые в 56 zip-архивов общим весом 82540008 байт.
Платформа: Asus X5DIJ (Pentium DualCore T4500 (2x2.30), 2GB RAM); Fedora 18, python 2.7.

Код


Написано на скорую руку, с претензией на универсальность:
#!/bin/env python
# -*- coding: utf-8 -*-
'''
'''

import sys, os, zipfile, hashlib, pprint
import xml.parsers.expat, magic

mime = magic.open(magic.MIME_TYPE)
mime.load()
tags = dict()
files = 0

reload(sys)
sys.setdefaultencoding('utf-8')

def start_element(name, attrs):
	tags[name] = tags[name] + 1 if name in tags else 1

def	parse_dir(fn):
	dirlist = os.listdir(fn)
	dirlist.sort()
	for i in dirlist:
		parse_file(os.path.join(fn, i))

def	parse_file(fn):
	m = mime.file(fn)
	if (m == 'application/zip'):
		parse_zip(fn)
	elif (m == 'application/xml'):
		parse_fb2(fn)
	else:
		print >> sys.stderr, 'Unknown mime type (%s) of file %s' % (m, fn)

def	parse_zip(fn):
	print >> sys.stderr, 'Zip:', os.path.basename(fn)
	z = zipfile.ZipFile(fn, 'r')
	filelist = z.namelist()
	filelist.sort()
	for n in filelist:
		try:
			parse_fb2(z.open(n))
			print >> sys.stderr, n
		except:
			print >> sys.stderr, 'X:', n

def	parse_fb2(fn):
	global files
	if isinstance(fn, str):
		fn = open(fn)
	parser = xml.parsers.expat.ParserCreate()
	parser.StartElementHandler = start_element
	parser.Parse(fn.read(), True)
	files += 1

def	print_result():
	out = open('result.txt', 'w')
	for k, v in tags.iteritems():
		out.write(u'%s\t%d\n' % (k, v))
	print 'Files:', files

if (__name__ == '__main__'):
	if len(sys.argv) != 2:
		print >> sys.stderr, 'Usage: %s <xmlfile|zipfile|folder>' % sys.argv[0]
		sys.exit(1)
	src = sys.argv[1]
	if (os.path.isdir(src)):
		parse_dir(src)
	else:
		parse_file(src)
	print_result()


Результаты


Заряжаем:
time nice ./thisfile.py ~/Torrent/....ec > out.txt 2>err.txt

Получаем:
* Время выполнения — 74'15..45" (параллельно выполнялась небольшая работа и слушалась музыка, ессно);
* Получилось, что скорость обработки — ~40 MB/s (или 58 тактов/байт)
* Отброшено 2584 файлов *.fb2 (expat хоть и non validate parser — но не до такой же степени...) — ~10%;
* в файле results.txt — чего только нет…
* ну и, собственно то, ради чего всё затевалось: из 65 тегов FB2 _не_ применяется только только один (output-document-class); еще парочку (output, part, stylesheet) можно пропустить; остальные применяются от 10 тыс. раз.
* по грубым прикидкам чтение файлов (с распаковкой) занимает 52%, парсинг — 40%, обработка start_element — 8%.

А быстрее — можно? На python.
Tags:
Hubs:
0
Comments14

Articles

Change theme settings