Files

8 lines
17 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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 = &quot;mode=fast; retries=3; mode=safe&quot;;\nauto pair = regex(&quot;(?P&lt;key&gt;[A-Za-z_][A-Za-z0-9_]*)=(?P&lt;value&gt;[^;]+)&quot;);\n\nforeach (hit; matchAll(input, pair)) {\n writeln(hit[&quot;key&quot;], &quot; =&gt; &quot;, hit[&quot;value&quot;]);\n}\n\n// Учебный вывод:\n// mode =&gt; fast\n// retries =&gt; 3\n// mode =&gt; 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 = &quot;one два&quot;;\nauto word = regex(&quot;[A-Za-zА-Яа-яЁё]+&quot;);\n\nforeach (hit; matchAll(input, word)) {\n auto byteOffset = hit.pre.length;\n writeln(hit.hit, &quot; starts at byte &quot;, 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>"
}