26 декабря 2012 в 13:25

Бенчмарк HTML парсеров

Переписывал в островке кусок одного сервиса с Python на Erlang. Сам сервис занимается тем, что скачивает по HTTP значительное количество однотипных HTML страниц и извлекает из них некоторую информацию. Основная CPU нагрузка сервиса приходится на парсинг HTML в DOM дерево.

Сперва захотелось сравнить производительность Erlang парсера mochiweb_html с используемым из Python lxml.etree.HTML(). Провел простейший бенчмарк, нужные выводы сделал, а потом подумал что неплохо было бы добавить в бенчмарк ещё парочку-другую парсеров и платформ, оформить покрасивее, опубликовать код и написать статью.
На данный момент успел написать бенчмарки на Erlang, Python, PyPy, NodeJS и С в следующих комбинациях:
  • Erlang — mochiweb_html
  • CPython — lxml.etree.HTML
  • CPython — BeautifulSoup 3
  • CPython — BeautifulSoup 4
  • CPython — html5lib
  • PyPy — BeautifulSoup 3
  • PyPy — BeautifulSoup 4
  • PyPy — html5lib
  • Node.JS — cheerio
  • Node.JS — htmlparser
  • Node.JS — jsdom
  • C — libxml2 (скорее для справки)

В тесте сравниваются скорость обработки N итераций парсера и пиковое потребление памяти.

Интрига: кто быстрее — Python или PyPy? Как сказывается иммутабельность Erlang на скорости парсинга и потреблении памяти? Насколько быстра V8 NodeJS? И как на всё это смотрит код на чистом C.

Термины


Скорее всего вы с ними знакомы, но почему бы не повторить?

Нестрогий HTML парсер — парсер HTML, который умеет обрабатывать некорректный HTML код (незакрытые теги, знаки > < внутри тегов <script>, незаэскейпленные символы амперсанда &, значения атрибутов без кавычек и т.п.). Понятно, что не любой поломанный HTML можно восстановить, но можно привести его к тому виду, к которому его приводит браузер. Примечательно, что большая часть HTML, которая встречается в интернете, является в той или иной степени невалидной!
DOM дерево — Document Object Model если говорить строго, то DOM это тот API, который предоставляется яваскрипту в браузере для манипуляций над HTML документом. Мы немного упростим задачу и будем считать, что это структура данных, которая представляет из себя древовидное отображение структуры HTML документа. В корне дерева находится элемент <html>, его дочерние элементы — <head> и <body> и так далее. Например, в Python документ
<html lang="ru-RU">
    <head></head>
    <body>Hello, World!</body>
</html>

Можно в простейшем виде представить как
("html", {"lang": "ru-RU"}, [
    ("head", {}, []),
    ("body", {}, ["Hello, World!"])
])

Обычно HTML преобразуют в DOM дерево для трансформации или для извлечения данных. Для извлечения данных из дерева очень удобно использовать XPath или CSS селекторы.

Конкурсанты


  • Erlang
    • Mochiweb html parser. Единственный нестрогий HTML парсер для Erlang. Написан на эрланге.

  • CPython
    • lxml.etree.HTML биндинг libxml2. Cython
    • BeautifulSoup 3 Написанный на python HTML DOM парсер (3-я версия).
    • BeautifulSoup 4 HTML DOM парсер с подключаемыми бекендами.
    • html5lib Написанный на питоне DOM парсер, ориентированный на HTML5.

  • PyPy (те же парсеры, что и CPython, кроме lxml)
    • BeautifulSoup 3
    • BeautifulSoup 4
    • html5lib

  • Node.JS
    • cheerio написанный на JS HTML DOM парсер с поддержкой jQuery API
    • htmlparser HTML DOM парсер на чистом JS
    • jsdom написанный на JS HTML DOM парсер с навороченным API, похожем на API браузера

  • C
    • libxml2 Написанный на C нестрогий HTML SAX/DOM парсер.



Цели


Вообще, парсинг HTML (как и JSON) интересен тем, что документ нужно просматривать посимвольно. В нём нет инструкций вроде «следующие 10Кб — это сплошной текст, его копируем как есть». Если мы встретили в тексте тег <p>, то нам нужно последовательно просматривать все последующие символы на наличие конструкции </. Тот факт, что HTML может быть невалидным, заставляет «перепроверять всё по 2 раза». Потому что, например, если мы встретили тег <option>, то далеко не факт, что встретим закрывающий </option>. Вторая проблема, которая обычно возникает с такими форматами — экранирование спецсимволов. Например, если весь документ это <html>...100 мегабайт текста... &amp; ...ещё 100 мегабайт текста...</html>, то парсер будет вынужден создать в памяти полную копию содержимого тега с единственным изменением — "&amp;", преобразованный в "&" (хотя некоторые парсеры просто разбивают такой текст на 3 отдельных куска).

