为什么这个正则表达式有3个匹配,而不是5个?

时间:2010-09-14 19:58:35

标签: php regex

我在PHP中编写了一个非常简单的preg_match_all文件:

$fileName = 'A_DATED_FILE_091410.txt';
$matches = array();
preg_match_all('/[0-9][0-9]/',$fileName,$matches);
print_r($matches);

我的预期输出:

$matches = array(
    [0] => array(
        [0] => 09,
        [1] => 91,
        [2] => 14,
        [3] => 41,
        [4] => 10
    )
)

我得到了什么:

$matches = array(
    [0] => array(
        [0] => 09,
        [1] => 14,
        [2] => 10
    )
)

现在,在这个特殊用例中,这是更可取的,但我想知道为什么它与其他子串不匹配?还有,正则表达式可能会给我我预期的输出,如果是,它是什么?

4 个答案:

答案 0 :(得分:7)

使用全局正则表达式(preg_match_all使用的),一旦匹配,正则表达式引擎将继续从上一个匹配结束时搜索字符串。

在您的情况下,正则表达式引擎从字符串的开头开始,并前进到0,因为这是匹配[0-9]的第一个字符。然后它会前进到下一个位置(9),由于这与第二个[0-9]匹配,因此需要09作为匹配。当引擎继续匹配时(因为它尚未到达字符串的末尾),它再次前进其位置(到1)(然后重复上述步骤)。

另请参阅:First Look at How a Regex Engine Works Internally


如果您必须获取每2位数序列,您可以使用preg_match并使用偏移来确定从哪里开始捕获:

$fileName = 'A_DATED_FILE_091410.txt';
$allSequences = array();
$matches = array();
$offset = 0;

while (preg_match('/[0-9][0-9]/', $fileName, $matches, PREG_OFFSET_CAPTURE, $offset))
{
  list($match, $offset) = $matches[0];
  $allSequences[] = $match;
  $offset++; // since the match is 2 digits, we'll start the next match after the first
}

请注意,使用PREG_OFFSET_CAPTURE标志返回的偏移量是匹配的 start


我有另一个解决方案,可以获得五场比赛,而不必使用偏移,但我只是为了好奇而在这里添加它,我可能不会在生产代码中自己使用它(它也是一个有点复杂的正则表达式)。您可以使用使用lookbehind的正则表达式在当前位置之前查找数字,并使用captures查看后备中的数字(通常情况下,结果是非捕获的):

(?<=([0-9]))[0-9]

让我们来看看这个正则表达式:

(?<=       # open a positive lookbehind
  (        # open a capturing group
    [0-9]  # match 0-9
  )        # close the capturing group
)          # close the lookbehind
[0-9]      # match 0-9

因为lookarounds是零宽度并且不移动正则表达式位置,所以这个正则表达式将匹配5次:引擎将前进到9(因为这是第一个满足lookbehind断言的位置)。由于9匹配[0-9],引擎会将9作为匹配项(但是因为我们在环视中捕获它,它还会捕获0!)。然后引擎移动到1。同样,lookbehind成功(并捕获),1被添加为第一个子组匹配(依此类推,直到引擎命中字符串的结尾)。

当我们将此模式提供给preg_match_all时,我们最终会得到一个看起来像的数组(使用PREG_SET_ORDER标志将捕获组与完整匹配分组):

Array
(
    [0] => Array
        (
            [0] => 9
            [1] => 0
        )

    [1] => Array
        (
            [0] => 1
            [1] => 9
        )

    [2] => Array
        (
            [0] => 4
            [1] => 1
        )

    [3] => Array
        (
            [0] => 1
            [1] => 4
        )

    [4] => Array
        (
            [0] => 0
            [1] => 1
        )

)

请注意,每个“匹配”的数字都不按顺序排列!这是因为lookbehind中的捕获组变为反向引用1,而整个匹配是反向引用0.我们可以按正确的顺序将它重新组合在一起,但是:

preg_match_all('/(?<=([0-9]))[0-9]/', $fileName, $matches, PREG_SET_ORDER);
$allSequences = array();
foreach ($matches as $match)
{
  $allSequences[] = $match[1] . $match[0];
}

答案 1 :(得分:2)

搜索下一场比赛从上一场比赛后的第一个角色开始。因此,当09中匹配091410时,搜索下一场比赛将从1410开始。

答案 2 :(得分:1)

  

此外,正如可能的正则表达式   给我预期的输出,如果是的话,   它是什么?

没有一个人会工作,因为它不会匹配同一部分两次。但你可以这样做:

$i = 0;
while (preg_match($pattern, $subject, $matches, PREG_OFFSET_CAPTURE, $i))
{
  $i = $matches[0][1]; /* + 1 in many cases */
}

以上情况对一般情况不安全。根据模式的不同,你可能陷入无限循环。此外,您可能不希望[0][1],而是[1][1]等等,这取决于模式。

对于这种特殊情况,我认为自己做这件事要简单得多:

$l = strlen($s);
$prev_digit = false;
for ($i = 0; $i < $l; ++$i)
{
  if ($s[$i] >= '0' && $s[$i] <= '9')
  {
    if ($prev_digit) { /* found match */ }
    $prev_digit = true;
  }
  else
    $prev_digit = false;
}

答案 3 :(得分:1)

只是为了好玩,另一种方式:

 <?php
 $fileName = 'A_DATED_FILE_091410.txt';
 $matches = array();
 preg_match_all('/(?<=([0-9]))[0-9]/',$fileName,$matches);
 $result = array();
 foreach($matches[1] as $i => $behind)
 {
     $result[] = $behind . $matches[0][$i];
 }
 print_r($result);
 ?>