8 lines
17 KiB
JSON
8 lines
17 KiB
JSON
{
|
||
"index": 367,
|
||
"slug": "пишем-аналог-функции-php-preg_match_all-на-языке-прог",
|
||
"title": "Аналог preg_match_all в D: группы, порядок и смещения",
|
||
"excerpt": "Как перенести поиск всех совпадений из PHP в D, сохранить смысл групп и не принять байтовый offset за номер символа.",
|
||
"contentHtml": "<p>Представим обработчик импорта, который переносит регулярное выражение из PHP в D. На строке <code>mode=fast; retries=3</code> тест проходит, но после добавления <code>mode=быстрый</code> адаптер может принять байтовое смещение за номер символа. Другая ошибка появляется, когда необязательная группа превращает соседнее поле в пустую строку или <code>null</code>. Исключения при этом может не быть.</p>\n<p>Цена ошибки зависит от места, где работает разбор. В конфигурации это неверная настройка. В импорте — пропущенная или продублированная запись. В интерфейсе — диапазон подсветки, который начинается не там. Поэтому аналог <code>preg_match_all</code> нужно строить вокруг контракта результата, а не вокруг похожего имени метода.</p>\n<h2>Тезис: сначала зафиксируйте форму результата</h2>\n<p><code>preg_match_all</code> ищет в исходной строке все непересекающиеся совпадения. После полного совпадения поиск продолжается с его конца. Форма массива зависит от режима. В режиме <code>PREG_PATTERN_ORDER</code> первый столбец содержит полные совпадения, следующие столбцы — группы. В режиме <code>PREG_SET_ORDER</code> каждый элемент содержит одно совпадение и его группы. Флаг <code>PREG_OFFSET_CAPTURE</code> добавляет позицию каждого фрагмента.</p>\n<p>В D модуль <code>std.regex</code> предоставляет <code>matchAll</code>. Он возвращает ленивый диапазон совпадений. Каждый элемент диапазона — объект <code>Captures</code>: индекс <code>0</code> содержит полный текст, следующие индексы — захватывающие группы. Это не готовая копия PHP-массива: адаптер должен выбрать собственную модель и явно преобразовать её в нужный порядок.</p>\n<table><thead><tr><th>Вопрос</th><th>PHP</th><th>D</th></tr></thead><tbody><tr><td>Все совпадения</td><td><code>preg_match_all</code> заполняет массив</td><td><code>matchAll</code> возвращает диапазон</td></tr><tr><td>Одна запись</td><td>Зависит от флага порядка</td><td>Один элемент диапазона</td></tr><tr><td>Группа</td><td>Номер или имя ключа</td><td>Индекс или имя группы</td></tr><tr><td>Нет результата</td><td>Количество совпадений равно нулю</td><td>Диапазон пуст</td></tr><tr><td>Позиция</td><td>При offset-флаге — байтовое смещение</td><td>Единицу измерения нужно закрепить в адаптере</td></tr></tbody></table>\n<h2>Механизм на коротком примере</h2>\n<p>Пусть вход содержит пары параметров. Регулярное выражение выделяет имя и значение, но не проверяет всю конфигурацию. Это разные задачи: regex находит фрагменты, а код после него проверяет обязательность ключей, допустимые значения и повторения.</p>\n<pre><code>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</code></pre>\n<p>Цикл получает очередное совпадение из диапазона. Он не должен вручную искать следующий фрагмент с той же позиции. Полное совпадение доступно через <code>hit.hit</code>, группы — через числовой индекс или имя. Для необязательной группы текущий <code>std.regex</code> возвращает null-срез; PHP без <code>PREG_UNMATCHED_AS_NULL</code> обычно возвращает пустую строку. Адаптер должен выбрать одно представление и закрепить его тестом.</p>\n<p>Этот пример намеренно ограничен. Он не разбирает значение с экранированной точкой с запятой, кавычками или переносом строки. Не переносите его на CSV или вложенный язык только потому, что один тест прошёл. Для таких форматов regex может выделить токены, но состояние и грамматика нужны отдельному парсеру.</p>\n<h2>Внутренняя модель и два порядка</h2>\n<p>Удобнее сначала собрать строки по одному совпадению. Затем из этой модели получить формат, который нужен вызывающему коду. Так режимы не смешиваются внутри цикла, а проверка количества групп остаётся одной.</p>\n<pre><code>import 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// отдельным проходом по группам.</code></pre>\n<p>Типы в этом фрагменте показывают границу адаптера, а не универсальную библиотеку. В актуальном <code>std.regex</code> <code>Captures</code> индексируется с нуля: элемент <code>0</code> — полное совпадение, элементы с <code>1</code> — группы. Для старой версии D закрепите версию Phobos и сверяйте сигнатуры перед сборкой: статья использует API с <code>matchAll</code>, <code>Regex</code> и <code>Captures</code>, а не обещает обратную совместимость.</p>\n<h2>Смещение: байты не равны символам</h2>\n<p>Самая дорогая ошибка возникает при переносе <code>PREG_OFFSET_CAPTURE</code>. В PHP смещение указывает позицию в байтах исходной строки. Строка D типа <code>string</code> хранит UTF-8, а её <code>length</code> и длина среза считают байты. При этом <code>std.regex</code> сопоставляет текст на уровне Unicode-кодовых точек. Поэтому байтовый offset и номер кодовой точки расходятся уже на кириллице.</p>\n<pre><code>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}</code></pre>\n<p>У <code>Captures</code> свойство <code>pre</code> — это срез входа перед текущим совпадением. Его длина даёт байтовый offset без арифметики указателей: для <code>one</code> результат равен <code>0</code>, для <code>два</code> — <code>4</code>, потому что перед кириллицей стоят три ASCII-символа и пробел. Срез остаётся привязанным к исходной строке, поэтому числовое значение всё равно нужно сохранить сразу.</p>\n<p>Выберите один контракт. Для совместимости с PHP называйте поле <code>byteOffset</code> и возвращайте байтовую позицию. Если потребителю нужен номер Unicode-кодовой точки, считайте его отдельным проходом по UTF-8. UI может требовать ещё одну единицу — количество видимых графем. Не называйте все три значения просто <code>position</code>.</p>\n<figure><img src='/assets/illustrations/preg-match-all-d.svg' alt='Обход совпадений регулярного выражения в D'><figcaption>Диапазон даёт совпадения, а адаптер выбирает форму выдачи.</figcaption></figure>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>Группы стоят не на своих местах</td><td>Смешаны порядок по группам и порядок по совпадениям</td><td>Сравнить два совпадения с двумя группами</td><td>Вынести преобразование в отдельную функцию</td></tr><tr><td>Поиск зациклился</td><td>Шаблон допускает пустое совпадение</td><td>Проверить пустой вход и <code>*</code> или <code>?</code></td><td>Запретить шаблон или обработать продвижение позиции</td></tr><tr><td>ASCII работает, UTF-8 нет</td><td>Байты приняли за символы</td><td>Сравнить byte offset с числом кодовых точек</td><td>Назвать единицу и добавить тест на кириллице</td></tr><tr><td>PHP находит больше</td><td>PCRE отличается от <code>std.regex</code></td><td>Скомпилировать шаблон D и сравнить группы</td><td>Переписать шаблон или оставить разбор в PHP</td></tr><tr><td>Значение поглощает разделитель</td><td>Класс символов слишком широкий</td><td>Проверить две записи и пустое значение</td><td>Ограничить класс и решить судьбу пустого поля</td></tr></tbody></table>\n<h2>Порядок действий</h2>\n<ol><li>Запишите вход, список полных совпадений, группы и единицу смещения.</li><li>Скомпилируйте шаблон в целевой версии D. Не переносите расширения PCRE вслепую.</li><li>Соберите внутренние записи: полное совпадение плюс группы.</li><li>Выберите порядок по совпадениям или по группам. Назовите функцию по этому выбору.</li><li>Проверьте пустой вход, отсутствие совпадений, одно и два соседних совпадения.</li><li>Добавьте необязательную группу и зафиксируйте её представление.</li><li>Проверьте UTF-8 и сравните byte offset с номером символа, если нужны обе величины.</li><li>Сравните количество и содержимое результатов с эталоном PHP на том же наборе входов.</li><li>Если шаблон описывает вложенную структуру, остановите перенос и выберите парсер.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Аналог не обязан копировать всю поверхность PHP. Переносите только нужные режимы и документируйте отсутствующие: стартовый offset, именованные ключи, необязательные группы и особые флаги. Метод с привычным названием не должен молча менять порядок результата или смысл unmatched-группы.</p>\n<p>Не используйте регулярное выражение для HTML, вложенных выражений и форматов с полноценным экранированием. Если разница PCRE и D ломает нужную конструкцию, отрицательный путь ясен: оставить обработку в PHP, применить библиотеку с нужным синтаксисом или изменить формат входа. Неполный адаптер, который иногда возвращает правдоподобный результат, опаснее явного отказа.</p>\n<p>Примеры в статье учебные. Они не доказывают скорость, потребление памяти или безопасность для произвольного пользовательского ввода. Для этих свойств нужны ограничения размера входа, набор случаев и отдельные измерения.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Адаптер готов, когда на зафиксированных входах он возвращает ожидаемое число полных совпадений, тот же текст групп и документированную единицу offset. Минимальный набор включает отсутствие совпадений, два совпадения с группами, соседние совпадения, необязательную группу и строку UTF-8. Шаблон должен компилироваться в целевой версии D, а код должен отличать ошибку шаблона от корректного пустого результата.</p>\n<p>Это проверяет поведение, а не одно имя функции. Если сравнение не прошло, сначала исправьте контракт результата. Оптимизацию аллокаций и сокращение копирований выполняйте после совпадения с эталоном.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href='https://www.php.net/manual/en/function.preg-match-all.php'>PHP Manual: preg_match_all</a> — режимы порядка, флаги, число совпадений и offset.</li><li><a href='https://dlang.org/phobos/std_regex.html'>D Phobos: std.regex</a> — регулярные выражения, совпадения, Captures и диапазоны.</li><li><a href='https://dlang.org/spec/arrays.html#strings'>D Language Specification: Strings</a> — строки D, UTF-8 и длина строковых массивов.</li></ul>"
|
||
}
|