PHP正则表达式preg_match提取

5
尽管我已经掌握了伪代码中的正则表达式知识,但我在将其翻译为php regex perl时遇到了困难。
我正在尝试使用preg_match提取表达式的一部分。
我有以下字符串${classA.methodA.methodB(classB.methodC(classB.methodD)))},需要完成两件事:

a.验证语法

  • ${classA.methodA.methodB(classB.methodC(classB.methodD)))} 有效
  • ${classA.methodA.methodB} 有效
  • ${classA.methodA.methodB()} 无效
  • ${methodB(methodC(classB.methodD)))} 无效

b. 我需要提取这些信息 ${classA.methodA.methodB(classB.methodC(classB.methodD)))} 应该返回

 1. classA
 2. methodA
 3. methodB(classB.methodC(classB.methodD)))

我已经创建了以下代码

$expression = '${myvalue.fdsfs.fsdf.blo(fsdf.fsfds(fsfs.fs))}';
$pattern = '/\$\{(?:([a-zA-Z0-9]+)\.)(?:([a-zA-Z\d]+)\.)*([a-zA-Z\d.()]+)\}/';
if(preg_match($pattern, $expression, $matches))
{
    echo 'found'.'<br/>';
    for($i = 0; $i < count($matches); $i++)
        echo $i." ".$matches[$i].'<br/>';
}

结果是:
找到了
0 ${myvalue.fdsfs.fsdf.blo(fsdf.fsfds(fsfs.fs))}
1 myvalue
2 fsdf
3 blo(fsdf.fsfds(fsfs.fs))
显然,我很难提取重复的方法,并且它没有正确验证(老实说,我把它留到最后一步解决),所以允许空括号,并且它不检查一旦括号打开就必须关闭。
谢谢大家
更新
X m.buettner 感谢您的帮助。我快速尝试了您的代码,但出现了一个非常小的问题,尽管我可以绕过它。这个问题与我之前没有发布在这里的代码中的问题相同,当我尝试这个字符串时:
$expression = '${myvalue.fdsfs}';

使用您的模式定义,它显示为:
found
0 ${myvalue.fdsfs}
1 myvalue.fdsfs
2 myvalue
3 
4 fdsfs

如您所见,第三行被识别为一个不存在的空格。我不明白为什么会这样,您能否建议我如何解决,或者是因为 PHP 正则表达式的限制而不得不容忍它?

话虽如此,我只能说谢谢您。您不仅回答了我的问题,还尽可能提供了许多关于开发模式时应该遵循的正确路径的建议。

最后一件事,我(愚蠢地)忘记添加一个重要的小案例,即通过逗号分隔的多个参数,所以

$expression = '${classA.methodAA(classB.methodBA(classC.methodCA),classC.methodCB)}';
$expression = '${classA.methodAA(classB.methodBA(classC.methodCA),classC.methodCB,classD.mehtodDA)}';

必须是有效的。

我将其编辑为:

    $expressionPattern =             
        '/
        ^                   # beginning of the string
        [$][{]              # literal ${
        (                   # group 1, used for recursion
          (                 # group 2 (class name)
            [a-z\d]+        # one or more alphanumeric characters
          )                 # end of group 2 (class name)
          [.]               # literal .
          (                 # group 3 (all intermediate method names)
            (?:             # non-capturing group that matches a single method name
              [a-z\d]+      # one or more alphanumeric characters
              [.]           # literal .
            )*              # end of method name, repeat 0 or more times
          )                 # end of group 3 (intermediate method names);
          (                 # group 4 (final method name and arguments)
            [a-z\d]+        # one or or more alphanumeric characters
            (?:             # non-capturing group for arguments
              [(]           # literal (
              (?1)          # recursively apply the pattern inside group 1
                (?:     # non-capturing group for multiple arguments        
                  [,]       # literal ,
                  (?1)      # recursively apply the pattern inside group 1 on parameters
                )*          # end of multiple arguments group; repeat 0 or more times
              [)]           # literal )
            )?              # end of argument-group; make optional
          )                 # end of group 4 (method name and arguments)  
        )                   # end of group 1 (recursion group)
        [}]                 # literal }
        $                   # end of the string
        /ix';   

X Casimir and Hippolyte

