Files
progcode/editorial/agent-rewrites/367.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
16 KiB
JSON
Raw 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 программа может продолжить работу и всё же разобрать строку неправильно. Ключ попадает в значение, необязательная группа сдвигает остальные поля, а совпадение на кириллице получает неверную позицию. Такой сбой часто не вызывает исключение. Он портит данные дальше по цепочке.</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>. Он возвращает диапазон совпадений. Каждый элемент диапазона содержит полный текст и захватывающие группы. Это не готовая копия 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>, группы — через индекс или имя, если это поддерживает версия Phobos в проекте. До интеграции сверяйте сигнатуры с целевой версией D.</p>\n<p>Этот пример намеренно ограничен. Он не разбирает значение с экранированной точкой с запятой, кавычками или переносом строки. Не переносите его на CSV или вложенный язык только потому, что один тест прошёл. Для таких форматов regex может выделить токены, но состояние и грамматика нужны отдельному парсеру.</p>\n<h2>Внутренняя модель и два порядка</h2>\n<p>Удобнее сначала собрать строки по одному совпадению. Затем из этой модели получить формат, который нужен вызывающему коду. Так режимы не смешиваются внутри цикла, а проверка количества групп остаётся одной.</p>\n<pre><code>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// отдельным проходом по группам.</code></pre>\n<p>Типы в этом фрагменте показывают границу адаптера, а не универсальную библиотеку. Точный тип объекта совпадения и доступ к его полям зависят от версии Phobos. Перед сборкой замените условную сигнатуру на ту, которую принимает ваш компилятор, и закрепите результат тестом.</p>\n<h2>Смещение: байты не равны символам</h2>\n<p>Самая дорогая ошибка возникает при переносе <code>PREG_OFFSET_CAPTURE</code>. В PHP смещение указывает позицию в байтах исходной строки. Строка D типа <code>string</code> хранит UTF-8. Один видимый символ может занимать несколько байтов. Поэтому byte offset и номер символа расходятся уже на кириллице.</p>\n<pre><code>import std.regex : regex, matchAll;\nimport std.stdio : writeln;\n\nauto input = &quot;one два&quot;;\nauto word = regex(&quot;\\\\w+&quot;);\n\nforeach (hit; matchAll(input, word)) {\n auto byteOffset = cast(size_t)(hit.hit.ptr - input.ptr);\n writeln(hit.hit, &quot; starts at byte &quot;, byteOffset);\n}</code></pre>\n<p>Разность указателей здесь используется только для учебной проверки: исходная строка должна жить во время вычисления, а числовое значение нужно сохранить сразу. Не храните указатель как долгоживший offset. Если строка позже собрана заново, старое значение уже относится к другому буферу.</p>\n<p>Выберите один контракт. Для совместимости с PHP называйте поле <code>byteOffset</code> и возвращайте байтовую позицию. Если потребителю нужен номер Unicode-кодовой точки, вычисляйте его отдельным шагом. 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> — регулярные выражения, совпадения и диапазоны.</li><li><a href='https://dlang.org/spec/arrays.html#strings'>D Language Specification: Strings</a> — строки D и UTF-8.</li></ul>"
}