Необходимость строить в памяти довольно большую структуру — дерево из мелких объектов, накладывает довольно жесткие требования на управление памятью, сборщик мусора, на оверхед на создание множества мелких объектов.

Нашим бенчмарком хотим:
  • Сравнить производительность и потребление памяти различных нестрогих HTML DOM парсеров.
  • Изучить стабильность работы парсера. Возрастет ли время обработки одной страницы и объём потребляемой памяти с ростом числа итераций?
  • Как зависит скорость парсинга и потребление памяти от размера HTML документа.
  • Ну и оценить эффективность платформы: скорость работы со строками, эффективность менеджмента памяти

Проверять качество работы парсера в плане полноты восстановления поломанных документов не будем. Сравнение удобства API и наличие инструментов для работы с деревом тоже оставим за кадром.

Условия и методика тестирования


Программа один раз считывает документ с диска в память и затем N раз последовательно парсит его в цикле.
Парсер на каждой итерации должен строить в памяти полное DOM дерево.
После N итераций программа печатает время работы цикла и завершается.

Каждый парсер запускаем на наборе из нескольких HTML документов на N = 10, 50, 100, 400, 600 и 1000 итераций.
Измеряем User CPU, System CPU, Real runtime и (примерное?) пиковое потребление памяти с помощью /usr/bin/time.
HTML документы:
  • page_google.html (116Kb) — выдача гугла, 50 результатов на странице. Очень много встроенного в страницу HTML и JS, мало текста, весь HTML в одну строку.
  • page_habrahabr-70330.html (1,6Mb) — статья на хабре с 900 комментариями. Очень большая страница, много тегов, пробелов и табуляции.
  • page_habrahabr-index.html (95Kb) — главная страница habrahabr. Типичная страница блога.
  • page_wikipedia.html (99Kb) — статья на wikipedia. Хотел страничку, на которой будет много текста и мало тегов, но выбрал не самую удачную. В итоге много тегов и встроенного CSS.

На самом деле понял, что большинство документов одинакового размера только под конец, но переделывать не стал, т.к. сам процесс измерения занимает довольно много времени. А так было бы интересно отследить различные зависимости ещё и от размера страницы. UPD: готовится вторая часть статьи, в ней будем парсить сайты из TOP1000 Alexa.

Тесты запускал последовательно на Ubuntu 3.5.0-19-generic x86_64, процессор Intel Core i7-3930K CPU @ 3.20GHz × 12. (Нафига 12 ядер, если тесты запускать последовательно? Эхх...)

Код


Весь код доступен на github. Можете попробовать запустить самостоятельно, подробные инструкции есть в README файле. Даже нет — настоятельно рекомендую не верить мне, а проверить как поведут себя тесты на вашем окружении!
Tip: если хотите протестировать только часть платформ (например, не хотите устанавливать себе Erlang или PyPy), то это легко задается переменной окружения PLATFORMS.
Буду рад пулл-реквестам с реализацией парсеров на других языках (PHP? Java? .NET? Ruby?), постараюсь добавить в результаты (в первую очередь интересны нативные реализации — биндинги к libxml как правило не отличаются по скорости). Интересно было бы попробовать запустить тесты на каких-нибудь других интересных HTML файлах (большая вложенность тегов, различные размеры файлов).

Результаты


Вот сырые результаты измерений в виде CSV файлов results-1000.csv results-600.csv results-400.csv results-100.csv results-50.csv results-10.csv. Попробуем их проанализировать, для этого воспользуемся скриптом на языке R (находится в репозитории с бенчмарком в папке stats/).

Скорость


Для исследования зависимости скорости работы парсера от числа итераций, построим гистограммы зависимости [времени на обработку одной страницы] от [числа итераций]. Время на обработку одной страницы считаем как время работы парсера, разделенное на число итераций. В идеальном варианте скорость работы парсера не должна зависеть от числа итераций, а лучше — должна возрастать (за счет JIT например).
Все графики кликабельны! Не ломайте глаза!

Зависимость времени на обработку документа от числа итераций парсера (здесь и далее — для каждой HTML страницы отдельный график).
html_parser_bench_pre-001 html_parser_bench_pre-002 html_parser_bench_pre-003 html_parser_bench_pre-004
Столбики одинаковой высоты — хорошо, разной — плохо. Видно, что для большинства парсеров зависимости нет (все столбики одинаковой высоты). Исключение составляют только BeautifulSoup 4 и html5lib под PyPy; по какой-то причине у них с ростом числа итераций производительность снижается. То есть если ваш парсер на PyPy должен работать продолжительное время, то производительность будет постепенно снижаться. Неожиданно…

