Заметка про группировки в регулярных выражениях при использовании preg_match_all

Сколько лет я в индустрии, более 20 лет в разработке и в PHP... За свою жизнь много использовал регулярные Perl и POSIX совместимые регулярные выражения и только недавно узнал что в PCRE регулярках помимо именованных груп есть еще и маркированные группы.

И вроде как они даже по потреблению ресурсов выигрывают перед именованными (как минимум нет дублирования значений).

Короче, суть: в регулярных выражениях можно получить группы искомых вхождений:

if (preg_match_all('~([a-z])|(\d)~', 'a 1 bc 23 def', $a))
{
    var_dump($a);
}

Получим следующий вывод:

array(3) {
  [0]=>
  array(9) {
    [0]=>
    string(1) "a"
    [1]=>
    string(1) "1"
    [2]=>
    string(1) "b"
    [3]=>
    string(1) "c"
    [4]=>
    string(1) "2"
    [5]=>
    string(1) "3"
    [6]=>
    string(1) "d"
    [7]=>
    string(1) "e"
    [8]=>
    string(1) "f"
  }
  [1]=>
  array(9) {
    [0]=>
    string(1) "a"
    [1]=>
    string(0) ""
    [2]=>
    string(1) "b"
    [3]=>
    string(1) "c"
    [4]=>
    string(0) ""
    [5]=>
    string(0) ""
    [6]=>
    string(1) "d"
    [7]=>
    string(1) "e"
    [8]=>
    string(1) "f"
  }
  [2]=>
  array(9) {
    [0]=>
    string(0) ""
    [1]=>
    string(1) "1"
    [2]=>
    string(0) ""
    [3]=>
    string(0) ""
    [4]=>
    string(1) "2"
    [5]=>
    string(1) "3"
    [6]=>
    string(0) ""
    [7]=>
    string(0) ""
    [8]=>
    string(0) ""
  }
}

В нулевом массиве всегда будут все вхождения, а вот далее мы применили группировки. И тут прям из примера видно что ничего не понятно или понятно, но с трудом - дублируется информация, много лишнего.

Именованные группы

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

<?php declare(strict_types=1);


preg_match_all('~(?P<alpha>[a-z]+)|(?P<digit>\d+)~', '1 a 2 bc 34 56 def', $a);
foreach (array_filter($a, 'is_string', ARRAY_FILTER_USE_KEY) as $key => $items)
    if (is_array($items) && !empty($items))
        foreach($items as $val)
            if (!empty($val))
                print "'$val' is $key\n";

Получаем такой вывод:

'a' is alpha
'bc' is alpha
'def' is alpha
'1' is digit
'2' is digit
'34' is digit
'56' is digit

Маркированные группы

И вот только недавно в мануалах я нашел что-то про маркированные группы:

pcre2pattern specification

Абзац про "Recording which path was taken"

Суть: каждую группу можно пометить маркером. В отличие от именованных групп, маркированные работают немного по другому, от этого и синтаксис другой:

<?php declare(strict_types=1);


preg_match_all('~(?:[a-z]+)(*:alpha)|(?:\d+)(*:digit)~', '1 a 2 bc 34 56 def', $a);
foreach($a[0] as $key => $val)
    print "'$val' is {$a['MARK'][$key]} \n";

Так выглядит предыдущий алгоритм, но с применением марикрованных груп.

Синтаксис:

(?:some regex)(*MARK:nameofgroup)

 или сокращенная запись
 
(?:some regex)(*:nameofgroup)

И в том и в том случае на выходе будет сформирован ключ MARK в котором будут указаны имена групп для каждого найденного совпадения.

array(2) {
  [0]=>
  array(7) {
    [0]=>
    string(1) "1"
    [1]=>
    string(1) "a"
    [2]=>
    string(1) "2"
    [3]=>
    string(2) "bc"
    [4]=>
    string(2) "34"
    [5]=>
    string(2) "56"
    [6]=>
    string(3) "def"
  }
  ["MARK"]=>
  array(7) {
    [0]=>
    string(5) "digit"
    [1]=>
    string(5) "alpha"
    [2]=>
    string(5) "digit"
    [3]=>
    string(5) "alpha"
    [4]=>
    string(5) "digit"
    [5]=>
    string(5) "digit"
    [6]=>
    string(5) "alpha"
  }
}

Резюмируя: иногда может быть удобнее и эффективнее работать с маркированными группами вместо именованных.