Всем привет! Сегодняшний пост напрямую связан с языком представления слабоструктурированных данных XML (extensible Markup Language). Думаю, многие из вас так или иначе сталкивались с ним в своей практике. Однако, далеко не все знакомы с XSLT - языком преобразования XML-документов. Он позволяет создавать новые файлы на основе предложенного XML-документа. Причем результирующие документы могут иметь самые разные форматы. С помощью XSLT можно решать достаточно большой класс задач. Одни из наиболее распространенных - это создание отчетов, например в формате HTML, или преобразование документа из одной XML схемы в другую. Сегодня мы займемся менее типичной для XSLT задачей. Наша цель: на основе результатов поисковых запросов к Yandex API, представленных в XML-формате, создать веб-страницу, на которой представлен небольшой статистический анализ произведенных запросов. Этот пример должен продемонстрировать основные возможности языка XSLT.
В июле этого года консорциум всемирной паутины объявил о появлении на свет версии 3.0 языка XSLT. Однако, для решения наших задач я использовал долгие годы применяемую всеми версию 2.0. Принцип работы с XSLT заключается в написании таблице стилей, в которой нужно описать правила преобразования документа XML. Всю остальную работу выполняет процессор XSLT. Я использовал наиболее полный процессор таблиц стилей под названием Saxon.
Перечислим несколько основных идей, заложенных в XSLT:
При обработке документа процессор XSLT использует следующую логику:
В одном из своих учебных проектов мне были необходимы результаты поисковых запросов для названий книг. В датасете были десятки тысяч экземпляров, что достаточно усложнило задачу, так как многие хорошие поисковые системы предоставляют очень ограничивающие лимиты на количество запросов в своем 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>"<hlword>Яндекс</hlword>" - поисковая система и интернет-портал</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>
Результат получился достаточно большим. Однако, зачастую среднее выборочное - не самая репрезентативная статистика. Давайте посмотрим насколько велик разброс в наших данных.
Посчитаем минимум и максимум нашей выборки. Для этого в 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>
Как видите, разброс в имеющихся данных колоссальный. Теперь представим наши данные в чуть большем объеме и в графическом виде.
Просто отобразим на графике пять самых популярных и непопулярных на ответы запросов. Для этого я использовал 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() <= 5 or position() > $count - 5">
{
"query": "<xsl:value-of select="translate(../../../request/query, '"', '')" />",
"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 график только подтвердил, что датасет содержит очень разнородные результаты.
В этом посте мы познакомились с такой технологией как XSLT. Мы решали достаточно нетипичную задачу с ее помощью, и, конечно, есть более удобные инструменты для анализа данных. Тем не менее XSLT - очень мощный движок, который позволяет решать многие проблемы, связанные с преобразованием XML-документов.
Written on October 15th, 2017 by Alexey Kalina