Калина Алексей блог программиста

XSLT и анализ данных

Всем привет! Сегодняшний пост напрямую связан с языком представления слабоструктурированных данных XML (extensible Markup Language). Думаю, многие из вас так или иначе сталкивались с ним в своей практике. Однако, далеко не все знакомы с XSLT - языком преобразования XML-документов. Он позволяет создавать новые файлы на основе предложенного XML-документа. Причем результирующие документы могут иметь самые разные форматы. С помощью XSLT можно решать достаточно большой класс задач. Одни из наиболее распространенных - это создание отчетов, например в формате HTML, или преобразование документа из одной XML схемы в другую. Сегодня мы займемся менее типичной для XSLT задачей. Наша цель: на основе результатов поисковых запросов к Yandex API, представленных в XML-формате, создать веб-страницу, на которой представлен небольшой статистический анализ произведенных запросов. Этот пример должен продемонстрировать основные возможности языка XSLT.

XSLT 2.0

В июле этого года консорциум всемирной паутины объявил о появлении на свет версии 3.0 языка XSLT. Однако, для решения наших задач я использовал долгие годы применяемую всеми версию 2.0. Принцип работы с XSLT заключается в написании таблице стилей, в которой нужно описать правила преобразования документа XML. Всю остальную работу выполняет процессор XSLT. Я использовал наиболее полный процессор таблиц стилей под названием Saxon.

Перечислим несколько основных идей, заложенных в XSLT:

  1. Каждая таблица преобразований также является документом XML. То есть, существует возможность преобразовать одну таблицу стилей в другую, путем применения третьей.
  2. Логика преобразований основывается на поиске по шаблону. При сопоставлении некоторой части документа конкретному шаблону, она преобразовывается указанным образом.
  3. В коде XSLT не может быть побочных эффектов. Этой чертой язык похож на функциональный. Например, в XSLT так же как и в функциональных языках нельзя переопределять переменные.

При обработке документа процессор XSLT использует следующую логику:

  1. Проверка того, что в текущем контексте остались узлы для обработки. Если остались, то переход к следующему шагу.
  2. Получение следующего узла в контексте и определение наличия элемента <xsl: template>, который ему соответствует.
  3. При наличии соответствия, выполняется обработка шаблона. При этом если обнаружено несколько шаблонов, обрабатывается наиболее конкретный.

Yandex.XML

В одном из своих учебных проектов мне были необходимы результаты поисковых запросов для названий книг. В датасете были десятки тысяч экземпляров, что достаточно усложнило задачу, так как многие хорошие поисковые системы предоставляют очень ограничивающие лимиты на количество запросов в своем API. Например, Google позволяет сделать только 100 бесплатных запросов в сутки.

Тем не менее среди достаточно жестких в этом плане заморских движков, нашелся наш любимый Яндекс, который предоставляет возможность делать поисковый запросы с ограничением в 10 тыс в день. Такой вариант меня вполне устроил. Что важно для нас, результаты запроса Яндекс API возвращает в формате XML. Ниже пример ответа на запрос yandex (некоторые элементы удалены).

<?xml version="1.0" encoding="utf-8"?>
<yandexsearch version="1.0">
<request>
   <query>yandex</query>
   <page>0</page>
   <sortby order="descending" priority="no">rlv</sortby>
   <groupings>
      <groupby  attr="d" mode="deep" groups-on-page="10" docs-in-group="3" curcateg="-1" />
   </groupings>
</request>
<response date="20120928T103130">
   <reqid>1348828873568466-1289158387737177180255457-3-011-XML</reqid>
   <found priority="all">206775197</found>
   <found-human>Нашлось 207 млн ответов</found-human>
   <results>
      <grouping attr="d" mode="deep" groups-on-page="10" docs-in-group="3" curcateg="-1">
         <found priority="all">45094</found>
         <found-docs priority="all">192685602</found-docs>
         <found-docs-human>нашёл 193 млн ответов</found-docs-human>
         <page first="1" last="10">0</page>
         <group>
            <doccount>34</doccount>
            <doc id="ZD831E1113BCFDD95">
               <relevance priority="phrase" />
               <url>https://www.yandex.ru/</url>
               <domain>www.yandex.ru</domain>
               <title>&quot;<hlword>Яндекс</hlword>&quot; - поисковая система и интернет-портал</title>
               <headline>Поиск по всему интернету с учетом региона пользователя.</headline>
               <modtime>20060814T040000</modtime>
               <size>26938</size>
               <charset>utf-8</charset>
               <passages>
                  <passage><hlword>Яндекс</hlword> — поисковая машина, способная по вашему запросу...</passage>
               </passages>
               <properties>
                   <_PassagesType>0</_PassagesType>
                   <lang>ru</lang>
               </properties>
               <mime-type>text/html</mime-type>
            </doc>
         </group>
         <!-- Другие результаты -->
      </grouping>
   </results>
