Pull to refresh

Как работает парсер Mediawiki

Reading time 6 min
Views 9.7K

Перевод статьи The MediaWiki parser, uncovered.

Актуальность перевода статьи 2009 года в том, что, во-первых, костяк парсера с тех пор существенно не поменялся, и во-вторых, с ним приходится ежедневно сталкиваться тем, кто пишет расширения для Mediawiki, на котором крутятся их корпоративные сайты.


Парсер Mediawiki — фундаментальная часть кода движка Mediawiki. Без него вы бы не смогли вставлять в свои статьи Википедии различные метки: секции, ссылки или картинки. Вы даже не смогли бы увидеть или быстро изменить разметку других статей. Вики-разметка достаточно гибка, чтобы дать возможность одинаково легко писать статьи как новичкам, так и HTML-экспертам. Из-за этого код парсера несколько сложноват, и с годами проходил через множество попыток его улучшить. Тем не менее, даже сегодня он все еще достаточно быстро работает для Википедии, одного из самых больших веб-сайтов в мире. Давайте взглянем на внутренности этого ценного (но чуть-чуть заумного) куска кода.


Краткая история

Дисклеймер. Эта история, как я ее понимаю, в основном почерпнута из обсуждений, в которых я участвовал в течение долгих лет в рассылке Викимедии, а также из обсуждений на конференции Викимания 2006. До 2008 года парсер Mediawiki сильно страдал от необычайной сложности, которая проистекала из необходимости уложиться в один проход (для скорости), но также и из-за того, что в существующий код всё время добавляли новые правила. С течением времени код парсера стал настоящим спагетти-кодом, который было очень сложно отлаживать, и еще сложнее улучшать. Переписать его было практически невозможно, т.к. он относится к ядру движка. Миллионы страниц Википедии могли полететь в один момент, если бы в каком-то месте нового кода произошла ошибка.

Что делать

Разразилось множество дискуссий о том, как решить эту проблему. Кто-то предлагал переписать парсер на C, который бы сделал парсер быстрее, что позволило бы разрешить парсеру разбирать текст не за один проход, а в цикле — этого требовало нарастающее число шаблонов и подшаблонов, которые включались на страницах Википедии. Были также и предложения изменить синтаксис Mediawiki так, чтобы устранить неопределенности при разборе некоторых конструкций (как, например, '''''жирный или курсив?''жирный или курсив?''''' или отношения между тройными и двойными фигурными скобками в шаблонах).
В конце концов было решено, и я считаю это блестящей идеей, оставить парсер на PHP (т.к. переписывание его на C привело бы к разделению разработчиков Mediawiki на 2 класса) и разделению парсинга на два этапа, препроцессинг и парсинг. Работой препроцессора стало представление викитекста в виде XML DOM. На этапе собственно парсинга происходила обработка дерева DOM в цикле из стольких итераций, сколько требовалось (для подстановки шаблонов. к примеру), чтобы получить валидный статический HTML. Прохождение в цикле по DOM невероятно быстро, и кроме того это очень естественно с точки зрения XHTML. И в PHP такую обработку тоже очень хорошо поддерживает.

Препроцессор

В папке исходников Mediawiki вы найдете две версии препроцессора, версию Hash и DOM, их можно найти соответственно по адресам /includes/parser/Preprocessor_Hash.php и /includes/parser/Preprocessor_DOM.php.
Мы сконцентрируемся на DOM-версии, так как она практически идентична Hash-версии, но работает быстрее, потому что пользуется PHP XML-поддержкой, опциональной компонентой PHP. Самая важная функция в классе препроцессора назвается preprocessToObj(). Внутри файла Preprocessor_DOM.php лежат несколько других важных классов, которые использует препроцессор: PPDStack, PPDStackElement, PPDPart, PPFrame_DOM и PPNode_DOM.

Препроцессор делает меньше, чем вы думаете

Итак, на что же походит Mediawiki XML? Вот пример того, как викитекст "{{МойШаблон}} это [[тест]]" выглядит в XML-представлении:
<root>
  <template>
    <title>mytemplate</title>
   </template>
  this is a [[test]]
</root>


Заметим. что внутренняя ссылка вообще не обрабатывается. Код препроцессора избегает той работы, которую можно сделать на более позднем этапе (и у него есть для этого основания), так что единственной настоящей работой препроцессора является создание XML элементов для шаблонов и парочки других вещей. Вот эти вещи, т.е. базовые узлы (полный перечень): template, tplarg, comment, ext, ignore, h.

Если вы когда-либо работали с викитекстом, то уже знаете, какая разметка соответствует этим базовым узлам. На всякий случай, вот она:

  • template = двойные фигурные скобки {{...}}
  • tplarg = тройные фигурные скобки {{{...}}}
  • comment = HTML-комментарий любого типа
  • ext = Всё, что должно разбираться в отдельном расширении
  • ignore = теги типа noinclude, а также теги includeonly и содержимое внутри них
  • h = секции


