From beb5e653e14162e7685271f224e01c026dd78100 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Fri, 4 Sep 2026 01:43:30 +0300 Subject: [PATCH] =?UTF-8?q?editorial-367:=20=D1=83=D1=82=D0=BE=D1=87=D0=BD?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B0=D0=BD=D0=B0=D0=BB=D0=BE=D0=B3=20pre?= =?UTF-8?q?g=5Fmatch=5Fall?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- editorial/agent-rewrites/367.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editorial/agent-rewrites/367.json b/editorial/agent-rewrites/367.json index e339d11..189c366 100644 --- a/editorial/agent-rewrites/367.json +++ b/editorial/agent-rewrites/367.json @@ -3,5 +3,5 @@ "slug": "пишем-аналог-функции-php-preg_match_all-на-языке-прог", "title": "Аналог preg_match_all в D: группы, порядок и смещения", "excerpt": "Как перенести поиск всех совпадений из PHP в D, сохранить смысл групп и не принять байтовый offset за номер символа.", - "contentHtml": "

После переноса регулярного выражения из PHP в D программа может продолжить работу и всё же разобрать строку неправильно. Ключ попадает в значение, необязательная группа сдвигает остальные поля, а совпадение на кириллице получает неверную позицию. Такой сбой часто не вызывает исключение. Он портит данные дальше по цепочке.

\n

Цена ошибки зависит от места, где работает разбор. В конфигурации это неверная настройка. В импорте — пропущенная или продублированная запись. В интерфейсе — диапазон подсветки, который начинается не там. Поэтому аналог preg_match_all нужно строить вокруг контракта результата, а не вокруг похожего имени метода.

\n

Тезис: сначала зафиксируйте форму результата

\n

preg_match_all ищет в исходной строке все непересекающиеся совпадения. После полного совпадения поиск продолжается с его конца. Форма массива зависит от режима. В режиме PREG_PATTERN_ORDER первый столбец содержит полные совпадения, следующие столбцы — группы. В режиме PREG_SET_ORDER каждый элемент содержит одно совпадение и его группы. Флаг PREG_OFFSET_CAPTURE добавляет позицию каждого фрагмента.

\n

В D модуль std.regex предоставляет matchAll. Он возвращает диапазон совпадений. Каждый элемент диапазона содержит полный текст и захватывающие группы. Это не готовая копия PHP-массива: адаптер должен выбрать собственную модель и явно преобразовать её в нужный порядок.

\n
ВопросPHPD
Все совпаденияpreg_match_all заполняет массивmatchAll возвращает диапазон
Одна записьЗависит от флага порядкаОдин элемент диапазона
ГруппаНомер или имя ключаИндекс или имя группы
Нет результатаКоличество совпадений равно нулюДиапазон пуст
ПозицияПри offset-флаге — байтовое смещениеЕдиницу измерения нужно закрепить в адаптере
\n

Механизм на коротком примере

\n

Пусть вход содержит пары параметров. Регулярное выражение выделяет имя и значение, но не проверяет всю конфигурацию. Это разные задачи: regex находит фрагменты, а код после него проверяет обязательность ключей, допустимые значения и повторения.

\n
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
\n

Цикл получает очередное совпадение из диапазона. Он не должен вручную искать следующий фрагмент с той же позиции. Полное совпадение доступно через hit.hit, группы — через индекс или имя, если это поддерживает версия Phobos в проекте. До интеграции сверяйте сигнатуры с целевой версией D.

\n

Этот пример намеренно ограничен. Он не разбирает значение с экранированной точкой с запятой, кавычками или переносом строки. Не переносите его на CSV или вложенный язык только потому, что один тест прошёл. Для таких форматов regex может выделить токены, но состояние и грамматика нужны отдельному парсеру.

\n

Внутренняя модель и два порядка

\n

Удобнее сначала собрать строки по одному совпадению. Затем из этой модели получить формат, который нужен вызывающему коду. Так режимы не смешиваются внутри цикла, а проверка количества групп остаётся одной.