Теперь самый интересный график — средняя скорость обработки одной странички каждым парсером. Построим box-plot диаграмму.
Среднее время на обработку документа.
html_parser_bench_pre-005 html_parser_bench_pre-006 html_parser_bench_pre-007 html_parser_bench_pre-008
Чем выше находится бокс — тем медленнее работает парсер. Чем больше бокс по площади, тем больше разброс значений (т.е. выше зависимость производительности от числа итераций). Видно, что парсер на C лидирует, за ним следуют lxml.etree, почти вплотную парсеры на NodeJS и Erlang, потом bsoup3 парсер на PyPy, парсеры на CPython и потом с большим отрывом те же парсеры, запущенные на PyPy. Вот так сюрприз! PyPy всем сливает.
Ещё одна странность — bsoup 3 парсеру на Python чем-то не понравилась страничка википедии :-).

Пример табличных данных:
> subset(res, (file=="page_google.html") & (loops==1000))[ c("platform", "parser", "parser.s", "real.s", "user.s") ]
    platform                parser   parser.s real.s user.s
6  c-libxml2 libxml2_html_parser.c   2.934295   2.93   2.92
30    erlang     mochiweb_html.erl  13.346997  13.51  13.34
14    nodejs     cheerio_parser.js   5.303000   5.37   5.36
38    nodejs  htmlparser_parser.js   6.686000   6.72   6.71
22    nodejs       jsdom_parser.js  98.288000  98.42  98.31
33      pypy      bsoup3_parser.py  40.779929  40.81  40.62
57      pypy      bsoup4_parser.py 434.215878 434.39 433.91
41      pypy    html5lib_parser.py 361.008080 361.25 360.46
65    python      bsoup3_parser.py  78.566026  78.61  78.58
49    python      bsoup4_parser.py  33.364880  33.45  33.43
60    python    html5lib_parser.py 200.672682 200.71 200.70
67    python        lxml_parser.py   3.060202   3.08   3.08


Память


Теперь посмотрим на использование памяти. Сперва посмотрим, как потребление памяти зависит от числа итераций. Снова построим гистограммы. В идеале все столбики одного парсера должны быть одинаковой высоты. Если потребление растет с увеличением числа итераций, то это указывает на утечки памяти или проблемы со сборщиком мусора.
Потребление памяти в зависимости от числа итераций парсера.
html_parser_bench_pre-009html_parser_bench_pre-010
Занятно. Bsoup4 и html5lib под PyPy заняли по 5Гб памяти после 1000 итераций по 1Мб файлу. (Привел здесь только 2 графика, т.к. на остальных такая же картина). Видно, что с ростом числа итераций практически линейно растет и потребляемая память. Получается, что PyPy просто не совместим с Bsoup4 и html5lib парсерами. С чем это связано и кто виноват я не знаю, но зато понятно, что использование PyPy без тщательной проверки совместимости со всеми используемыми библиотеками — весьма рискованное занятие.
Выходит, что комбинация PyPy с этими парсерами выбывает. Попробуем убрать их с графиков:
Потребление памяти в зависимости от числа итераций парсера (без Bsoup4 и html5lib на PyPy).
html_parser_bench_dropped_pre-009 html_parser_bench_dropped_pre-010 html_parser_bench_dropped_pre-011 html_parser_bench_dropped_pre-012
Видим, что для парсера на C все столбики практически идентичной высоты. То же самое для lxml.etree. Для большинства парсеров потребление памяти при 10 итерациях немного меньше. Возможно просто time не успевает её замерить. NodeJS парсер jsdom ведет себя довольно странно — у него потребление памяти для некоторых страниц скачет весьма случайным образом, но в целом виден рост потребления памяти со временем. Возможно какие-то проблемы со сборкой мусора.

Сравним усредненное потребление памяти для оставшихся парсеров. Построим box-plot.
Усредненное потребление памяти.
html_parser_bench_dropped_pre-013 html_parser_bench_dropped_pre-014 html_parser_bench_dropped_pre-015 html_parser_bench_dropped_pre-016
Видим, что расстановка примерно такая же, как в сравнении скорости, но у Erlang потребление памяти оказалось ниже, чем у NodeJS. lxml.etree требует памяти примерно в 2 раза больше, чем C libxml2, но меньше чем любой другой парсер. NodeJS парсер jsdom несколько выпадает из общей картины, потребляя ~ в 2 раза больше памяти, чем другие NodeJS парсеры — видимо у него значительный оверхед, связанный с созданием дополнительных атрибутов у элементов DOM дерева.
Пример табличных данных:
> subset(res, (file=="page_google.html") & (loops==1000))[ c("platform", "parser", "maximum.RSS") ]
    platform                parser maximum.RSS
