DarkRiDDeR12 мин

D и регулярные выражения: повторяем preg_match_all, не теряя смысл совпадений

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

Перенос регулярного выражения из PHP в D компилируется, но возвращает другой набор групп. Цена ошибки — неверно разобранный текст, который выглядит корректным до первого вложенного или пустого совпадения.

Исходная статья полезна как портирование знакомой функции. Здесь оно превращается в контракт: что считается совпадением, какие группы нужны, как обрабатывается отсутствие результата и где заканчивается ответственность regex.

D и регулярные выражения: повторяем preg_match_all, не теряя смысл совпадений: схема границ проверки
Иллюстрация показывает границу между симптомом, техническим механизмом и проверяемым действием.

Что сохраняем из исходной заметки

В PHP есть очень удобная функция для глобального поиска шаблона регулярного выражения в строке preg_match_all. Давайте напишем аналогичный класс статических методов для реализации этой функции с разными флагами на языке программирования D.


Функция ищет в строке *subject* все совпадения с шаблоном pattern и помещает результат в массив *matches* в порядке, определяемом комбинацией флагов *flags*.
 После нахождения первого соответствия последующие поиски будут осуществляться не с начала строки, а от конца последнего найденного вхождения.

Данная функцию очень удобна и чаще всего она применяется с третьим параметром, чтобы обработать полученный массив совпадений шаблона. В *D* подобной регулярной функции нет. Мне вообще не сильно нравятся, как реализованы регулярные выражения в *D*. Ну да ладно.

Полную документацию по функции можете посмотреть здесь [http://php.net/manual/ru/function.preg-match-all.php](http://php.net/manual/ru/function.preg-match-all.php).

Возможные флаги функции *preg_match_all*:

- *PREG_PATTERN_ORDER* – упорядочивает результаты так, что элемент *$matches[0]* содержит массив полных вхождений шаблона, элемент *$matches[1]* содержит массив вхождений первой подмаски, и так далее;
- *PREG_SET_ORDER* – упорядочивает результаты так, что элемент *$matches[0]* содержит первый набор вхождений, элемент *$matches[1]* содержит второй набор вхождений, и так далее;
- *PREG_OFFSET_CAPTURE* – в случае, если этот флаг указан, для каждой найденной подстроки будет указана ее позиция в исходной строке. Необходимо помнить, что этот флаг меняет формат возвращаемого массива matches в массив, каждый элемент которого содержит массив, содержащий в индексе с номером *0* найденную подстроку, а смещение этой подстроки в параметре *subject* — в индексе *1*.

Не будем заморачиваться с реализацией через передачу константы, как в *PHP,* или через шаблоны в *D*. Создадим для каждого флага *PHP*данной функции аналогичные статические методы. Параметр *offset* оставим за бортом. Класс давайте назовём *PReg*. Вырисовывается следующая структура:

… matchAllPatternOrder (...) {...} … matchAllSetOrder (...){...} … matchAllOffsetCapture (…) {...} }


Как параметры будем передавать строку для поиска (тип *string*), регулярное выражение (тип *???*) и переменную для вывода количества совпадений шаблона регулярного выражения (тип *out int*). Тип регулярного выражения можно посмотреть, чтобы не капаться в модуле, применив такую хитрость:

writeln(typeof(regex(\d+)).stringof);


Также как аргумент вы без проблем сможете передавать compile-time регулярное выражение (*ctRegex*). Для данного типа создадим алиас типа, чтобы поудобнее было.
 Функция *matchAllPatternOrde*r и *matchAllSetOrde*r будет возвращать двумерный массив строк, для них тоже создадим алиас типов (мне так кажется, что путаницы меньше). *MatchAllOffsetCapture* пока оставим на закуску.

В итоге у нас получается:

public static: alias typePatternOrder = string[]; alias typeSetOrder = string[];


Привожу пример реализованной функции *matchAllPatternOrder* (основные моменты пояснены ниже):

for (int i = 1; i < stdMatches.front.length; i++) { matches.length++; matches[count+1] ~= stdMatches.front[i]; } stdMatches.popFront(); count++; } return matches; }

- Функция *matchAll* осуществляет глобальный поиск шаблона регулярного выражения obRegex.
- Цикл *while* файл перебирает найденные совпадения пока они не закончатся (проверка с помомощью *stdMatches.empty*);
- *stdMatches.front* хранит строку и найденные подстроки;
- *stdMatches.front.hit* возвращает всё строку входящую в найденный шаблон;
- *stdMatches.front[i]* возвращает найденные части по позиции маски;
- *stdMatches.popFront(*) переход к следующей найденной строке.

Реализация функции *matchAllSetOrder* по сути ничем сильно не отличается кроме как позициями найденных строк в массиве (она даже проще). Приведу лишь реализованный вариант:

typeSetOrder[] matchAllSetOrder (string subject, typeRegex obRegex, out int count) { typeSetOrder[] matches; auto stdMatches = matchAll(subject, obRegex);


Реализация функции *matchAllOffsetCapture* имеет свои особенности. Найденные данные функции будем хранить в двумерном массиве картежа типов *Tuple!(string, «text», int, «position»)*, по которому можно будет получить позицию и найденую строку. Реализация:

typeOffsetCapture[] matchAllOffsetCapture (string subject, typeRegex obRegex, out int count) { typeOffsetCapture[] matches;


Единственное, что здесь важно отметить, как определяются позиции, а именно:

Позиция определяется через разность указателей позиций элементов массива. Также здесь выполняется приведение типа к *int*, так как многие процессоры уже использует 64-х битное представление, и указатели будут иметь тип*long*.

Полная реализация модуля с тестами представлена ниже:

// analog of preg regex in php

public static:

for (int i = 1; i < stdMatches.front.length; i++) { matches.length++; matches[count+1] ~= stdMatches.front[i]; } stdMatches.popFront(); count++; } return matches; }