你的建议也很好,但使用这段代码时会出现一些复杂的情况。我的意思是说,代码本身很容易理解,但灵活性较差。话虽如此,它也为我提供了许多信息,肯定在未来会有所帮助。

X Denomales

感谢您的支持,但您的代码在我尝试后出现了错误:

$sourcestring='${classA1.methodA0.methodA1.methodB1(classB.methodC(classB.methodD))}';

结果为:

Array

( [0] => 数组 ( [0] => ${classA1方法A0方法A1方法B1(classB方法C(classB方法D))} )

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

[2] => Array
    (
        [0] => methodA0
    )

[3] => Array
    (
        [0] => methodA1.methodB1(classB.methodC(classB.methodD))
    )

)

应该是这样的

    [2] => Array
    (
        [0] => methodA0.methodA1
    )

[3] => Array
    (
        [0] => methodB1(classB.methodC(classB.methodD))
    )

)

或者
[2] => Array
    (
        [0] => methodA0
    )

[3] => Array
    (
        [0] => methodA1
    )

[4] => Array
    (
        [0] => methodB1(classB.methodC(classB.methodD))
    )

)
3个回答

6
这是一个棘手的问题。递归模式通常超出了正则表达式的范畴,即使可能实现,也会导致非常难以理解和维护的表达式。
您正在使用PHP,因此使用PCRE,它确实支持递归正则表达式构造(?n)。由于您的递归模式相当规则,因此可以使用正则表达式找到一种相对实用的解决方案。
我应该立即提到一个警告:由于您允许每个级别中任意数量的“中间”方法调用(在您的片段fdsfsfsdf中),因此您无法将所有这些内容捕获到单独的捕获组中。这在PCRE中是不可能的。每次匹配始终会产生相同有限数量的捕获组,由您的模式包含的开放括号的数量确定。如果重复使用捕获组(例如使用类似([a-z]+\.)+的东西),则每次使用该组时,先前的捕获将被覆盖,并且您只会得到最后一个实例。因此,我建议您将所有“中间”方法调用捕获在一起,然后简单地explode该结果。
同样,您无法一次获取多个嵌套级别的捕获(如果您想要的话)。因此,您所需的捕获(最后一个包括所有嵌套级别)是唯一的选择 - 然后,您可以再次将模式应用于最后一个匹配项以进一步深入一级。
现在看看实际表达式:
$pattern = '/
    ^                     # beginning of the string
    [$][{]                # literal ${
    (                     # group 1, used for recursion
      (                   # group 2 (class name)
        [a-z\d]+          # one or more alphanumeric characters
      )                   # end of group 2 (class name)
      [.]                 # literal .
      (                   # group 3 (all intermediate method names)
        (?:               # non-capturing group that matches a single method name
          [a-z\d]+        # one or more alphanumeric characters
          [.]             # literal .
        )*                # end of method name, repeat 0 or more times
      )                   # end of group 3 (intermediate method names);
      (                   # group 4 (final method name and arguments)
        [a-z\d]+          # one or or more alphanumeric characters
        (?:               # non-capturing group for arguments
          [(]             # literal (
          (?1)            # recursively apply the pattern inside group 1
          [)]             # literal )
        )?                # end of argument-group; make optional
      )                   # end of group 4 (method name and arguments)  
    )                     # end of group 1 (recursion group)
    [}]                   # literal }
    $                     # end of the string
    /ix';

一些通用注意事项:对于复杂的表达式(在支持它的正则表达式中),始终使用自由间隔x修饰符,它允许您引入空格和注释来格式化表达式以满足您的需求。如果没有它们,模式看起来像这样:
'/^[$][{](([a-z\d]+)[.]((?:[a-z\d]+[.])*)([a-z\d]+(?:[(](?1)[)])?))[}]$/ix'

即使您自己编写了正则表达式并且是唯一的项目工作人员-尝试在一个月后理解它。
其次,我使用不区分大小写的 i 修饰符略微简化了模式。 它只是消除了一些杂乱,因为您可以省略字母的大写变体。
第三,注意我使用类似于 [$] [.] 的单字符类来转义可能的字符。 这只是品味问题,您可以自由地使用反斜杠变量。 我只是个人更喜欢字符类的可读性(我知道其他人在这里持不同意见),所以我也想向您介绍此选项。
第四,我在您的模式周围添加了锚点,以便在 $ {...} 之外没有无效的语法。
最后,递归是如何工作的?(?n)类似于反向引用\n,它引用捕获组n(从左到右按括号计数)。区别在于,反向引用尝试重新匹配与组n匹配的内容,而(?n)再次应用模式。即(.)\1连续两次匹配任何字符,而(.)(?1)匹配任何字符,然后再次应用模式,因此匹配另一个任意字符。如果在第n组中使用其中一种(?n)构造,则会出现递归。 (?0)(?R)引用整个模式。这就是所有的魔法。
上述模式应用于输入
 '${abc.def.ghi.jkl(mno.pqr(stu.vwx))}'