6  c-libxml2 libxml2_html_parser.c        2240
30    erlang     mochiweb_html.erl       21832
14    nodejs     cheerio_parser.js       49972
38    nodejs  htmlparser_parser.js       48740
22    nodejs       jsdom_parser.js      119256
33      pypy      bsoup3_parser.py       61756
57      pypy      bsoup4_parser.py     1701676
41      pypy    html5lib_parser.py     1741944
65    python      bsoup3_parser.py       42192
49    python      bsoup4_parser.py       54116
60    python    html5lib_parser.py       45496
67    python        lxml_parser.py        9364


Оверхед на запуск программы


Это уже не столько тест HTML парсера, сколько попытка выяснить какую платформу стоит использовать для написания консольных утилит. Просто небольшое дополнение (раз у нас уже есть данные). Оверхед платформы — это время, которое программа тратит не на непосредственно работу, а на подготовку к ней (инициализация библиотек, считывание HTML файла и т.п.). Что бы его вычислить, вычтем из времени, которое вывела утилита time — «time.s», время, которое замерили вокруг цикла парсера — «parser.s».

html_parser_bench_overhead_pre-001

Видно, что оверхед в большинстве случаев несущественный. Примечательно, что у Erlang он сравнимый с Python. Можно предположить, что он зависит от массивности библиотек, которые программа импортирует.

Выводы


Как видим — реализация на C впереди планеты всей (но и кода в ней получилось побольше).

Биндинг libxml2 к питону (lxml.etree.HTML) работает практически с такой же скоростью, но потребляет в 2 раза больше памяти (скорее всего оверхед собственно интерпретатора). Выходит, предпочтительный парсер на Python — это lxml.

Парсер на голом Erlang показывает на удивление высокие результаты, несмотря на приписываемые эрлангу «копирования данных на каждый чих» ©. Скорость сравнима с простыми парсерами на NodeJS и выше, чем у Python парсеров. Потребление памяти ниже только у C и lxml. Стабильность отличная. Такой парсер можно выпускать в продакшн (что я и сделал).

Простые парсеры на NodeJS работают очень быстро — в 2 раза медленнее сишной libxml. V8 работает крайне эффективно. Но памяти потребляют на уровне с Python, причем память расходуется не слишком стабильно (расход памяти может вырасти при увеличении числа итераций с 10 до 100, но потом стабилизируется). Парсер jsdom для простого парсинга явно не подходит, т.к. у него слишком высокие накладные расходы. Так что для парсинга HTML в NodeJS лучший выбор — cheerio.

Парсеры на чистом Python сливают как по скорости, так и по потреблению памяти, причем результаты сильно скачут на разных страницах. Но при этом ведут себя стабильно на разном количестве итераций (GC работает равномерно?)

Но больше всех удивил PyPy. То ли какие-то проблемы с GC, то ли задача для него не подходящая, то ли парсеры реализованы неудачно, то ли я где-то накосячил, но производительность парсеров на PyPy снижается с ростом числа итераций, а потребление памяти линейно растет. Bsoup3 парсер более-менее справляется, но его показатели находятся на уровне с CPython. Т.е. для парсинга на PyPy подходит только Bsoup3, но заметных преимуществ перед CPython он не дает.

Ссылки


Код бенчмарка Мой блог
Document Object Model

Присылайте свои реализации! Это очень просто!

UPD:
Результаты для добавленных парсеров (графики, CSV):
golang (3 шт, один быстрее С(!!!))
haskell (крайне неплохо)
java (openjdk, oracle-jre) (автоматически параллелится, занимая ~150% CPU, user CPU > real CPU)
perl
php + tidy
ruby (биндинг к libxml)
dart (очень медленный)
mono (2 шт) (автоматически параллелится, занимая ~150% CPU, user CPU > real CPU)

Полноценную статью + исследование зависимости скорости и потребления памяти от размера страницы по TOP1000 Alexa будет позже (надеюсь).
+60
36244
240
seriyPS 58,4

Комментарии (35)

+1
soomrack, #
Если Вам не сложно, сравните, пожалуйста с парсером HTML::TreeBuilder (Perl). Я пользуюсь исключительно им.
+1
seriyPS, #
Присылайте код — добавлю c удовольствием.
Добавить очень просто — см github.com/seriyps/html-parsers-benchmark#how-to-add-my-platformname-to-benchmark-set
0
inikulin, #
jsdom использует упомянутый htmlparser непосредственно для парсинга страницы, более того он на выходе дает полноценный DOM, а не AST. ИМХО, я бы его выкинул из сравнения, т.к. по сути это не совсем чистый парсер
0
seriyPS, #
Спасибо за пояснение. Тем не менее, кто-то jsdom использует и для простого парсинга — habrahabr.ru/qa/29711/
+5
dmitriid, #
1. Емнип, сами парсеры разной сложности, из-за чего может быть разброс по памяти скорости и т.п. То есть если какой-то парсер в процессе еще жнет, шьет и на дуде играет, то… Но я не спец по этим парсерам, поэтому ничего не смогу сказать