Вот и все. Все остальное игнорируется и возвращается как исходный викитекст в парсер.

Как работает препроцессор

Тут нет ничего особенного, но стоит сказать несколько слов. Для того, чтобы получить XML-представление, нужное нам, препроцессор проходит викитекст в цикле столько раз, сколько символов содержит этот текст. Нет другого способа для корректной обработки рекурсивных шаблонов, которые могут быть представлены как угодно в тексте благодаря синтаксису. Так что, если статья в Википедии содержит 40 000 знаков, то вероятно, что цикл будет состоять из 40 000 итераций. Теперь понятно, почему скорость так важна для парсера.

Собственно парсинг

Пропустим остальные детали препроцессора и дополнительные классы, которые используются для генерации XML-кода. Обратим наше внимание на сам парсер и взглянем на типичный случай работы парсера при клике на статью Википедии. Здесь, правда, не следует забывать, что вики-страницы кешируются любыми возможными способами, так что вряд ли вы каждый раз при клике на страницу вызовете разбор парсером страницы.

Вот типичное дерево вызовов парсера для текущей версии страницы, начиная от объекта Article.
01. Article->view
02. --Article->getContent
03. ----Article->loadContent
04. ------Article->fetchContent->возвращает викитекст, извлеченный из БД
05. --Article->outputWikiText->prepare for the parse
06. ----Parser->parse
07. ------Parser->internalParse
08. --------Parser->replaceVariables
09. ----------Parser->preprocessToDom
10. ------------Preprocessor->preprocessToObj
11. ----------Frame->expand
12. --------Parser->doTableStuff
13. --------Parser->replaceInternalLinks
14. --------Parser->replaceExternalLinks
15. ------Parser->doBlockLevels
16. ------Parser->replaceLinkHolders


Давайте взлянем на эти функции. Снова, это главные функции, а не все, которые вызываются в данном примере. Пункты 2-4 извлекают и возвращают викитекст статьи из базы данных. Этот текст передается объекту outputWikiText, который подготавливает его для передачи функции Parser::parse().
Далее снова интересно становится на пунктах 8-11. Внутри функции replaceVariables текст превращается в представление DOM, в цикле по каждому символу статьи ищутся начальные и конечные метки шаблонов, подшаблонов и других узлов, упомянутых выше.

Пункт номер 11 — интересный шаг, который я сейчас пропущу, потому что он требует некоторых знаний о других классах из файла Preprocessor_DOM.php (упомянутых выше). Expand — очень важная функция, делает кучу вещей (среди которых её рекурсивный вызов), но достаточно сказать, что она выполняет работу по фактическому извлечению текста из узлов DOM (мы помним. что шаблоны могут быть вложенными — так что вы не всегда получите полный текст из каждой включенной статьи) и воврату валидного HTML-текста, в котором раскрыты все вики-метки, за исключением трех видов: таблиц, ссылок и списков. Так что, в примере выше, "{{МойШаблон}} это [[тест]]" expand() вернет текст вида:

«Я включил [[текст]] из моего шаблона, это [[тест]]»

Как вы можете видеть из этого простого примера, на данном этапе разбирается все, за исключением таблиц, ссылок и списков.

Ссылки — это особенный случай

Да, у ссылок есть своя отдельная секция. И не только потому, что они, пожалуй, самая существенная часть того, что делает из wiki её саму (помимо возможности редактирования). Но еще и потому, что они разбираются совершенно особенным от всей остальной разметки образом в коде парсера. Особенными их делает то, что они обрабатываются в два этапа: на первом этапе каждой ссылке присваивается уникальный id, а на втором этапе её валидный HTML вставляется на место «заместителя ссылки» (link holder). В нашем примере вот какой результат получается после первого этапа:

«Я включил <!--LINK 0--> из моего шаблона, это <!--LINK 1-->».

Как вы можете представить, существует также массив, который сопоставляет текст ссылок с их ID вида LINK #ID, это переменная класса Parser с именем mLinkHolders. Помимо сопоставления, эта переменная также хранит объекты Title для каждой ссылки.

Так что на втором этапе разбора ссылок мы используем этот массив для простого поиска и замены. Теперь всё! Отправляем готовый текст за дверь!

Следующий этап

В следующем посте я сконцентрируюсь больше на препроцессоре и деталях классов из файла Preprocessor__DOM.php, а именно как они используются для построения первоначального дерева DOM XML. Я также расскажу о том, как я воспользовался ими для кеширования инфобоксов в расширении Unbox.
Tags:
Hubs:
+27
Comments 17
Comments Comments 17

Articles