\n
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// отдельным проходом по группам.
\n

Типы в этом фрагменте показывают границу адаптера, а не универсальную библиотеку. Точный тип объекта совпадения и доступ к его полям зависят от версии Phobos. Перед сборкой замените условную сигнатуру на ту, которую принимает ваш компилятор, и закрепите результат тестом.

\n

Смещение: байты не равны символам

\n

Самая дорогая ошибка возникает при переносе PREG_OFFSET_CAPTURE. В PHP смещение указывает позицию в байтах исходной строки. Строка D типа string хранит UTF-8. Один видимый символ может занимать несколько байтов. Поэтому byte offset и номер символа расходятся уже на кириллице.

\n
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.

\n
Обход совпадений регулярного выражения в D
Диапазон даёт совпадения, а адаптер выбирает форму выдачи.
\n

Симптом → причина → проверка → действие

\n
СимптомПричинаПроверкаДействие
Группы стоят не на своих местахСмешаны порядок по группам и порядок по совпадениямСравнить два совпадения с двумя группамиВынести преобразование в отдельную функцию
Поиск зациклилсяШаблон допускает пустое совпадениеПроверить пустой вход и * или ?Запретить шаблон или обработать продвижение позиции
ASCII работает, UTF-8 нетБайты приняли за символыСравнить byte offset с числом кодовых точекНазвать единицу и добавить тест на кириллице
PHP находит большеPCRE отличается от std.regexСкомпилировать шаблон D и сравнить группыПереписать шаблон или оставить разбор в PHP
Значение поглощает разделительКласс символов слишком широкийПроверить две записи и пустое значениеОграничить класс и решить судьбу пустого поля
\n

Порядок действий

\n
  1. Запишите вход, список полных совпадений, группы и единицу смещения.
  2. Скомпилируйте шаблон в целевой версии D. Не переносите расширения PCRE вслепую.
  3. Соберите внутренние записи: полное совпадение плюс группы.
  4. Выберите порядок по совпадениям или по группам. Назовите функцию по этому выбору.
  5. Проверьте пустой вход, отсутствие совпадений, одно и два соседних совпадения.
  6. Добавьте необязательную группу и зафиксируйте её представление.
  7. Проверьте UTF-8 и сравните byte offset с номером символа, если нужны обе величины.
  8. Сравните количество и содержимое результатов с эталоном PHP на том же наборе входов.
  9. Если шаблон описывает вложенную структуру, остановите перенос и выберите парсер.
\n

Ограничения и отрицательный путь

\n

Аналог не обязан копировать всю поверхность PHP. Переносите только нужные режимы и документируйте отсутствующие: стартовый offset, именованные ключи, необязательные группы и особые флаги. Метод с привычным названием не должен молча менять порядок результата или смысл unmatched-группы.

\n

Не используйте регулярное выражение для HTML, вложенных выражений и форматов с полноценным экранированием. Если разница PCRE и D ломает нужную конструкцию, отрицательный путь ясен: оставить обработку в PHP, применить библиотеку с нужным синтаксисом или изменить формат входа. Неполный адаптер, который иногда возвращает правдоподобный результат, опаснее явного отказа.

\n

Примеры в статье учебные. Они не доказывают скорость, потребление памяти или безопасность для произвольного пользовательского ввода. Для этих свойств нужны ограничения размера входа, набор случаев и отдельные измерения.

\n

Проверяемый критерий готовности

\n

Адаптер готов, когда на зафиксированных входах он возвращает ожидаемое число полных совпадений, тот же текст групп и документированную единицу offset. Минимальный набор включает отсутствие совпадений, два совпадения с группами, соседние совпадения, необязательную группу и строку UTF-8. Шаблон должен компилироваться в целевой версии D, а код должен отличать ошибку шаблона от корректного пустого результата.

\n

Это проверяет поведение, а не одно имя функции. Если сравнение не прошло, сначала исправьте контракт результата. Оптимизацию аллокаций и сокращение копирований выполняйте после совпадения с эталоном.

\n

Проверяемые источники

\n" + "contentHtml": "

