8 lines
16 KiB
JSON
8 lines
16 KiB
JSON
{
|
||
"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 = "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>, группы — через индекс или имя, если это поддерживает версия 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 = "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}</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>"
|
||
}
|