2. Erlang порадовал, сам не ожидал, но, не глядя в код, можно сделать следующие предположения:
— файл считывается, как binary, после чего вычленение нужных участков становится во-первых легким (google: erlang bit syntax), а во-вторых эффективным (см. пункт 3)

3. Про «копирование всего и вся» и память
— из-за немутабельности данных сборщик мусора с одной стороны простой, с другой — эффективный. Никаких stop-the-world и прочего, неиспользованные данные убираются из памяти весьма оперативно
— Для большинства данных в пределах одного процесса копируется только указатель на структуру, содержащую данные, а сами данные упакованы весьма эффективно. При передаче из процесса в процесс данные копируются
— В случае двоичных данных больше определенного размера эти данные существуют в одном экземпляре на ноду (не процесс, а именно ноду), и повсюду передается только указатель на эти данные.
Поэтому потребление памяти может быть весьма маленьким по сравнению с другими участниками

4. Оверхед на запуск — есть такое, потому что запускается много придожений и подгружается много кода. Можно сделать минимальную систему, запускающую только полтора приложения и т.п., но в реальном приложении это не так критично, потому что VM запускается один раз, а потом работает до скончания времен

5. Есть еще sax-парсеры для Эрланга (как и для других языков), по идее они должны смочь обработать невалидный HTML
+1
seriyPS, #
1. Да, всё верно. Например jsdom делает много дополнительной работы, а тот же mochiweb_html или cheerio строят простейшее дерево. Но в контексте бенчмарка это не важно. Цель как раз показать, что использовать слишком навороченные парсеры для простых задач не стоит.

2. Да, mochiweb_html парсит всё в бинарном виде, дерево тоже состоит из таплов и бинарей (листы только для списков атрибутов и дочерних тегов). В процессе парсинга в контексте передается текущая позиция парсера как сдвиг в исходном HTML binary, т.е. он делает примерно так
S=#decoder{offset=O}
...
<<_:O/binary, C, _/binary>> when ?IS_WHITESPACE(C) ->
            tokenize_attributes(B, ?INC_CHAR(S, C), Acc);


3. Мне вот интересно, Erlang парсер в итоге строит структуру наподобие
{<<"html">>, [{<<"lang">>, <<"ru-RU">>}], [
    {<<"head">>, [], []},
    {<<"body">>, [], [<<"Hello, World!">>]}
]}
Интересно все вот эти мелкие бинари на самом деле являются ссылками на участки исходного большого бинари (бинари слайс это вроде называется), или создаются отдельные маленькие копии участков памяти?

4. Да, я упомянул оверхед на запуск мельком просто потому что были все нужные данные чтобы его посчитать, так это не особо интересная информация.

5. Да, SAX несомненно будет и быстрее и экономнее по памяти (mochiweb_html например в 2 этапа парсит — сперва строит чем-то вроде SAX парсера плоский список токенов, а на втором этапе генерирует из него вложенное дерево). Я хотел померять именно DOM парсеры.
0
dmitriid, #
> Интересно все вот эти мелкие бинари на самом деле являются ссылками на участки исходного большого бинари

Таки на участки исходного binary. См. erlanger.ru/ru/page/2431/erlang-binaries-i-sborka-musora-i-tyazhelyj-vzdoh

«Более того, Erlang проводит некоторые дополнительные оптимизации. При использовании erlang:split_binary/2 или при извлечении данных при помощи сопоставления с образцом результат не обязательно является новым binary. Вместо этого VM создает новый тип под названием sub-binary, который на деле является ссылкой на оригинальные данные — на heap binary или на refc-binary.»