将导致捕获

0 ${abc.def.ghi.jkl(mno.pqr(stu.vwx))}
1 abc.def.ghi.jkl(mno.pqr(stu.vwx))
2 abc
3 def.ghi.
4 jkl(mno.pqr(stu.vwx))

请注意,实际输出结果与您预期的有一些差异: 0 是完整匹配(在这种情况下只是输入字符串本身)。PHP 总是首先报告它,因此您无法摆脱它。 1 是第一个捕获组,它包含递归部分。您不需要在输出中使用它,但是 (?n) 不幸不能引用非捕获组,因此您也需要它。 2 是所需的类名。 3 是中间方法名称列表,加上一个尾随句点。使用 explode 可以轻松从中提取所有方法名称。

4 是最终的方法名,带有可选的(递归)参数列表。现在,如果需要,您可以采用这种模式再次应用它。请注意,对于完全递归的方法,您可能需要稍微修改模式。也就是说:单独将${}剥离出来作为第一步,以便整个模式具有与最终捕获相同的(递归)模式,然后使用(?0)而不是(?1)。然后进行匹配、删除方法名和括号,并重复此过程,直到最后一个捕获中没有更多的括号。

有关递归的更多信息,请参见PHP的PCRE文档


为了阐明我的观点,这里有一段代码片段,可以递归提取所有元素:
if(!preg_match('/^[$][{](.*)[}]$/', $expression, $matches))
    echo 'Invalid syntax.';
else
    traverseExpression($matches[1]);

function traverseExpression($expression, $level = 0) {
    $pattern = '/^(([a-z\d]+)[.]((?:[a-z\d]+[.])*)([a-z\d]+(?:[(](?1)[)])?))$/i';
    if(preg_match($pattern, $expression, $matches)) {
        $indent = str_repeat(" ", 4*$level);
        echo $indent, "Class name: ", $matches[2], "<br />";
        foreach(explode(".", $matches[3], -1) as $method)
            echo $indent, "Method name: ", $method, "<br />";
        $parts = preg_split('/[()]/', $matches[4]);
        echo $indent, "Method name: ", $parts[0], "<br />";
        if(count($parts) > 1) {
            echo $indent, "With arguments:<br />";
            traverseExpression($parts[1], $level+1);
        }
    }
    else
    {
        echo 'Invalid syntax.';
    }
}

请注意,我不建议将该模式用作一行代码,但这个答案已经足够长了。

当我尝试这个 $expression = '${myvalue.fdsfs}'; - user2463968
@user2463968 然后呢?你得到一个类名,没有中间方法和一个没有参数的最终方法。这不是你的意图吗? - Martin Ender

4

您可以使用相同的模式进行验证和提取,例如:

$subjects = array(
'${classA.methodA.methodB(classB.methodC(classB.methodD))}',
'${classA.methodA.methodB}',
'${classA.methodA.methodB()}',
'${methodB(methodC(classB.methodD))}',
'${classA.methodA.methodB(classB.methodC(classB.methodD(classC.methodE)))}',
'${classA.methodA.methodB(classB.methodC(classB.methodD(classC.methodE())))}'
);

$pattern = <<<'LOD'
~
# definitions
(?(DEFINE)(?<vn>[a-z]\w*+))

# pattern
^\$\{
    (?<classA>\g<vn>)\.
    (?<methodA>\g<vn>)\.
    (?<methodB>
        \g<vn> ( 
            \( \g<vn> \. \g<vn> (?-1)?+ \)
        )?+
    )
}$

~x
LOD;

foreach($subjects as $subject) {
    echo "\n\nsubject: $subject";
    if (preg_match($pattern, $subject, $m))
        printf("\nclassA: %s\nmethodA: %s\nmethodB: %s",
            $m['classA'], $m['methodA'], $m['methodB']);
    else
        echo "\ninvalid string";    
}

