PHP и RegEx: разделите строку запятыми, которые не находятся внутри квадратных скобок (а также вложенных скобок)

Два дня назад я начал работать над парсером кода и застрял.

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

У меня есть эта строка для анализа:

one, two, three, (four, (five, six), (ten)), seven

Я хотел бы получить этот результат:

array(
 "one"; 
 "two"; 
 "three"; 
 "(four, (five, six), (ten))"; 
 "seven"
)

но вместо этого я получаю:

array(
  "one"; 
  "two"; 
  "three"; 
  "(four"; 
  "(five"; 
  "six)"; 
  "(ten))";
  "seven"
)

Как я могу сделать это в PHP RegEx.

Заранее спасибо !


person Cristian Toma    schedule 05.07.2009    source источник


Ответы (7)


Вы можете сделать это проще:

preg_match_all('/[^(,\s]+|\([^)]+\)/', $str, $matches)

Но было бы лучше, если бы вы использовали настоящий парсер. Может быть, что-то вроде этого:

$str = 'one, two, three, (four, (five, six), (ten)), seven';
$buffer = '';
$stack = array();
$depth = 0;
$len = strlen($str);
for ($i=0; $i<$len; $i++) {
    $char = $str[$i];
    switch ($char) {
    case '(':
        $depth++;
        break;
    case ',':
        if (!$depth) {
            if ($buffer !== '') {
                $stack[] = $buffer;
                $buffer = '';
            }
            continue 2;
        }
        break;
    case ' ':
        if (!$depth) {
            continue 2;
        }
        break;
    case ')':
        if ($depth) {
            $depth--;
        } else {
            $stack[] = $buffer.$char;
            $buffer = '';
            continue 2;
        }
        break;
    }
    $buffer .= $char;
}
if ($buffer !== '') {
    $stack[] = $buffer;
}
var_dump($stack);
person Gumbo    schedule 05.07.2009
comment
Да, проще, но не работает в случае вложенных скобок, например: раз, два, три, (четыре, (пять, шесть), (десять)), семь - person Cristian Toma; 06.07.2009
comment
Это тот момент, когда вы должны использовать настоящий парсер. Регулярные выражения не могут считать или обрабатывать состояния. - person Gumbo; 06.07.2009
comment
Я должен использовать регулярные выражения. Регулярные выражения рекурсивны и жадны, вы можете сделать это, используя их. - person Cristian Toma; 06.07.2009
comment
Нет, ты не можешь. Конечно, в современных реализациях есть функции, которые могут это сделать, такие как группа балансировки (?<name1-name2> … ) msdn.microsoft.com/bs2twtah.aspx. Но они используют конечный автомат, и это уже не регулярное выражение в классическом стиле. - person Gumbo; 06.07.2009
comment
Это более правильно, но все еще не работает для вложенных скобок /[^(,]*(?:([^)]+))?[^),]*/ - person DarkSide; 25.03.2013

Хм... ОК, уже отмечен как ответ, но, поскольку вы просили простое решение, я все же попробую:

$test = "one, two, three, , , ,(four, five, six), seven, (eight, nine)";
$split = "/([(].*?[)])|(\w)+/";
preg_match_all($split, $test, $out);
print_r($out[0]);              

Выход

Array
(
    [0] => one
    [1] => two
    [2] => three
    [3] => (four, five, six)
    [4] => seven
    [5] => (eight, nine)
)
person merkuro    schedule 05.07.2009
comment
Большое спасибо, ваша помощь очень ценится. Но теперь я понимаю, что я также столкнусь с вложенными скобками, и ваше решение не применимо. - person Cristian Toma; 06.07.2009

Вы не можете, напрямую. Вам понадобится, как минимум, просмотр назад с переменной шириной, и, насколько я знаю, PHP PCRE имеет только просмотр назад с фиксированной шириной.

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