Из-за этого, видать, потребление памяти относительно низкое — все лежит в одном большом двоичном куске и расходы только на указатели
0
seriyPS, #
Это здорово. Недавно наткнулся на другой бенчмарк XML парсеров, в котором с огромным отрывом побеждал парсер на языке D (бенчмарк пояснения). Так вот, он выигрывал как раз в основном за счет zero-copy — т.е. не копировал куски исходного HTML, а ссылался на них (array-slicing).
Но он не раскодировал XML entities, так что &lt; не превращается в <. Если бы не это допущение, то пришлось бы автору парсера придумывать какие-то хаки, чтобы оставаться zero-copy.
В эрланге это можно оптимизировать, используя io_lists наподобие [<<"бинари до entity">>, <<"<">>, <<"бинари после entity">>], хотя работать с таким уже менее удобно.
–12
artem_dev, #
> Вот так сюрприз! PyPy всем сливает.
Не понимаю, что вас удивило — PyPy является интерпретатором Python написанный на Python, откуда там взяться производительности?
+5
evilbloodydemon, #
Где вы этой ереси все набираетесь?
0
artem_dev, #
Я достаточно давно его пробовал, несколько лет назад там все было на Python, хорошо что добавили новых бэкендов.
+6
kost_bebix, #
Он не на python написан, а на RPython, это (синтаксически) подмножество python, но по сути Си-подобный язык (который, собственно, в си и транслируется).
0
AterCattus, #
Есть предположение, что тормоза и аппетиты нативных реализаций на python являются следствием immutable типов данных, которые копируются при каждом изменении. А ведь к ним, применимо к парсингу текста, относятся строки и кортежи.

И я рад за наличие у python такого качественного libxml2 биндинга.
0
dmitriid, #
> Есть предположение, что тормоза и аппетиты нативных реализаций на python являются следствием immutable типов данных, которые копируются при каждом изменении.

Это проблема невпихуемости невпихуемого :) То есть рантайм Питона не приспособлен к работе с такими данными и, видать, GC тупит.
0
jetman, #
Присылайте свои реализации! Это очень просто!
На гитхабе уже прислали реализации для ruby и golang. Обновляйте пост!
+1
seriyPS, #
Доберусь до дома и запущу в том-же окружении. Спасибо!
+2
tulskiy, #
Я там еще добавил реализацию на джаве на JSoup, не забывайте про нас :)
+1
seriyPS, #
Ага, я видел. Только вы таймер стартуете до того, как файл считали, а в других парсерах таймер стоит непосредственно вокруг цикла. Я там в комментарии к пулл-реквесту написал.
Хотя может это и не так существенно.
+1
seriyPS, #
К сожалению, пока не успеваю добавить на графики, но кому интересно — вот результаты для 100 итераций для perl, Go и Ruby
Скрытый текст
$ PLATFORMS="golang perl ruby" ./run.sh 100
===============
golang
===============
******************************
parser:./bin/bench_exp_html	file:../page_google.html
0.270294 s
real:0.27	user:0.27	sys:0.00	max RSS:4540
******************************
parser:./bin/bench_gokogiri	file:../page_google.html
0.315391 s
real:0.32	user:0.28	sys:0.03	max RSS:78196
******************************
parser:./bin/bench_h5	file:../page_google.html
5.436318 s
real:5.43	user:5.42	sys:0.00	max RSS:6716
******************************
parser:./bin/bench_exp_html	file:../page_habrahabr-70330.html
5.826405 s
real:5.83	user:5.78	sys:0.02	max RSS:50492
******************************
parser:./bin/bench_gokogiri	file:../page_habrahabr-70330.html
6.471175 s
real:6.56	user:5.89	sys:0.65	max RSS:1943176
******************************
parser:./bin/bench_h5	file:../page_habrahabr-70330.html
96.829145 s
real:96.83	user:96.51	sys:0.02	max RSS:62356
******************************
parser:./bin/bench_exp_html	file:../page_habrahabr-index.html
0.293615 s
real:0.29	user:0.29	sys:0.00	max RSS:4828
******************************
parser:./bin/bench_gokogiri	file:../page_habrahabr-index.html
0.361011 s
real:0.36	user:0.34	sys:0.02	max RSS:97308
******************************
parser:./bin/bench_h5	file:../page_habrahabr-index.html
4.111917 s
real:4.11	user:4.09	sys:0.00	max RSS:5472
******************************
parser:./bin/bench_exp_html	file:../page_wikipedia.html
0.370002 s
real:0.37	user:0.36	sys:0.00	max RSS:5100
******************************
parser:./bin/bench_gokogiri	file:../page_wikipedia.html
0.343510 s
real:0.35	user:0.31	sys:0.04	max RSS:106832
******************************
parser:./bin/bench_h5	file:../page_wikipedia.html
4.674953 s
real:4.67	user:4.64	sys:0.01	max RSS:6564
===============
perl
===============
******************************
parser:mojo_parser.pm	file:../page_google.html
3.89669394493103 s
real:4.02	user:3.98	sys:0.01	max RSS:8096
******************************
parser:mojo_parser.pm	file:../page_habrahabr-70330.html
75.7632319927216 s
real:75.81	user:75.56	sys:0.01	max RSS:36384
******************************
parser:mojo_parser.pm	file:../page_habrahabr-index.html
3.66110587120056 s
real:3.70	user:3.68	sys:0.00	max RSS:8288
******************************
parser:mojo_parser.pm	file:../page_wikipedia.html
4.00335907936096 s
real:4.04	user:4.01	sys:0.01	max RSS:8404
===============
ruby
===============
******************************
parser:nokogiri_parser.rb	file:../page_google.html
0.343421056 s
real:0.39	user:0.37	sys:0.01	max RSS:13124
******************************
parser:nokogiri_parser.rb	file:../page_habrahabr-70330.html
8.438090464 s
real:8.49	user:8.45	sys:0.00	max RSS:36876
******************************
parser:nokogiri_parser.rb	file:../page_habrahabr-index.html
0.363908843 s
real:0.40	user:0.40	sys:0.00	max RSS:14352
******************************
parser:nokogiri_parser.rb	file:../page_wikipedia.html
0.378928282 s
real:0.41	user:0.41	sys:0.00	max RSS:14776

