В прошедший понедельник на московской конференции DevConf я презентовал в виде мастер-класса питоновую библиотеку для потоковой генерации XML. Как и надеялся, получил хороший фидбек, и вот сегодня формально представляю проект и первый его релиз.
Описание
Название обыгрывает "ElementTree" в том смысле, что в отличие от неё elementflow не генерит собственно дерево.
Текущие известные библиотеки, умеющие генерировать XML (ElementTree, lxml), строят в памяти XML-дерево целиком, а затем его сериализуют. Это может быть неэффективно при генерации более-менее объёмных XML-документов (например в контексте контент-ориентированного веб-сервиса, выдающего большое количество XML-данных). Существующий же потоковый генератор xml.sax.saxutils.XMLGenerator очень низкоуровневый, там надо руками закрывать элементы.
А кроме того, большинство XML-библиотек, откровенно говоря, отвратительно работают с namespace'ами.
Использование
Базовый пример:
import elementflow
file = open('text.xml', 'w') # can be any object with .write() method
with elementflow.xml(file, u'root') as xml:
xml.element(u'item', attrs={u'key': u'value'}, text=u'text')
with xml.container(u'container', attrs={u'key': u'значение'}):
xml.text(u'текст')
xml.element(u'subelement', text=u'subelement text')
Использовать with
обязательно, чтобы правильно закрывались элементы-контейнеры. Библиотека ожидает на вход юникодные строки и выдаёт на выход байтовую строку в utf-8. Библиотека правильно escape'ит текстовый строки и значения атрибутов, но не будет заморачиваться с проверкой корректности имён тегов и атрибутов.
Про работу с namespace'ами, форматирование вывода и превращение генерирующего кода в итератор читайте в README.
Производительность
Все тесты проводились вот на таком XML:
<contacts>
<person id="0">
<name>John & Smith</name>
<email>john.smith@megacorp.com</email>
<phones>
<phone type="work">123456</phone>
<phone type="home">123456</phone>
</phones>
</person>
<!-- повторить <person> .. </person> несколько тысяч раз -->
</contacts>
Записи генерируются простым циклом, то есть времени на выборку данных не тратится, это чистое время работы генератора и вывода в /dev/null
. Спецификация машины неважна, это некая средняя десктопная машина.
Вот результаты одиночного теста на 40000 записей:
ElementTree | 10,7 сек |
---|---|
cElementTree | 6,2 сек |
lxml.etree | 0,9 сек |
elementflow | 2,3 сек |
Низкая скорость ElementTree меня сильно озадачила, не знаю, почему он такой медленный, даже в C-версии. Высокая скорость lxml, напротив, вполне ожидаема. Это, впрочем, не очень полезный тест, потому что гораздо интересней то, как будет вести себя веб-сервис, генерирующий XML для нескольких одновременных запросов.
Для этого я на мастер-классе провёл два разных теста: под Tornado и под Django. В них сравнивались две модели генерации XML:
- сначала всё сгенерировать в памяти, потом выдать одним куском
- выдавать XML потоком мелких кусочков по мере генерации
В обоих случаях использовалась elementflow. Веб-сервис нагружался тулзой siege смесью URL'ов: 80% маленьких и быстрых ("Hello, World!") и 20% — генерирующих большой XML. Использовались 10 одновременных запросов на Tornado и 20 — на Django.
Код обоих тестовых сервисов можно посмотреть в истории Bazaar'а.
Tornado
При генерации в памяти сразу заметен вот такой неприятный эффект:
HTTP/1.1 200 16.18 secs: 3548949 bytes ==> /memory?count=20000
HTTP/1.1 200 16.21 secs: 3548949 bytes ==> /memory?count=20000
HTTP/1.1 200 10.34 secs: 13 bytes ==> /
HTTP/1.1 200 16.25 secs: 3548949 bytes ==> /memory?count=20000
HTTP/1.1 200 9.32 secs: 13 bytes ==> /
HTTP/1.1 200 8.64 secs: 13 bytes ==> /
HTTP/1.1 200 9.42 secs: 13 bytes ==> /
HTTP/1.1 200 0.11 secs: 13 bytes ==> /
HTTP/1.1 200 0.18 secs: 13 bytes ==> /
HTTP/1.1 200 0.00 secs: 13 bytes ==> /
Те "hello-world" ответы, которые запрашиваются одновременно с долгими XML-ответами, вынуждены их ждать, в результате чего отдаются очень долго. В обычной же ситуации должны отдаваться мгновенно.
При генерации потоком эффект пропадает:
HTTP/1.1 200 3.69 secs: 1086426 bytes ==> /stream?count=20000&bufsize=4096
HTTP/1.1 200 3.69 secs: 1086426 bytes ==> /stream?count=20000&bufsize=4096
HTTP/1.1 200 0.02 secs: 13 bytes ==> /
HTTP/1.1 200 3.33 secs: 1086426 bytes ==> /stream?count=20000&bufsize=4096
HTTP/1.1 200 3.33 secs: 1086426 bytes ==> /stream?count=20000&bufsize=4096
HTTP/1.1 200 3.33 secs: 1086426 bytes ==> /stream?count=20000&bufsize=4096
HTTP/1.1 200 3.33 secs: 1086426 bytes ==> /stream?count=20000&bufsize=4096
HTTP/1.1 200 0.02 secs: 13 bytes ==> /
HTTP/1.1 200 0.02 secs: 13 bytes ==> /
Кроме того, и большие ответы генерируются быстрее.
Django
Под Django такого же эффекта, как в однопоточном Tornado, проявляться не может, там запросы друг другу не мешают. Там происходит другой эффект: при генерации в памяти создаётся очень много отфорканных процессов, которые создают на машине большой LA, отчего она работает существенно медленней. При этом я подбирал такие цифры нагрузок, чтобы физически памяти ещё хватало.
Вот картинка при генерации в памяти:
HTTP/1.1 200 9.05 secs: 5620 bytes ==> /memory?count=20000
HTTP/1.1 200 9.39 secs: 5620 bytes ==> /memory?count=20000
HTTP/1.1 200 0.00 secs: 13 bytes ==> /
HTTP/1.1 200 0.00 secs: 13 bytes ==> /
HTTP/1.1 200 0.29 secs: 13 bytes ==> /
HTTP/1.1 200 0.00 secs: 13 bytes ==> /
HTTP/1.1 200 10.51 secs: 5620 bytes ==> /memory?count=20000
HTTP/1.1 200 0.00 secs: 13 bytes ==> /
HTTP/1.1 200 0.01 secs: 13 bytes ==> /
LA: 5,9; средняя скорость: 6,81 rps
С потоковой генерацией всё лучше:
HTTP/1.1 200 3.83 secs: 1086426 bytes ==> /stream?count=20000&bufsize=4096
HTTP/1.1 200 3.84 secs: 1086426 bytes ==> /stream?count=20000&bufsize=4096
HTTP/1.1 200 3.86 secs: 1086426 bytes ==> /stream?count=20000&bufsize=4096
HTTP/1.1 200 3.86 secs: 1086426 bytes ==> /stream?count=20000&bufsize=4096
HTTP/1.1 200 3.98 secs: 1086426 bytes ==> /stream?count=20000&bufsize=4096
HTTP/1.1 200 0.48 secs: 13 bytes ==> /
HTTP/1.1 200 0.00 secs: 13 bytes ==> /
HTTP/1.1 200 0.00 secs: 13 bytes ==> /
HTTP/1.1 200 0.00 secs: 13 bytes ==> /
LA: 2,7; средняя скорость: 13,18 rps
Заключение
Хоть библиотека и не такая быстрая, как пресловутый lxml, мне кажется, что она довольно полезна в определённых ситуациях. В ближайшем будущем я планирую сделать ещё бранч для Python 3 и, конечно, собрать как можно больше фидбека и контрибуций.
Комментарии: 5
А почему размер ответов разный? Особенно смущают 5K в варианте Django/memory по сравнению с 1M в Django/stream.
Это меня тоже удивило. Я подозреваю, что это что-то у siege то ли считается не так, то ли не забирается до конца. Потому что я проверял, спрашивая те же запросы curl'ом — там всё отдаётся до конца, и файлы получаются одинаковые.
Хорошая статья. Плохо разбираюсь в Питоне, но даже мне было все ясно
Думаю буду использовать..
Интересно было бы посмотреть на эти же siege тесты, но с lxml.
Кстати, мне кажется с XML-ками с большой вложенностью тоже будет грузить прилично, а если много похожих элементов на одном уровне, то да, будет летать.
И еще вопрос - есть ли смысл использовать эту библиотеку для генерации xml под XSLT шаблонизацию?
До этого руки не дошли :-).
Почему? Она в любом случае выдаёт контент сразу, не дожидаясь закрытия элементов, поэтому на вложеность тут ничего не завязано.
По идее, её для всего можно использовать. Почему нет? :-)