Парсер: регулярные выражения, OQuery, JSON

«Парсер» разбирает любые текстовые структуры: HTML, XML, JSON или произвольный текст. Чаще всего его применяют для разбора ответов веб-сервисов, полученных компонентом «Запрос к WEB-сервису».

На вход подаётся документ — структура, подлежащая разбору. Задать её можно текстом, переменной, выражением или функцией. Дальше выбирается алгоритм разбора и пишется поисковый запрос; результат сохраняется в переменную.

Цепочка парсеров Если одним запросом до нужного значения не добраться, ставят несколько «Парсеров» подряд: каждый следующий получает на вход результат предыдущего. Так разбирается структура любой глубины. На уровне сценария вокруг них организуются циклы и условия.

Свойства компонента

СвойствоОписание
ДокументАргумент с текстовой структурой, подлежащей разбору.
АлгоритмРегулярные выражения, язык OQuery для HTML, парсер JSON.
Поисковый запросСтрока запроса для выбранного алгоритма. Язык регулярных выражений совпадает со стандартом .NET.
ФункцияЧто именно вернуть; набор зависит от алгоритма — см. ниже.
Номер элементаПорядковый номер структуры, если запрос нашёл несколько результатов. Доступно для регулярных выражений и OQuery.
АтрибутТолько для OQuery и функций, связанных с атрибутами. «Значение атрибута» ждёт имя или порядковый номер атрибута, «Имя атрибута» — только порядковый номер.
Результат в переменнуюПеременная, куда сохранится результат: число либо вырезанный или сформированный текст.
ПереходКуда идти при корректном выполнении запроса.
Переход, неудачаКуда идти при неудаче разбора.
Нумерация элементов различается по алгоритмам У парсера OQuery нумерация начинается с 1.
У регулярных выражений и парсера JSON — с 0.
Это самая частая причина того, что цикл перебора возвращает не тот элемент или промахивается на один.

Функции по алгоритмам

АлгоритмДоступные функции
Регулярные выражения Содержимое; Количество найденных.
OQuery Документ — текст структуры с обрамляющими тегами; Содержимое — без обрамляющих тегов; Количество элементов — число найденных структур на одном уровне; Значение атрибута; Имя атрибута; Количество атрибутов.
JSON Содержимое — значение элемента, объект или массив; значение элемента возвращается в кавычках. Восстановленное содержимое — то же без кавычек. Количество элементов — число вложенных элементов, объектов, массивов.

Язык OQuery

Запрос OQuery — это набор команд, записанный в одну строку и выполняемый слева направо. Для отладки запросов служит программа OQuery-tester.exe.

Уровни

ЗнакЧто означает
*Все элементы
>Все дочерние элементы
^Корневые элементы — на нулевом уровне искомой структуры
пробелВсе элементы, кроме корневых

Простые команды

КомандаЧто делаетПример
название тегаТеги; английские буквы и цифры, начинается с буквы.^TITLE — тег TITLE в корне
#значениеТеги, у которых атрибут id равен указанному значению.FORM#32 — теги FORM с id=32
[название=значение]Теги, в теле которых есть атрибут с таким названием и значением.TABLE[width=90%]
.значениеТеги, у которых атрибут class содержит указанное значение.INPUT.b.c — теги с class="a b c"

Порядок перечисления команд на результат не влияет: TABLE#big.sample и TABLE.sample#big — одно и то же.

Операции сравнения значений атрибута

ЗнакУсловиеПример
=Полное совпадениеTABLE[width=90%]
^=Начинается со значенияIMG[src^="http://example.org/images"]
$=Заканчивается значениемIMG[src$=".gif"]
*=Значение содержится где-то внутриIMG[src*="banner"]

Суффиксы и функции позиции

КомандаЧто делаетПример
:firstПервый элемент коллекцииTD:first
:lastПоследний элемент коллекцииUL:last
:parentРодительские теги элементов коллекцииTABLE>TD>A:parent:parent
:gt(позиция)Элементы начиная с указанной позицииTD:gt(10)
:lt(позиция)Элементы до указанной позиции включительноTD:lt(15)
:eq(позиция)Элемент указанной позиции; отрицательное число отсчитывается с концаTD:eq(-3) — третий тег с конца
:eq(начало,конец)Набор элементов от начальной до конечной позиции; отрицательные числа — с концаTD:eq(-1,-3) — последние три элемента

Синонимы eq: equal, range.

Сложные функции

КомандаЧто делает
:has(запрос)Теги, содержащие другие теги, удовлетворяющие указанному запросу. TABLE:has(^TD>A) — аналог TABLE>TD>A:parent:parent.
:not(запрос)Теги, не удовлетворяющие указанному запросу. A:eq(1,10):not(:eq(2)) — все теги A с первого по десятый, кроме второго.
:has и :not не переставляются местами
A:has(:not(IMG))   — все теги A, которые содержат не IMG
A:not(:has(IMG))   — все теги A, которые не содержат IMG
Результаты у этих запросов разные.

