{ "index": 367, "slug": "пишем-аналог-функции-php-preg_match_all-на-языке-прог", "title": "Аналог preg_match_all в D: группы, порядок и смещения", "excerpt": "Как перенести поиск всех совпадений из PHP в D, сохранить смысл групп и не принять байтовый offset за номер символа.", "contentHtml": "

После переноса регулярного выражения из PHP в D программа может продолжить работу и всё же разобрать строку неправильно. Ключ попадает в значение, необязательная группа сдвигает остальные поля, а совпадение на кириллице получает неверную позицию. Такой сбой часто не вызывает исключение. Он портит данные дальше по цепочке.

\n

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

\n

Тезис: сначала зафиксируйте форму результата

\n

preg_match_all ищет в исходной строке все непересекающиеся совпадения. После полного совпадения поиск продолжается с его конца. Форма массива зависит от режима. В режиме PREG_PATTERN_ORDER первый столбец содержит полные совпадения, следующие столбцы — группы. В режиме PREG_SET_ORDER каждый элемент содержит одно совпадение и его группы. Флаг PREG_OFFSET_CAPTURE добавляет позицию каждого фрагмента.

\n

В D модуль std.regex предоставляет matchAll. Он возвращает диапазон совпадений. Каждый элемент диапазона содержит полный текст и захватывающие группы. Это не готовая копия PHP-массива: адаптер должен выбрать собственную модель и явно преобразовать её в нужный порядок.

\n
ВопросPHPD
Все совпаденияpreg_match_all заполняет массивmatchAll возвращает диапазон
Одна записьЗависит от флага порядкаОдин элемент диапазона
ГруппаНомер или имя ключаИндекс или имя группы
Нет результатаКоличество совпадений равно нулюДиапазон пуст
ПозицияПри offset-флаге — байтовое смещениеЕдиницу измерения нужно закрепить в адаптере
\n

Механизм на коротком примере

\n

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

\n
import std.regex : regex, matchAll;\nimport std.stdio : writeln;\n\nauto input = "mode=fast; retries=3; mode=safe";\nauto pair = regex("(?P<key>[A-Za-z_][A-Za-z0-9_]*)=(?P<value>[^;]+)");\n\nforeach (hit; matchAll(input, pair)) {\n    writeln(hit["key"], " => ", hit["value"]);\n}\n\n// Учебный вывод:\n// mode => fast\n// retries => 3\n// mode => safe
\n

Цикл получает очередное совпадение из диапазона. Он не должен вручную искать следующий фрагмент с той же позиции. Полное совпадение доступно через hit.hit, группы — через индекс или имя, если это поддерживает версия Phobos в проекте. До интеграции сверяйте сигнатуры с целевой версией D.

\n

Этот пример намеренно ограничен. Он не разбирает значение с экранированной точкой с запятой, кавычками или переносом строки. Не переносите его на CSV или вложенный язык только потому, что один тест прошёл. Для таких форматов regex может выделить токены, но состояние и грамматика нужны отдельному парсеру.

\n

Внутренняя модель и два порядка

\n

Удобнее сначала собрать строки по одному совпадению. Затем из этой модели получить формат, который нужен вызывающему коду. Так режимы не смешиваются внутри цикла, а проверка количества групп остаётся одной.

\n
struct MatchRow {\n    string full;\n    string[] groups;\n}\n\nMatchRow[] collect(string input, Regex!char pattern) {\n    MatchRow[] rows;\n    foreach (hit; matchAll(input, pattern)) {\n        MatchRow row;\n        row.full = hit.hit;\n        foreach (index; 0 .. hit.length) {\n            row.groups ~= hit[index].text;\n        }\n        rows ~= row;\n    }\n    return rows;\n}\n\n// MatchRow[] можно преобразовать в pattern order\n// отдельным проходом по группам.
\n

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

\n

Смещение: байты не равны символам

\n

Самая дорогая ошибка возникает при переносе PREG_OFFSET_CAPTURE. В PHP смещение указывает позицию в байтах исходной строки. Строка D типа string хранит UTF-8. Один видимый символ может занимать несколько байтов. Поэтому byte offset и номер символа расходятся уже на кириллице.

\n
import std.regex : regex, matchAll;\nimport std.stdio : writeln;\n\nauto input = "one два";\nauto word = regex("\\\\w+");\n\nforeach (hit; matchAll(input, word)) {\n    auto byteOffset = cast(size_t)(hit.hit.ptr - input.ptr);\n    writeln(hit.hit, " starts at byte ", byteOffset);\n}
\n

Разность указателей здесь используется только для учебной проверки: исходная строка должна жить во время вычисления, а числовое значение нужно сохранить сразу. Не храните указатель как долгоживший offset. Если строка позже собрана заново, старое значение уже относится к другому буферу.

\n

Выберите один контракт. Для совместимости с PHP называйте поле byteOffset и возвращайте байтовую позицию. Если потребителю нужен номер Unicode-кодовой точки, вычисляйте его отдельным шагом. UI может требовать ещё одну единицу — количество видимых графем. Не называйте все три значения просто position.

\n
Обход совпадений регулярного выражения в D
Диапазон даёт совпадения, а адаптер выбирает форму выдачи.
\n

Симптом → причина → проверка → действие

\n
СимптомПричинаПроверкаДействие
Группы стоят не на своих местахСмешаны порядок по группам и порядок по совпадениямСравнить два совпадения с двумя группамиВынести преобразование в отдельную функцию
Поиск зациклилсяШаблон допускает пустое совпадениеПроверить пустой вход и * или ?Запретить шаблон или обработать продвижение позиции
ASCII работает, UTF-8 нетБайты приняли за символыСравнить byte offset с числом кодовых точекНазвать единицу и добавить тест на кириллице
PHP находит большеPCRE отличается от std.regexСкомпилировать шаблон D и сравнить группыПереписать шаблон или оставить разбор в PHP
Значение поглощает разделительКласс символов слишком широкийПроверить две записи и пустое значениеОграничить класс и решить судьбу пустого поля
\n

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

\n
  1. Запишите вход, список полных совпадений, группы и единицу смещения.
  2. Скомпилируйте шаблон в целевой версии D. Не переносите расширения PCRE вслепую.
  3. Соберите внутренние записи: полное совпадение плюс группы.
  4. Выберите порядок по совпадениям или по группам. Назовите функцию по этому выбору.
  5. Проверьте пустой вход, отсутствие совпадений, одно и два соседних совпадения.
  6. Добавьте необязательную группу и зафиксируйте её представление.
  7. Проверьте UTF-8 и сравните byte offset с номером символа, если нужны обе величины.
  8. Сравните количество и содержимое результатов с эталоном PHP на том же наборе входов.
  9. Если шаблон описывает вложенную структуру, остановите перенос и выберите парсер.
\n

Ограничения и отрицательный путь

\n

Аналог не обязан копировать всю поверхность PHP. Переносите только нужные режимы и документируйте отсутствующие: стартовый offset, именованные ключи, необязательные группы и особые флаги. Метод с привычным названием не должен молча менять порядок результата или смысл unmatched-группы.

\n

Не используйте регулярное выражение для HTML, вложенных выражений и форматов с полноценным экранированием. Если разница PCRE и D ломает нужную конструкцию, отрицательный путь ясен: оставить обработку в PHP, применить библиотеку с нужным синтаксисом или изменить формат входа. Неполный адаптер, который иногда возвращает правдоподобный результат, опаснее явного отказа.

\n

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

\n

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

\n

Адаптер готов, когда на зафиксированных входах он возвращает ожидаемое число полных совпадений, тот же текст групп и документированную единицу offset. Минимальный набор включает отсутствие совпадений, два совпадения с группами, соседние совпадения, необязательную группу и строку UTF-8. Шаблон должен компилироваться в целевой версии D, а код должен отличать ошибку шаблона от корректного пустого результата.

\n

Это проверяет поведение, а не одно имя функции. Если сравнение не прошло, сначала исправьте контракт результата. Оптимизацию аллокаций и сокращение копирований выполняйте после совпадения с эталоном.

\n

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

\n" }