$ PLATFORMS="c-libxml2" ./run.sh 100  # добавил просто для сравнения
===============
c-libxml2
===============
******************************
parser:libxml2_html_parser.c	file:../page_google.html
0.306867 s
real:0.31	user:0.28	sys:0.00	max RSS:2244
******************************
parser:libxml2_html_parser.c	file:../page_habrahabr-70330.html
6.887496 s
real:6.89	user:6.80	sys:0.06	max RSS:24292
******************************
parser:libxml2_html_parser.c	file:../page_habrahabr-index.html
0.291910 s
real:0.29	user:0.28	sys:0.00	max RSS:2412
******************************
parser:libxml2_html_parser.c	file:../page_wikipedia.html
0.374730 s
real:0.37	user:0.37	sys:0.00	max RSS:2504


На подходе Java и Haskell
0
homm, #
> Проверять качество работы парсера в плане полноты восстановления поломанных документов не будем.
А зря, как мне кажется. Html5 включает обработку любых ошибок в документе, поэтому строго говоря, любой текст — валидный html5-документ и должен одинаково разбираться всеми парсерами.
+4
KEKSOV, #
Добавил тест для связки Tidy-SimpleXML на PHP
+1
Framework, #
Решил проверить Dart. Замена же JS как ни как. Может я его не умею готовить, но результаты какие-то совсем печальные.

Скрытый текст
$ PLATFORMS="dart" ./run.sh 100
===============
dart
===============
******************************
parser:html5lib_parser.dart	file:../page_google.html
38864
real:39.13	user:38.76	sys:0.27	max RSS:73740
******************************
parser:html5lib_parser.dart	file:../page_habrahabr-70330.html
686300
real:687.00	user:679.05	sys:5.72	max RSS:269588
******************************
parser:html5lib_parser.dart	file:../page_habrahabr-index.html
34617
real:34.88	user:34.48	sys:0.26	max RSS:80804
******************************
parser:html5lib_parser.dart	file:../page_wikipedia.html
36756
real:37.01	user:36.53	sys:0.33	max RSS:76560

+4
seriyPS, #
И так, текущий список платформ:

# были изначально
pypy
python
c-libxml2
erlang
nodejs
# добавились
golang (3 шт)
haskell
java
perl
php + tidy
ruby
dart

Спасибо тем, кто присылал свои варианты парсеров!
В ближайшее время потестирую добавленные парсеры и обновлю статью (или даже вторую часть напишу).
+2
seriyPS, #
Сделал небольшой апдейт — прикрепил результаты для новых парсеров и платформ. К сожалению, пока только как архив с CSV и графиками.
0
Artima, #
Я смотрю php + tidy весьма позитивно выглядит в тестах, ожидал более плохого результата.
0
KEKSOV, #
Чем хорош PHP это тем, что если используемые библиотеки хорошо написаны, в данном случае на C/C++, то и скорость будет хорошей, оверхед на вызов самих функций минимальный. Совсем другое дело, когда какая-то тяжелая либа написана на самом интерпретаторе, то тогда да — туши свет, покупай апгрейд CPU…
0
KEKSOV, #
Надо бы другие варианты еще потестить. Мне кажется, он может еще быстрее :)
0
seriyPS, #
Думаю можно утилизировать возможности libxml как то так php.net/manual/en/domdocument.loadhtml.php в комбинации с php.net/manual/en/class.domdocument.php#domdocument.props.recover и php.net/manual/en/class.domdocument.php#domdocument.props.stricterrorchecking

Что то вроде
$doc = new DomDocument();
$doc->recover = TRUE;
$doc->stricterrorchecking = FALSE;
$doc->loadHTML($html_string);

(сам не проверял)
0
seriyPS, #
Кстати, когда пробовал запустить php+tidy парсер на top1000 Alexa (тест на зависимость от размера HTML файла), получил 100500 сообщений об ошибках / Warnings, пришлось php для этого теста убрать. Если посоветуете как это поправить — будет здорово.