</response>
</yandexsearch>

Небольшую часть собранных данных я использую для демонстрации возможностей XSLT.

Объединение данных

Прежде чем переходить к анализу необходимо подготовить XML-документ, с которым мы будем работать. При загрузке данных из Яндекса, я сохранял результаты в отдельные XML файлы. Для того, чтобы нам было удобно анализировать все результаты в общем, нужно объединить их в один документ. Для этого напишем нашу первую таблицу стилей XSLT.

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="yes"/>
  <xsl:template match="/">
    <queries>
      <xsl:for-each select="collection('../queries/?select=*.xml')" >
        <xsl:copy-of select="document(document-uri(.))"/>
      </xsl:for-each>
    </queries>
  </xsl:template>
</xsl:stylesheet>

Любая таблица стилей должна начинаться с тега <xsl:stylesheet>, в ней мы определяем версию XSLT и пространство имен. В теге <xsl:output> указываем формат выходного файла, и то, что теги будут автоматически форматироваться с учетом иерархии (indent). Далее следует единственный в этой таблице шаблон, который применяется к корню документа. В выходном документе корнем будет тег <queries>. Его дочерними элементами будет содержимое XML-документов из папки queries, которые обходятся в цикле for-each.

Анализ данных

Я проведу простейший анализ, которого хватит чтобы понять, что из себя представляют полученные данные. В качестве изучения рассмотрим количество результатов, которые обнаружила поисковая система в ответ на запрос.

Среднее выборочное

Для подсчета среднего выборочного сначала рассчитаем сумму полученных результатов во всех запросах:

<xsl:variable name="sum">
  <xsl:value-of select=
  "sum(//queries/yandexsearch/response/results/grouping/found-docs[@priority='all'])"/>
</xsl:variable>

Для того, чтобы обращаться к конкретным элементам XML-документа в XSLT используется язык запросов XPath. Символ / обозначает переход к дочернему элементу, квадратные скобки - альтернатива ключевому слову where, а с помощью символа @ происходит доступ к атрибутам элемента. Далее посчитаем количество запросов:

<xsl:variable name="count">
  <xsl:value-of select=
  "count(//queries/yandexsearch/response/results/grouping/found-docs[@priority='all'])"/>
</xsl:variable>

И наконец выведем в HTML документе результат:

<div >
  Общее число запросов: <xsl:value-of select="$count" />
</div>
<div>
  Среднее выборочное: <xsl:value-of select="$sum div $count" />
</div>
Общее число запросов: 997
Среднее выборочное: 1,920,658

Результат получился достаточно большим. Однако, зачастую среднее выборочное - не самая репрезентативная статистика. Давайте посмотрим насколько велик разброс в наших данных.

Порядковые статистики

Посчитаем минимум и максимум нашей выборки. Для этого в XSLT также есть встроенные функции:

<xsl:variable name="min">
  <xsl:value-of select=
  "min(//queries/yandexsearch/response/results/grouping/found-docs[@priority='all'])"/>
</xsl:variable>   
<xsl:variable name="max">
  <xsl:value-of select=
  "max(//queries/yandexsearch/response/results/grouping/found-docs[@priority='all'])"/>
</xsl:variable>

Вместе с полученными значениями выведем в результирующий документ текст самих запросов. Для этого создадим еще один шаблон (все предыдущие преобразования располагались в корневом шаблоне). Кроме того в шаблоне можно указывать параметры.