Представим обработчик импорта, который переносит регулярное выражение из PHP в D. На строке mode=fast; retries=3 тест проходит, но после добавления mode=быстрый адаптер может принять байтовое смещение за номер символа. Другая ошибка появляется, когда необязательная группа превращает соседнее поле в пустую строку или null. Исключения при этом может не быть.

\n

Цена ошибки зависит от места, где работает разбор. В конфигурации это неверная настройка. В импорте — пропущенная или продублированная запись. В интерфейсе — диапазон подсветки, который начинается не там. Поэтому аналог preg_match_all нужно строить вокруг контракта результата, а не вокруг похожего имени метода.

\n

Тезис: сначала зафиксируйте форму результата

\n

preg_match_all ищет в исходной строке все непересекающиеся совпадения. После полного совпадения поиск продолжается с его конца. Форма массива зависит от режима. В режиме PREG_PATTERN_ORDER первый столбец содержит полные совпадения, следующие столбцы — группы. В режиме PREG_SET_ORDER каждый элемент содержит одно совпадение и его группы. Флаг PREG_OFFSET_CAPTURE добавляет позицию каждого фрагмента.

\n

В D модуль std.regex предоставляет matchAll. Он возвращает ленивый диапазон совпадений. Каждый элемент диапазона — объект Captures: индекс 0 содержит полный текст, следующие индексы — захватывающие группы. Это не готовая копия PHP-массива: адаптер должен выбрать собственную модель и явно преобразовать её в нужный порядок.

\n
ВопросPHPD
Все совпаденияpreg_match_all заполняет массивmatchAll возвращает диапазон
Одна записьЗависит от флага порядкаОдин элемент диапазона
ГруппаНомер или имя ключаИндекс или имя группы
Нет результатаКоличество совпадений равно нулюДиапазон пуст
ПозицияПри offset-флаге — байтовое смещениеЕдиницу измерения нужно закрепить в адаптере
\n

Механизм на коротком примере

\n

Пусть вход содержит пары параметров. Регулярное выражение выделяет имя и значение, но не проверяет всю конфигурацию. Это разные задачи: regex находит фрагменты, а код после него проверяет обязательность ключей, допустимые значения и повторения.

\n
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
\n

Цикл получает очередное совпадение из диапазона. Он не должен вручную искать следующий фрагмент с той же позиции. Полное совпадение доступно через hit.hit, группы — через числовой индекс или имя. Для необязательной группы текущий std.regex возвращает null-срез; PHP без PREG_UNMATCHED_AS_NULL обычно возвращает пустую строку. Адаптер должен выбрать одно представление и закрепить его тестом.

\n

Этот пример намеренно ограничен. Он не разбирает значение с экранированной точкой с запятой, кавычками или переносом строки. Не переносите его на CSV или вложенный язык только потому, что один тест прошёл. Для таких форматов regex может выделить токены, но состояние и грамматика нужны отдельному парсеру.

\n

Внутренняя модель и два порядка

\n

Удобнее сначала собрать строки по одному совпадению. Затем из этой модели получить формат, который нужен вызывающему коду. Так режимы не смешиваются внутри цикла, а проверка количества групп остаётся одной.

\n
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// отдельным проходом по группам.
\n

Типы в этом фрагменте показывают границу адаптера, а не универсальную библиотеку. В актуальном std.regex Captures индексируется с нуля: элемент 0 — полное совпадение, элементы с 1 — группы. Для старой версии D закрепите версию Phobos и сверяйте сигнатуры перед сборкой: статья использует API с matchAll, Regex и Captures, а не обещает обратную совместимость.

\n

Смещение: байты не равны символам

\n

Самая дорогая ошибка возникает при переносе PREG_OFFSET_CAPTURE. В PHP смещение указывает позицию в байтах исходной строки. Строка D типа string хранит UTF-8, а её length и длина среза считают байты. При этом std.regex сопоставляет текст на уровне Unicode-кодовых точек. Поэтому байтовый offset и номер кодовой точки расходятся уже на кириллице.

