{ "index": 367, "slug": "пишем-аналог-функции-php-preg_match_all-на-языке-прог", "title": "Аналог preg_match_all в D: группы, порядок и смещения", "excerpt": "Как перенести поиск всех совпадений из PHP в D, сохранить смысл групп и не принять байтовый offset за номер символа.", "contentHtml": "
Представим обработчик импорта, который переносит регулярное выражение из PHP в D. На строке mode=fast; retries=3 тест проходит, но после добавления mode=быстрый адаптер может принять байтовое смещение за номер символа. Другая ошибка появляется, когда необязательная группа превращает соседнее поле в пустую строку или null. Исключения при этом может не быть.
Цена ошибки зависит от места, где работает разбор. В конфигурации это неверная настройка. В импорте — пропущенная или продублированная запись. В интерфейсе — диапазон подсветки, который начинается не там. Поэтому аналог preg_match_all нужно строить вокруг контракта результата, а не вокруг похожего имени метода.
preg_match_all ищет в исходной строке все непересекающиеся совпадения. После полного совпадения поиск продолжается с его конца. Форма массива зависит от режима. В режиме PREG_PATTERN_ORDER первый столбец содержит полные совпадения, следующие столбцы — группы. В режиме PREG_SET_ORDER каждый элемент содержит одно совпадение и его группы. Флаг PREG_OFFSET_CAPTURE добавляет позицию каждого фрагмента.
В D модуль std.regex предоставляет matchAll. Он возвращает ленивый диапазон совпадений. Каждый элемент диапазона — объект Captures: индекс 0 содержит полный текст, следующие индексы — захватывающие группы. Это не готовая копия PHP-массива: адаптер должен выбрать собственную модель и явно преобразовать её в нужный порядок.
| Вопрос | PHP | D |
|---|---|---|
| Все совпадения | preg_match_all заполняет массив | matchAll возвращает диапазон |
| Одна запись | Зависит от флага порядка | Один элемент диапазона |
| Группа | Номер или имя ключа | Индекс или имя группы |
| Нет результата | Количество совпадений равно нулю | Диапазон пуст |
| Позиция | При offset-флаге — байтовое смещение | Единицу измерения нужно закрепить в адаптере |
Пусть вход содержит пары параметров. Регулярное выражение выделяет имя и значение, но не проверяет всю конфигурацию. Это разные задачи: regex находит фрагменты, а код после него проверяет обязательность ключей, допустимые значения и повторения.
\nimport 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, группы — через числовой индекс или имя. Для необязательной группы текущий std.regex возвращает null-срез; PHP без PREG_UNMATCHED_AS_NULL обычно возвращает пустую строку. Адаптер должен выбрать одно представление и закрепить его тестом.
Этот пример намеренно ограничен. Он не разбирает значение с экранированной точкой с запятой, кавычками или переносом строки. Не переносите его на CSV или вложенный язык только потому, что один тест прошёл. Для таких форматов regex может выделить токены, но состояние и грамматика нужны отдельному парсеру.
\nУдобнее сначала собрать строки по одному совпадению. Затем из этой модели получить формат, который нужен вызывающему коду. Так режимы не смешиваются внутри цикла, а проверка количества групп остаётся одной.
\nimport std.regex : Regex, matchAll;\n\nstruct 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; 1 .. hit.length) {\n row.groups ~= hit[index];\n }\n rows ~= row;\n }\n return rows;\n}\n\n// MatchRow[] можно преобразовать в pattern order\n// отдельным проходом по группам.\nТипы в этом фрагменте показывают границу адаптера, а не универсальную библиотеку. В актуальном std.regex Captures индексируется с нуля: элемент 0 — полное совпадение, элементы с 1 — группы. Для старой версии D закрепите версию Phobos и сверяйте сигнатуры перед сборкой: статья использует API с matchAll, Regex и Captures, а не обещает обратную совместимость.
Самая дорогая ошибка возникает при переносе PREG_OFFSET_CAPTURE. В PHP смещение указывает позицию в байтах исходной строки. Строка D типа string хранит UTF-8, а её length и длина среза считают байты. При этом std.regex сопоставляет текст на уровне Unicode-кодовых точек. Поэтому байтовый offset и номер кодовой точки расходятся уже на кириллице.
import std.regex : matchAll, regex;\nimport std.stdio : writeln;\n\nauto input = "one два";\nauto word = regex("[A-Za-zА-Яа-яЁё]+");\n\nforeach (hit; matchAll(input, word)) {\n auto byteOffset = hit.pre.length;\n writeln(hit.hit, " starts at byte ", byteOffset);\n}\nУ Captures свойство pre — это срез входа перед текущим совпадением. Его длина даёт байтовый offset без арифметики указателей: для one результат равен 0, для два — 4, потому что перед кириллицей стоят три ASCII-символа и пробел. Срез остаётся привязанным к исходной строке, поэтому числовое значение всё равно нужно сохранить сразу.
Выберите один контракт. Для совместимости с PHP называйте поле byteOffset и возвращайте байтовую позицию. Если потребителю нужен номер Unicode-кодовой точки, считайте его отдельным проходом по UTF-8. UI может требовать ещё одну единицу — количество видимых графем. Не называйте все три значения просто position.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Группы стоят не на своих местах | Смешаны порядок по группам и порядок по совпадениям | Сравнить два совпадения с двумя группами | Вынести преобразование в отдельную функцию |
| Поиск зациклился | Шаблон допускает пустое совпадение | Проверить пустой вход и * или ? | Запретить шаблон или обработать продвижение позиции |
| ASCII работает, UTF-8 нет | Байты приняли за символы | Сравнить byte offset с числом кодовых точек | Назвать единицу и добавить тест на кириллице |
| PHP находит больше | PCRE отличается от std.regex | Скомпилировать шаблон D и сравнить группы | Переписать шаблон или оставить разбор в PHP |
| Значение поглощает разделитель | Класс символов слишком широкий | Проверить две записи и пустое значение | Ограничить класс и решить судьбу пустого поля |
Аналог не обязан копировать всю поверхность PHP. Переносите только нужные режимы и документируйте отсутствующие: стартовый offset, именованные ключи, необязательные группы и особые флаги. Метод с привычным названием не должен молча менять порядок результата или смысл unmatched-группы.
\nНе используйте регулярное выражение для HTML, вложенных выражений и форматов с полноценным экранированием. Если разница PCRE и D ломает нужную конструкцию, отрицательный путь ясен: оставить обработку в PHP, применить библиотеку с нужным синтаксисом или изменить формат входа. Неполный адаптер, который иногда возвращает правдоподобный результат, опаснее явного отказа.
\nПримеры в статье учебные. Они не доказывают скорость, потребление памяти или безопасность для произвольного пользовательского ввода. Для этих свойств нужны ограничения размера входа, набор случаев и отдельные измерения.
\nАдаптер готов, когда на зафиксированных входах он возвращает ожидаемое число полных совпадений, тот же текст групп и документированную единицу offset. Минимальный набор включает отсутствие совпадений, два совпадения с группами, соседние совпадения, необязательную группу и строку UTF-8. Шаблон должен компилироваться в целевой версии D, а код должен отличать ошибку шаблона от корректного пустого результата.
\nЭто проверяет поведение, а не одно имя функции. Если сравнение не прошло, сначала исправьте контракт результата. Оптимизацию аллокаций и сокращение копирований выполняйте после совпадения с эталоном.
\n