<xsl:template name="printQueryText">
<xsl:param name="resultsCount" select="."/>
  <xsl:value-of select=
  "//queries/yandexsearch/request/query[../../response/results/grouping/found-docs[@priority='all']=format-number($resultsCount, '#')]"/>
</xsl:template>

Для вывода больших чисел в удобочитаемом виде, применим функцию format-number:

<div>
  Минимум найденных результатов: <xsl:value-of select="format-number($min, '#,###')" />
</div>
<div>
  Текст запроса: <xsl:call-template name="printQueryText">
                  <xsl:with-param name="resultsCount" select="$min"/>
                 </xsl:call-template>
</div>
<div>
  Максимум найденных результатов: <xsl:value-of select="format-number($max, '#,###')" />
</div>
<div>
  Текст запроса: <xsl:call-template name="printQueryText">
                  <xsl:with-param name="resultsCount" select="$max"/>
                 </xsl:call-template>
</div>
Минимум найденных результатов: 8
Текст запроса: Intramvros
Максимум найденных результатов: 335,365,787
Текст запроса: Bsc

Как видите, разброс в имеющихся данных колоссальный. Теперь представим наши данные в чуть большем объеме и в графическом виде.

Отображение данных

Просто отобразим на графике пять самых популярных и непопулярных на ответы запросов. Для этого я использовал JavaScript библиотеку amcharts. Все что необходимо, это заполнить свойство dataProvider парами ключ-значение.

"dataProvider": [
  <xsl:for-each select="//queries/yandexsearch/response/results/grouping">
    <xsl:sort select="./found-docs[@priority='all']" data-type="number"/>
    <xsl:choose>
      <xsl:when test="position() &lt;= 5 or position() > $count - 5">
        {
        "query": "<xsl:value-of select="translate(../../../request/query, '&#34;', '')" />",
        "count": "<xsl:value-of select="./found-docs[@priority='all']"/>"
        },
      </xsl:when>
    </xsl:choose>
    <xsl:choose>
      <xsl:when test="position() = 6">
        {
        "query": "other queries ...",
        "count": "0"
        },
      </xsl:when>
    </xsl:choose>
  </xsl:for-each>
]

Элемент <xsl:choose> позволяет создавать логику условий с множественным выбором. Функция translate поможет не испортить JavaScript код в случае, если в тексте запроса содержатся двойные кавычки. Посмотрим на результат:

Различия в количестве результатов настолько велики, что понять, сколько на самом деле ответов у малопопулярных запросов, можно только наведя курсор на запрос. Теперь представим наши разнородные данные в полном объеме.

Выбросы

Выбросами называются данные, которые выбиваются из общей выборки. Судя по предыдущему графику, можно предположить, что в нашем датасете таких выбросов может быть довольно много. Для представления всех данных на одном графике и выделения при этом выбросов воспользуемся Box-Plot графиком. На нем отображаются медиана, верхний и нижний квартили, минимальное и максимальное значение выборки (без учета выбросов). Кроме того, красными точками на графике указаны умеренные выбросы, а синими - экстремальные. Для демонстрации Box-Plot графика я также исопльзовал готовую библиотеку - Plotly JS. Все, что нужно: заполнить массив данными и передать его в конструктор:

<xsl:for-each select="//queries/yandexsearch">
    <xsl:choose>
      <xsl:when test="./response/results/grouping/found-docs[@priority='all']">
        y[<xsl:value-of select="position() - 1"/>] = 
          <xsl:value-of select="./response/results/grouping/found-docs[@priority='all']"/>;
      </xsl:when>
      <xsl:otherwise>
        y[<xsl:value-of select="position() - 1"/>] = 0;
      </xsl:otherwise>
    </xsl:choose>
  </xsl:for-each>

На первый взгляд даже трудно понять, что это действительно Box-Plot график. Однако, если смаштабировать график до нужного размера, то мы увидим следующую картину: box-plot Box-Plot график только подтвердил, что датасет содержит очень разнородные результаты.

Заключение

В этом посте мы познакомились с такой технологией как XSLT. Мы решали достаточно нетипичную задачу с ее помощью, и, конечно, есть более удобные инструменты для анализа данных. Тем не менее XSLT - очень мощный движок, который позволяет решать многие проблемы, связанные с преобразованием XML-документов.