\n
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}
\n

У Captures свойство pre — это срез входа перед текущим совпадением. Его длина даёт байтовый offset без арифметики указателей: для one результат равен 0, для два — 4, потому что перед кириллицей стоят три ASCII-символа и пробел. Срез остаётся привязанным к исходной строке, поэтому числовое значение всё равно нужно сохранить сразу.

\n

Выберите один контракт. Для совместимости с PHP называйте поле byteOffset и возвращайте байтовую позицию. Если потребителю нужен номер Unicode-кодовой точки, считайте его отдельным проходом по UTF-8. UI может требовать ещё одну единицу — количество видимых графем. Не называйте все три значения просто position.

\n
Обход совпадений регулярного выражения в D
Диапазон даёт совпадения, а адаптер выбирает форму выдачи.
\n

Симптом → причина → проверка → действие

\n
СимптомПричинаПроверкаДействие
Группы стоят не на своих местахСмешаны порядок по группам и порядок по совпадениямСравнить два совпадения с двумя группамиВынести преобразование в отдельную функцию
Поиск зациклилсяШаблон допускает пустое совпадениеПроверить пустой вход и * или ?Запретить шаблон или обработать продвижение позиции
ASCII работает, UTF-8 нетБайты приняли за символыСравнить byte offset с числом кодовых точекНазвать единицу и добавить тест на кириллице
PHP находит большеPCRE отличается от std.regexСкомпилировать шаблон D и сравнить группыПереписать шаблон или оставить разбор в PHP
Значение поглощает разделительКласс символов слишком широкийПроверить две записи и пустое значениеОграничить класс и решить судьбу пустого поля
\n

Порядок действий

\n
  1. Запишите вход, список полных совпадений, группы и единицу смещения.
  2. Скомпилируйте шаблон в целевой версии D. Не переносите расширения PCRE вслепую.
  3. Соберите внутренние записи: полное совпадение плюс группы.
  4. Выберите порядок по совпадениям или по группам. Назовите функцию по этому выбору.
  5. Проверьте пустой вход, отсутствие совпадений, одно и два соседних совпадения.
  6. Добавьте необязательную группу и зафиксируйте её представление.
  7. Проверьте UTF-8 и сравните byte offset с номером символа, если нужны обе величины.
  8. Сравните количество и содержимое результатов с эталоном PHP на том же наборе входов.
  9. Если шаблон описывает вложенную структуру, остановите перенос и выберите парсер.
\n

Ограничения и отрицательный путь

\n

Аналог не обязан копировать всю поверхность PHP. Переносите только нужные режимы и документируйте отсутствующие: стартовый offset, именованные ключи, необязательные группы и особые флаги. Метод с привычным названием не должен молча менять порядок результата или смысл unmatched-группы.

\n

Не используйте регулярное выражение для HTML, вложенных выражений и форматов с полноценным экранированием. Если разница PCRE и D ломает нужную конструкцию, отрицательный путь ясен: оставить обработку в PHP, применить библиотеку с нужным синтаксисом или изменить формат входа. Неполный адаптер, который иногда возвращает правдоподобный результат, опаснее явного отказа.

\n

Примеры в статье учебные. Они не доказывают скорость, потребление памяти или безопасность для произвольного пользовательского ввода. Для этих свойств нужны ограничения размера входа, набор случаев и отдельные измерения.

\n

Проверяемый критерий готовности

\n

Адаптер готов, когда на зафиксированных входах он возвращает ожидаемое число полных совпадений, тот же текст групп и документированную единицу offset. Минимальный набор включает отсутствие совпадений, два совпадения с группами, соседние совпадения, необязательную группу и строку UTF-8. Шаблон должен компилироваться в целевой версии D, а код должен отличать ошибку шаблона от корректного пустого результата.

\n

Это проверяет поведение, а не одно имя функции. Если сравнение не прошло, сначала исправьте контракт результата. Оптимизацию аллокаций и сокращение копирований выполняйте после совпадения с эталоном.

\n

Проверяемые источники

\n" }