typeSetOrder[] matchAllSetOrder (string subject, typeRegex obRegex, out int count) { typeSetOrder[] matches;

typeOffsetCapture[] matchAllOffsetCapture (string subject, typeRegex obRegex, out int count) { typeOffsetCapture[] matches;

unittest { import std.stdio; writeln("test PReg.matchAll");

int count;

auto matches1 = PReg.matchAllPatternOrder(one two, regex((\w)(\w)\w+), count); assert(matches1[0][0] == one); assert(matches1[0][1] == two); assert(matches1[1][0] == o); assert(matches1[1][1] == n); assert(matches1[2][0] == t); assert(matches1[2][1] == w); assert(count == 2);

auto matches2 = PReg.matchAllSetOrder(one two, regex((\w)(\w)\w+), count); assert(matches2[0][0] == one); assert(matches2[0][1] == o); assert(matches2[0][2] == n); assert(matches2[1][0] == two); assert(matches2[1][1] == t); assert(matches2[1][2] == w); assert(count == 2);

auto matches3 = PReg.matchAllOffsetCapture(one two, ctRegex!((\w)(\w)\w+), count); assert(matches3[0][0].text == "one"); assert(matches3[0][0].position == 0); assert(matches3[0][1].text == "two"); assert(matches3[0][1].position == 4);


Надеюсь, что из этой статьи вы подчеркнули для себя что-то полезное и интересное. Всем спасибо!

## Как довести класс до рабочего состояния

После реализации флагов обязательно добавьте тесты на три случая: нет совпадений, одно совпадение с группами и несколько совпадений с позициями. Именно на позициях чаще всего появляются ошибки, потому что PHP возвращает смещения в исходной строке, а не в подстроке после очередного поиска.

Механизм без лишних обещаний

`preg_match_all` возвращает структуру, зависящую от режима и групп. В D `std.regex` даёт диапазон совпадений, который удобно обходить, но порядок доступа к группам нужно зафиксировать отдельно.

Регулярное выражение не валидирует весь бизнес-формат автоматически. Если нужен полный разбор строки, проверяем якоря `^` и `$`; если нужно найти части внутри текста, не добавляем их случайно.

Пустое совпадение опасно для цикла: выражение может не продвинуть позицию так, как ожидает вызывающий код. Тест на пустую строку и соседние совпадения обязателен.

Минимальный воспроизводимый пример

Ниже — маленькая проверка, которую можно запустить или адаптировать в отдельном тестовом окружении. Значения демонстрационные; проектные идентификаторы, пути и версии нужно заменить своими и сохранить рядом с результатом.

import std.regex : regex, matchAll;
import std.stdio : writeln;

auto pattern = regex(`(?P<key>[A-Za-z_][A-Za-z0-9_]*)=(?P<value>[^;]+)`);
auto input = "mode=fast; retries=3";

foreach (m; matchAll(input, pattern)) {
    writeln(m["key"], " => ", m["value"]);
}

// Ожидаемый вывод:
// mode => fast
// retries => 3

Матрица диагностики

Вопрос: рабочая матрица проверки
ВопросPHPD
Все совпадения`preg_match_all``matchAll` как диапазон
ГруппаИндекс или имяИндекс или именованная группа
Нет результатаПустой массив/falseПустой диапазон
Проверка форматаОтдельная логикаОтдельная логика

Порядок действий

  1. Записать пример входа и ожидаемые совпадения до переноса.
  2. Перенести шаблон и отдельно проверить экранирование строкового литерала D.
  3. Выбрать доступ к группам по имени или индексу и закрепить его тестом.
  4. Проверить пустой вход, две соседние записи и значение с разделителем.
  5. Сравнить количество и содержимое совпадений с эталоном PHP.
  6. Если формат стал сложнее regex, передать результат полноценному парсеру.

Ограничения и безопасный следующий шаг

Совместимость синтаксиса PCRE и regex D не абсолютна; сверяйте документацию конкретной версии.

Регулярное выражение не заменяет проверку размера, кодировки и семантики значения.

Пример разбирает учебную строку и не читает пользовательский файл.

После проверки должен остаться конкретный артефакт: вывод команды, тест, diff конфигурации или запись результата. Если его нет, формулировку нужно вернуть к симптому и не выдавать гипотезу за исправление.

Что записать в ревью

Короткая запись должна отвечать на четыре вопроса: какой вход использовали, какой результат увидели, какая граница была проверена и какое действие разрешено дальше. Такая форма полезнее длинного вывода «всё работает»: другой инженер сможет повторить проверку и понять, где заканчивается пример.

Если результат зависит от версии Windows, PHP, Bitrix, D или браузера, версию фиксируем рядом с командой. Если проверка не охватывает сеть, production или реальные пользовательские данные, это ограничение пишем прямо. Тогда следующий шаг расширяет evidence, а не расширяет обещание.

Проверяемые источники

  • D: std.regex — описывает regex, match и диапазоны совпадений
  • PHP: preg_match_all — фиксирует исходный контракт PHP
  • D language: strings — помогает сверить работу со строками и срезами