{ "index": 367, "slug": "пишем-аналог-функции-php-preg_match_all-на-языке-прог", "title": "Аналог preg_match_all в D: группы, порядок и смещения", "excerpt": "Как перенести поиск всех совпадений из PHP в D, сохранить смысл групп и не принять байтовый offset за номер символа.", "contentHtml": "
После переноса регулярного выражения из PHP в D программа может продолжить работу и всё же разобрать строку неправильно. Ключ попадает в значение, необязательная группа сдвигает остальные поля, а совпадение на кириллице получает неверную позицию. Такой сбой часто не вызывает исключение. Он портит данные дальше по цепочке.
\nЦена ошибки зависит от места, где работает разбор. В конфигурации это неверная настройка. В импорте — пропущенная или продублированная запись. В интерфейсе — диапазон подсветки, который начинается не там. Поэтому аналог preg_match_all нужно строить вокруг контракта результата, а не вокруг похожего имени метода.
preg_match_all ищет в исходной строке все непересекающиеся совпадения. После полного совпадения поиск продолжается с его конца. Форма массива зависит от режима. В режиме PREG_PATTERN_ORDER первый столбец содержит полные совпадения, следующие столбцы — группы. В режиме PREG_SET_ORDER каждый элемент содержит одно совпадение и его группы. Флаг PREG_OFFSET_CAPTURE добавляет позицию каждого фрагмента.
В D модуль std.regex предоставляет matchAll. Он возвращает диапазон совпадений. Каждый элемент диапазона содержит полный текст и захватывающие группы. Это не готовая копия 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, группы — через индекс или имя, если это поддерживает версия Phobos в проекте. До интеграции сверяйте сигнатуры с целевой версией D.
Этот пример намеренно ограничен. Он не разбирает значение с экранированной точкой с запятой, кавычками или переносом строки. Не переносите его на CSV или вложенный язык только потому, что один тест прошёл. Для таких форматов regex может выделить токены, но состояние и грамматика нужны отдельному парсеру.
\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; 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Самая дорогая ошибка возникает при переносе PREG_OFFSET_CAPTURE. В PHP смещение указывает позицию в байтах исходной строки. Строка D типа string хранит UTF-8. Один видимый символ может занимать несколько байтов. Поэтому byte offset и номер символа расходятся уже на кириллице.
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.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Группы стоят не на своих местах | Смешаны порядок по группам и порядок по совпадениям | Сравнить два совпадения с двумя группами | Вынести преобразование в отдельную функцию |
| Поиск зациклился | Шаблон допускает пустое совпадение | Проверить пустой вход и * или ? | Запретить шаблон или обработать продвижение позиции |
| 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