Регулярное выражение с возможными пустыми совпадениями и многострочным соответствием

Я пытался "разобрать" некоторые данные, используя регулярное выражение, и мне кажется, что я близок к этому, но я просто не могу собрать все это домой.
данные, требующие разбора, обычно выглядят так: <param>: <value>\n. Количество параметров может варьироваться, как и значение. Тем не менее, вот пример:

FooID: 123456
Name: Chuck
When: 01/02/2013 01:23:45
InternalID: 789654
User Message: Hello,
this is nillable, but can be quite long. Text can be spread out over many lines
And can start with any number of \n's. It can be empty, too.
What's worse, though is that this CAN contain colons (but they're _"escaped"_ using `\`), and even basic markup!

Чтобы вставить этот текст в объект, я составил это небольшое выражение

if (preg_match_all('/^([^:\n\\]+):\s*(.+)/m', $this->structuredMessage, $data))
{
    $data = array_combine($data[1], $data[2]);
    //$data is assoc array FooID => 123456, Name => Chuck, ...
    $report = new Report($data);
}

Теперь это работает нормально большую часть времени, за исключением бита User Message: . не соответствует новым строкам, потому что, если бы я использовал флаг s, вторая группа соответствовала бы всему после FooID: до самого конца строки.
Мне приходится использовать грязный обходной путь для этого:

$msg = explode(end($data[1], $string);
$data[2][count($data[2])-1] = array_pop($msg);

После некоторого тестирования я понял, что иногда один или два параметра не заполнены (например, InternalID может быть пустым). В этом случае мое выражение не терпит неудачу, а скорее приводит к:

    [1] => Array
        (
            [0] => FooID
            [1] => Name
            [2] => When
            [3] => InternalID
        )

    [2] => Array
        (
            [0] => 123465
            [1] => Chuck
            [2] => 01/02/2013 01:23:45
            [3] => User Comment: Hello,
        )

Я пробовал различные другие выражения и придумал это:

/^([^:\n\\]++)\s{0,}:(.*+)(?!^[^:\n\\]++\s{0,}:)/m
//or:
/^([^:\n\\]+)\s{0,}:(.*)(?!^[^:\\\n]+\s{0,}:)/m

Вторая версия немного медленнее.
Это решает проблемы, которые у меня были с InternalID: <void>, но все же оставляет последнее препятствие: User Message: <multi-line>. Использование флага s не работает с моим выражением ATM.
Я могу думать только об этом:

^([^:\n\\]++)\s{0,}:((\n(?![^\n:\\]++\s{0,}:)|.)*+)

Что, по крайней мере, на мой взгляд, слишком сложно, чтобы быть единственным вариантом. Идеи, предложения, ссылки, ... все будет очень признательно


person Elias Van Ootegem    schedule 10.07.2013    source источник
comment
Если содержимое также может содержать двоеточия, какие критерии мы можем использовать для их идентификации, чтобы не спутать их с двоеточием, разделяющим ключ/значение?   -  person Tim Pietzcker    schedule 10.07.2013
comment
@TimPietzcker: Хорошо, я проверил. Текст генерируется другой программой, которая на самом деле экранирует двоеточия в пользовательском вводе. Я обновил свои выражения, как здесь, так и в моем коде :)   -  person Elias Van Ootegem    schedule 10.07.2013
comment
О, если двоеточия экранированы, то это легко. Подожди секунду.   -  person Tim Pietzcker    schedule 10.07.2013


Ответы (4)


Следующее регулярное выражение должно работать, но я больше не уверен, что это правильный инструмент для этого:

preg_match_all(
    '%^            # Start of line
    ([^:]*)        # Match anything until a colon, capture in group 1
    :\s*           # Match a colon plus optional whitespace
    (              # Match and capture in group 2:
     (?:           # Start of non-capturing group (used for alternation)
      .*$          #  Either match the rest of the line
      (?=          #  only if one of the following follows here:
       \Z          #  The end of the string
      |            #  or
       \r?\n       #  a newline
       [^:\n\\\\]* #  followed by anything except colon, backslash or newline
       :           #  then a colon
      )            #  End of lookahead
     |             # or match
      (?:          #  Start of non-capturing group (used for alternation/repetition)
       [^:\\\\]    #  Either match a character except colon or backslash
      |            #  or
       \\\\.       #  match any escaped character
      )*           #  Repeat as needed (end of inner non-capturing group)
     )             # End of outer non-capturing group
    )              # End of capturing group 2
    $              # Match the end of the line%mx', 
    $subject, $result, PREG_PATTERN_ORDER);

Смотрите в прямом эфире на regex101.

person Tim Pietzcker    schedule 10.07.2013
comment
Спасибо, хотя (извините за придирчивость) экранируются только двоеточия в пользовательском вводе. Другие значения гарантированно будут встроенными (однострочные совпадения), но часто указывается дата j/m/Y H:i:s - person Elias Van Ootegem; 10.07.2013
comment
@EliasVanOotegem: я включил это в регулярное выражение. Все должно снова быть в порядке, но оно стало довольно громоздким. Я почти уверен, что мне будет трудно отлаживать это через шесть месяцев... - person Tim Pietzcker; 10.07.2013

я новичок в PHP, так что, возможно, это совершенно не так, но, возможно, вы могли бы использовать что-то вроде

$data = <<<EOT
FooID: 123456
Name: Chuck
When: 01/02/2013 01:23:45
InternalID: 789654
User Message: Hello,
this is nillable, but can be quite long. Text can be spread out over many     lines
And can start with any number of \n's. It can be empty, too
EOT;

if ($key = preg_match_all('~^[^:\n]+?:~m', $data, $match)) {
    $val = explode('¬', preg_filter('~^[^:\n]+?:~m', '¬', $data));

    array_shift($val);

    $res = array_combine($match[0], $val);
}

print_r($res);

урожаи

Array
(
    [FooID:] =>  123456
    [Name:] =>  Chuck
    [When:] =>  01/02/2013 01:23:45
    [InternalID:] =>  789654
    [User Message:] =>  Hello,
this is nillable, but can be quite long. Text can be spread out over many     lines
And can start with any number of 
's. It can be empty, too
)
person mzmm56    schedule 10.07.2013
comment
Спасибо за усилия, ~^[^:\n]+?~m было бы лучше, IMO (не включая двоеточие в совпадении, но для этого все еще требуются 2 регулярных выражения и не работает с экранированными двоеточиями - person Elias Van Ootegem; 10.07.2013

Итак, вот что я придумал, используя хитрый preg_replace_callback():

$string ='FooID: 123456
Name: Chuck
When: 01/02/2013 01:23:45
InternalID: 789654
User Message: Hello,
this is nillable, but can be quite long. Text can be spread out over many lines
And can start with any number of \n\'s. It can be empty, too
Yellow:cool';

$array = array();
preg_replace_callback('#^(.*?):(.*)|.*$#m', function($m)use(&$array){
    static $last_key = ''; // We are going to use this as a reference
    if(isset($m[1])){// If there is a normal match (key : value)
        $array[$m[1]] = $m[2]; // Then add to array
        $last_key = $m[1]; // define the new last key
    }else{ // else
        $array[$last_key] .= PHP_EOL . $m[0]; // add the whole line to the last entry
    }
}, $string); // Anonymous function used thus PHP 5.3+ is required
print_r($array); // print

Онлайн-демонстрация

Недостаток: я использую PHP_EOL для добавления новых строк, связанных с ОС.

person HamZa    schedule 10.07.2013
comment
Спасибо за усилия, но я забыл упомянуть, что пользовательское сообщение также может содержать двоеточие. который портит ваш код. Также: я не заинтересован в использовании замыканий в PHP. - person Elias Van Ootegem; 10.07.2013

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

Схема базового алгоритма

  1. Разделите строку на \n, используя explode
  2. Loop over the resulting array
    1. Split the resulting strings on : also using explode with a limit of 2.
    2. Если длина созданного массива меньше 2, добавьте все данные к предыдущему значению ключа.
    3. В противном случае используйте первый индекс массива в качестве ключа, а второй — в качестве значения, если только разделенное двоеточие не было экранировано (в этом случае вместо этого добавьте ключ + разделение + значение к предыдущему значению ключа)

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

Код

$str = <<<EOT
FooID: 123456
Name: Chuck
When: 01/02/2013 01:23:45
InternalID: 
User Message: Hello,
this is nillable, but can be quite long. Text can be spread out over many lines
This\: works too. And can start with any number of \\n's. It can be empty, too.
What's worse, though is that this CAN contain colons (but they're _"escaped"_


using `\`) like so `\:`, and even basic markup!
EOT;

$arr = explode("\n", $str);

$prevKey = '';
$split = ': ';
$output = array();
for ($i = 0, $arrlen = sizeof($arr); $i < $arrlen; $i++) {
  $keyValuePair = explode($split, $arr[$i], 2);
  // ?: Is this a valid key/value pair
  if (sizeof($keyValuePair) < 2 && $i > 0) {
    // -> Nope, append the value to the previous key's value
    $output[$prevKey] .= "\n" . $keyValuePair[0];
  }
  else {
    // -> Maybe
    // ?: Did we miss an escaped colon
    if (substr($keyValuePair[0], -1) === '\\') {
      // -> Yep, this means this is a value, not a key/value pair append both key and
      // value (including the split between) to the previous key's value ignoring
      // any colons in the rest of the string (allowing dates to pass through)
      $output[$prevKey] .= "\n" . $keyValuePair[0] . $split . $keyValuePair[1];
    }
    else {
      // -> Nope, create a new key with a value
      $output[$keyValuePair[0]] = $keyValuePair[1];
      $prevKey = $keyValuePair[0];
    }
  }
}

var_dump($output);

Выход

array(5) {
  ["FooID"]=>
  string(6) "123456"
  ["Name"]=>
  string(5) "Chuck"
  ["When"]=>
  string(19) "01/02/2013 01:23:45"
  ["InternalID"]=>
  string(0) ""
  ["User Message"]=>
  string(293) "Hello,
this is nillable, but can be quite long. Text can be spread out over many lines
This\: works too. And can start with any number of \n's. It can be empty, too.
What's worse, though is that this CAN contain colons (but they're _"escaped"_


using `\`) like so `\:`, and even basic markup!"
}

Онлайн-демонстрация

person ohaal    schedule 10.07.2013
comment
Спасибо, но я забыл упомянуть, что значения всегда содержат двоеточие (формат даты заканчивается на H:i:s), а пользовательское сообщение почти всегда содержит \n и может также содержать двоеточия. - person Elias Van Ootegem; 10.07.2013
comment
@EliasVanOotegem: Учитывая, что двоеточия экранированы, я исправил это. - person ohaal; 10.07.2013