Операции и скобки

ЗнакЧто делаетПример
,Логическое «или» — перечисление запросов*A>IMG,>TABLE — все IMG внутри A и все TABLE в корне
+Теги по запросу 1, за которыми идут теги по запросу 2A + IMG + #num
-Теги по запросу 1, перед которыми идут теги по запросу 2—
( )Объединение команд одинакового приоритета(FONT,A)(:has(IMG),:has(SPAN))

Типовые наборы команд

ЗапросЧто вернёт
^FONTВсе теги FONT корневого (нулевого) уровня
>FONTВсе теги FONT первого уровня
*FONTВсе теги FONT
*>FONTВсе теги FONT, кроме корня
*>>FONT или >*>FONTВсе теги FONT начиная со второго уровня
A>>BВсе теги B на втором уровне у A

Разбор XML: пример

Пусть разбирается такая структура — два листа, на первом матрица 3×3, на втором 2×2:

<info>
  <list index="1" name="matrix33">
    <row id="1" name="aaa">
      <col id="1" name="r1c1" />
      <col id="2" name="r1c2" />
      <col id="3" name="r1c3" />
    </row>
    …
  </list>
  <list index="2" name="matrix22">
    <row id="1" name="ddd">
      <col id="1" name="element11" />
      <col id="2" name="element12" />
    </row>
    <row id="2" name="eee">
      <col id="1" name="element21" />
      <col id="2" name="element22" />
    </row>
  </list>
</info>
ЗапросЧто вернёт
infoВсе теги <info> с содержимым
listВсе теги <list> — информацию по обоим листам
list[index="2"]Всю информацию второго листа
col[id="2"]Все элементы второго столбца в обеих матрицах
list[index="2"] col[id="2"]Элементы второго столбца только на втором листе
col[id="2"][name="element22"]Один конкретный тег
(col[id="1"]):last:parentРодительский тег последнего <col id="1">

Добравшись до нужного тега, значение его атрибута берут функцией «Значение атрибута», указав имя атрибута в поле «Атрибут». Если запрос вернул несколько тегов, их перебирают в цикле через «Номер элемента», а общее число узнают функцией «Количество элементов».

Регулярные выражения

Применяются для разбора обычного текста. Язык совпадает со стандартом .NET.

Незахватывающие группы Перед группой рекомендуется ставить ?: — иначе значение не вернётся в переменную сценария.

Примеры

ВыражениеЧто находит
\S+Все целые слова
[A-Z][A-Za-z]+Слова, начинающиеся с прописной буквы
X\-Forwarded\-For\:\s*(\S+(?:[^\r\n]+\S+)*)Всё после заголовка X-Forwarded-For: до переноса строки
(?:[a-zA-Z0-9][a-zA-Z0-9\-\.]*@)?(?:[a-zA-Z0-9][a-zA-Z0-9\-]*\.)+[a-zA-Z]{2,999}Адреса электронной почты и доменные имена в тексте
Типовая задача: распознавание недоставленных писем При рассылке часть адресов оказывается недействительной, и в ответ приходит письмо от почтового сервера о неудачной доставке. Служебный сценарий забирает такое письмо, разбирает его «Парсером» с регулярным выражением и вычёркивает адрес из базы. Заголовки вида X-Forwarded-For и Delivered-To в теле такого письма как раз и содержат исходный адрес получателя.

Если выражение вернуло несколько значений, их перебирают в цикле; общее число даёт функция «Количество найденных».

Парсер JSON

У платформы собственный синтаксис разбора JSON, и он предельно прост:

  • доступ к значению — по имени в кавычках;
  • переход к вложенному объекту — через /;
  • обращение к элементу массива — по индексу, отсчёт с нуля.

Для документа вида:

{
  "info": {
    "list1": {
      "index": 1,
      "name": "matrix",
      "rows": [
        { "id": 1, "name": "row1",
          "cols": [ {"id":"1","name":"element11"}, {"id":"2","name":"element12"} ] },
        { "id": 2, "name": "row2",
          "cols": [ {"id":"1","name":"element21"}, {"id":"2","name":"element22"} ] }
      ]
    }
  }
}
ЗапросРезультат
"info"/"list1"/"name""matrix"
"info"/"list1"/"rows"/1/"name""row2" — второй элемент массива, отсчёт с нуля
"info"/"list1"/"rows"/1/"cols"/0/"name""element21"

Функция «Содержимое» вернёт значение в кавычках, «Восстановленное содержимое» — без них. Для дальнейших сравнений в сценарии обычно нужен второй вариант.

Как ускорить разбор

Сокращайте документ перед циклом Время выполнения запроса зависит в основном от размера исходного текста и количества тегов в нём, и значительно меньше — от длины самого запроса. Если данные нужно получать в цикле, сначала вырежьте первым «Парсером» нужный фрагмент, а перебирайте уже его.