正则表达式解释:
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

在模式的结尾,你可以看到修饰符 x,它允许在模式内部包含空格、换行和注释。

首先,该模式以一个命名组 vn(变量名称)的定义开头,这里可以定义类A或方法B的外观,适用于整个模式。然后,你可以在整个模式中使用 \g<vn> 引用此定义。

注意,你可以通过添加其他定义来定义不同类型的类和方法名称。例如:

(?(DEFINE)(?<cn>....))  # for class name
(?(DEFINE)(?<mn>....))  # for method name 

模式本身:

(?<classA>\g<vn>) 在命名组 classA 中捕获由 vn 定义的模式。

methodA 同样如此。

methodB 不同,因为它可以包含嵌套的括号,这就是我在这部分使用递归模式的原因。

细节:

\g<vn>         # the method name (methodB)
(              # open a capture group
    \(         # literal opening parenthesis
    \g<vn> \. \g<vn> # for classB.methodC⑴
    (?-1)?+    # refer the last capture group (the actual capture group)
               # one or zero time (possessive) to allow the recursion stop
               # when there is no more level of parenthesis
    \)         # literal closing parenthesis
)?+            # close the capture group 
               # one or zero time (possessive)
               # to allow method without parameters

如果你想允许多个方法,可以用\g<vn>(?>\.\g<vn>)+替换它。

关于占有量词:

你可以在量词(*+?)后面添加+来使其成为占有量词,优点是正则表达式引擎知道不必回溯来测试与子模式匹配的其他方式。这样正则表达式就更有效率了。


你比我快了27秒 :D 命名捕获组加1。我也应该养成这个习惯。 - Martin Ender
@m.buettner: 我认为我们已经创造了一个历史性的话题! - Casimir et Hippolyte
你的建议也不错,但是使用这段代码时会涉及到一些复杂的情况。我的意思是,这段代码本身很容易理解,但它的灵活性较差。话虽如此,它也给了我很多信息,肯定在未来会有所帮助。 - user2463968

2

描述

该表达式只匹配并捕获${classA.methodA.methodB(classB.methodC(classB.methodD)))}${classA.methodA.methodB}格式。

(?:^|\n|\r)[$][{]([^.(}]*)[.]([^.(}]*)[.]([^(}]*(?:[(][^}]+[)])?)[}](?=\n|\r|$)

enter image description here

分组

第0组从起始的美元符号到闭合花括号获取整个匹配。

  1. 获取类名
  2. 获取第一个方法名
  3. 获取第二个方法名,以及直到闭合花括号但不包括其中的所有文本。如果此组具有空的圆括号(),则此匹配将失败

PHP代码示例:

<?php
$sourcestring="${classA1.methodA1.methodB1(classB.methodC(classB.methodD)))}
${classA2.methodA2.methodB2}
${classA3.methodA3.methodB3()}
${methodB4(methodC4(classB4.methodD)))}
${classA5.methodA5.methodB5(classB.methodC(classB.methodD)))}";
preg_match_all('/(?:^|\n|\r)[$][{]([^.(}]*)[.]([^.(}]*)[.]([^(}]*(?:[(][^}]+[)])?)[}](?=\n|\r|$)/im',$sourcestring,$matches);
echo "<pre>".print_r($matches,true);
?>

$matches Array:
(
    [0] => Array
        (
            [0] => ${classA1.methodA1.methodB1(classB.methodC(classB.methodD)))}
            [1] => 
${classA2.methodA2.methodB2}
            [2] => 
${classA5.methodA5.methodB5(classB.methodC(classB.methodD)))}
        )

    [1] => Array
        (
            [0] => classA1
            [1] => classA2
            [2] => classA5
        )

    [2] => Array
        (
            [0] => methodA1
            [1] => methodA2
            [2] => methodA5
        )

    [3] => Array
        (
            [0] => methodB1(classB.methodC(classB.methodD)))
            [1] => methodB2
            [2] => methodB5(classB.methodC(classB.methodD)))
        )

)

免责声明

  • 我在类和方法名的结尾添加了一个数字,以帮助说明组中发生了什么。
  • OP提供的示例文本没有平衡的开放和关闭圆括号。
  • 虽然()将被禁止,但(())将被允许。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接