person chaos    schedule 05.07.2009
comment
Да, это был хак, который я планировал использовать. Замените скобки на $1, $2 или что-то подобное, разделите строку и восстановите скобки в результате. Спасибо ! - person Cristian Toma; 06.07.2009
comment
Дело в том, что то, что вы описываете, не является обычным языком, поэтому регулярные выражения не подходят. Таким образом, сначала разобрать все вложенные части — это не хак, а наиболее разумная вещь. - person Svante; 06.07.2009

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

function process($data)
{
        $entries = array();
        $filteredData = $data;
        if (preg_match_all("/\(([^)]*)\)/", $data, $matches)) {
                $entries = $matches[0];
                $filteredData = preg_replace("/\(([^)]*)\)/", "-placeholder-", $data);
        }

        $arr = array_map("trim", explode(",", $filteredData));

        if (!$entries) {
                return $arr;
        }

        $j = 0;
        foreach ($arr as $i => $entry) {
                if ($entry != "-placeholder-") {
                        continue;
                }

                $arr[$i] = $entries[$j];
                $j++;
        }

        return $arr;
}

Если вы вызываете его так:

$data = "one, two, three, (four, five, six), seven, (eight, nine)";
print_r(process($data));

Он выводит:

Array
(
    [0] => one
    [1] => two
    [2] => three
    [3] => (four, five, six)
    [4] => seven
    [5] => (eight, nine)
)
person Emil H    schedule 05.07.2009
comment
Большое спасибо, это должно сработать. Сначала я планировал сделать это так, но подумал, что существует более простой способ. - person Cristian Toma; 06.07.2009
comment
Ваш метод не может разобрать один, два, три, ((пять), (четыре(шесть))), семь, восемь, девять. Я думаю, что правильное регулярное выражение будет рекурсивным: /(([^()]+|(?R))*)/. - person Cristian Toma; 06.07.2009
comment
Однако вы не упомянули, что он должен был иметь возможность анализировать рекурсивные выражения, когда я впервые написал этот ответ. Тем не менее, другие определенно предложили лучшие решения после того, как я написал это. - person Emil H; 06.07.2009

Неуклюже, но работает...

<?php

function split_by_commas($string) {
  preg_match_all("/\(.+?\)/", $string, $result); 
  $problem_children = $result[0];
  $i = 0;
  $temp = array();
  foreach ($problem_children as $submatch) { 
    $marker = '__'.$i++.'__';
    $temp[$marker] = $submatch;
    $string   = str_replace($submatch, $marker, $string);  
  }
  $result = explode(",", $string);
  foreach ($result as $key => $item) {
    $item = trim($item);
    $result[$key] = isset($temp[$item])?$temp[$item]:$item;
  }
  return $result;
}


$test = "one, two, three, (four, five, six), seven, (eight, nine), ten";

print_r(split_by_commas($test));

?>
person Dycey    schedule 05.07.2009

Я боюсь, что может быть очень сложно разобрать вложенные скобки, такие как one, two, (three, (four, five)), только с помощью RegExp.

person MyKey_    schedule 05.07.2009

Я чувствую, что стоит отметить, что вы всегда должны избегать регулярных выражений, когда это возможно. С этой целью вы должны знать, что для PHP 5.3+ вы можете использовать str_getcsv(). Однако если вы работаете с файлами (или файловыми потоками), такими как файлы CSV, функция fgetcsv() может быть тем, что вам нужно, и он доступен с PHP4.

Наконец, я удивлен, что никто не использовал preg_split( ), или он не работал так, как нужно?

person ken    schedule 06.07.2009
comment
Да, Кен, я хочу использовать preg_split(), но каким будет регулярное выражение, игнорирующее запятые в скобках? - person Cristian Toma; 06.07.2009
comment
Ах да, хороший момент, попробовав минуту или 2, я вижу, что это сложно с изложенными условиями. - person ken; 06.07.2009
comment
Да, вы правы, я тоже пробовал ваше решение и не работает. Спасибо еще. - person Cristian Toma; 07.07.2009