parser:tidy_simplexml.php       file:../pages/page_10086.cn.html
PHP Warning:  simplexml_load_string(): Entity: line 2: parser error : PEReference in prolog in /home/seriy/workspace/html_parser_bench/php-tidy/tidy_simplexml.php on line 43
PHP Warning:  simplexml_load_string():  "%20http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> in /home/seriy/workspace/html_parser_bench/php-tidy/tidy_simplexml.php on line 43
PHP Warning:  simplexml_load_string():   ^ in /home/seriy/workspace/html_parser_bench/php-tidy/tidy_simplexml.php on line 43
******************************
parser:tidy_simplexml.php       file:../pages/page_163.com.html
PHP Warning:  simplexml_load_string(): Entity: line 1192: parser error : Char 0xDF71 out of allowed range in /home/seriy/workspace/html_parser_bench/php-tidy/tidy_simplexml.php on line 43
PHP Warning:  simplexml_load_string(): ���<U+DF71>2012���<U+05FD>����ѡ</a> in /home/seriy/workspace/html_parser_bench/php-tidy/tidy_simplexml.php on line 43
PHP Warning:  simplexml_load_string():          ^ in /home/seriy/workspace/html_parser_bench/php-tidy/tidy_simplexml.php on line 43
PHP Warning:  simplexml_load_string(): Entity: line 1192: parser error : PCDATA invalid Char value 57201 in /home/seriy/workspace/html_parser_bench/php-tidy/tidy_simplexml.php on line 43
******************************
parser:tidy_simplexml.php       file:../pages/page_addthis.com.html
PHP Warning:  simplexml_load_string(): namespace error : Namespace prefix addthis for userid on a is not defined in /home/seriy/workspace/html_parser_bench/php
-tidy/tidy_simplexml.php on line 43
PHP Warning:  simplexml_load_string(): addthis:userid="AddThis"></a>  in /home/seriy/workspace/html_parser_bench/php-tidy/tidy_simplexml.php on line 43
PHP Warning:  simplexml_load_string():                         ^ in /home/seriy/workspace/html_parser_bench/php-tidy/tidy_simplexml.php on line 43
PHP Warning:  simplexml_load_string(): namespace error : Namespace prefix addthis for userid on a is not defined in /home/seriy/workspace/html_parser_bench/php-tidy/tidy_simplexml.php on line 43
PHP Warning:  simplexml_load_string(): addthis:userid="addthis"></a>  in /home/seriy/workspace/html_parser_bench/php-tidy/tidy_simplexml.php on line 43
PHP Warning:  simplexml_load_string():                         ^ in /home/seriy/workspace/html_parser_bench/php-tidy/tidy_simplexml.php on line 43
PHP Warning:  simplexml_load_string(): namespace error : Namespace prefix addthis for usertype on a is not defined in /home/seriy/workspace/html_parser_bench/php-tidy/tidy_simplexml.php on line 43
PHP Warning:  simplexml_load_string(): addthis:usertype="company" addthis:userid="167173"></a>  in /home/seriy/workspace/html_parser_bench/php-tidy/tidy_simplexml.php on line 43
PHP Warning:  simplexml_load_string():                                                   ^ in /home/seriy/workspace/html_parser_bench/php-tidy/tidy_simplexml.php on line 43

и т.п. Причем остальные парсеры с этим нормально справляются как то.
0
andrewsch, #
Скачал архив с «Save link as» в FireFox — выдает ошибку прираспаковке, но tar вроде нормальный

$ bunzip2 html_parser_bench.tar.bz2

bunzip2: html_parser_bench.tar.bz2: trailing garbage after EOF ignored
0
seriyPS, #
Попробуйте ещё раз скачать (обновил конфиг Nginx). Возможно были проблемы с конвертацией переносов строк или ещё чем-то (хотя у меня через Save as в FF нормально распаковывал в Linux)
0
andrewsch, #
Да не, я говорю — несмотря на ошибку, тар был нормальный и все распаковалось.
Сейчас перекачал — то же самое.

$ md5sum html_parser_bench.tar.bz2
cbe8903fc6803587900d474d35053793 html_parser_bench.tar.bz2

У меня Debian testing
0
seriyPS, #
На сервере прям проверил
/var/www/dl.seriyps.ru/docs$ md5sum html_parser_bench.tar.bz2 
cbe8903fc6803587900d474d35053793  html_parser_bench.tar.bz2


Совпадает
0
andrewsch, #
Значит моя локальная проблема
0
jetman, #
Отлично! Возможно для статьи пригодится ссылка на goquery: github.com/opesun/goquery
Jquery style selector engine for HTML documents, in Go.
Использует exp/html для парсинга HTML, но не знаю, модифицированный или нет.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.