【问题标题】:Extra characters on the end of replaced text替换文本末尾的额外字符
【发布时间】:2013-03-20 08:20:19
【问题描述】:

在 PHP 和 Java 中,我将 /^[^\pL]*|[^\pL]*$/ 应用到 ‍‍‍-A- 并得到 *A**。我应用了对称图案并得到了不对称的结果!为什么?我想知道为什么它的输出不是*A*?

模式说字符串末尾除了字母之外的所有东西都应该替换为*,它也是贪婪的,应该将所有非字母的东西一起替换。

RegexBuddy 中的 Alos note 我得到了 *A* 这正是我所期望的。

更新:我简化了问题以集中我的主要关注点。

【问题讨论】:

    标签: java php regex pcre


    【解决方案1】:
    #^[^\pL]+|[^\pL]+$#u
    

    将* 替换为+。将* 与$ 结合使用并不完全符合人们的预期。正则表达式引擎如何工作的一个奇怪结果是,X*$ 将找到 两个 匹配 X*。使用+ 修复它。

    说明

    [^\pL]*$
    

    让我们看一下正则表达式的这一部分,它没有按预期工作。为什么它在某些字符串的末尾放了两个*?

    1. 考虑替换第一组破折号后的第三个示例字符串---A---:

      *A---$
      
    2. 正则表达式引擎在这里找到匹配的正则表达式:

      *A---$
        ^
      
    3. 并用星号替换"---":

      *A*$
        ^
      
    4. 然后它将其内部光标移动到替换字符串的右侧。

      *A*$
         ^
      
    5. 它从这个光标位置开始并寻找另一个匹配。它找到了一个!它找到""——空字符串! "" 由 0 个或多个非字母 ([^\pL]*) 组成,它锚定在字符串的末尾 ($),因此它是有效匹配。它确实找到了空字符串,但这是允许的。

      这是出乎意料的,因为它再次与 $ 锚点匹配。那不是错了吗?它不应该再次匹配$,不是吗?好吧,实际上,它应该而且确实如此。它可以再次匹配$,因为$ 不是输入字符串中的实际字符——它是一个零宽度断言。它不会被第一次替换“用完”。 $ 允许匹配两次。

    6. 因此,它用星号“替换”了空字符串""。这就是为什么你最终会得到两个星号。

      *A**$
         ^
      
    7. 如果正则表达式引擎返回到步骤 4,它将找到另一个空字符串并添加另一个星号。从概念上讲,那里有无数个空字符串。为了避免这种情况,引擎不允许下一场比赛在与前一场比赛相同的位置开始。此规则可防止它进入无限循环。

    【讨论】:

    • @PHPst 我没有关注。我忽略了什么?
    • @PHPst 你能告诉我你不遵循(或不同意)我的解释的哪一部分吗?
    • @PHPst 他什么都没忽略。您的字符串与[^\pL]*$ 匹配两次。一旦被替换为'---'。紧随其后的是一个''。与您的“A”变为“*A”的原因相同。
    • @JohnKugelman 你忽略了 RegEx 的贪婪。替换操作应在第三个破折号 ---A---| 之后发生。A 后面的字符应整体替换为“*”。我不明白您的回答如何显示意外替换。
    • 补充一点:第三次不匹配的原因,是因为之前的匹配是空的。在这种情况下,引擎不允许下一场比赛从同一位置开始,因为接下来的所有比赛也会从那里开始。它会陷入无限循环。
    【解决方案2】:

    正确的正则表达式应该是这样的:

    $arr = preg_replace('#^[^\pL]+|[^\pL]+$#','*', 
               array('A','-A-','---A---','-+*A*+-','------------A------------'));
    

    注意+ 而不是*。这将给出输出:

    Array
    (
        [0] => A
        [1] => *A*
        [2] => *A*
        [3] => *A*
        [4] => *A*
    )
    

    PS:请注意,第一个元素将保持不变,因为在 A 之前和之后没有 non-alpha 字符。

    【讨论】:

    • @didierc:请参阅我关于第一个元素的回答中的注释。
    • 谢谢,但我的问题是关于上述正则表达式的行为,而不是另一个!
    【解决方案3】:

    试一试:
    在代码之后和代码正文中都给出了解释——作为 cmets。

    <?php
    class String
    {
        private $str;
        public function __construct($str)
        {
            $this->str=$str;
        }
        public function replace($regex,$replacement)
        {
            return preg_replace($regex,$replacement,$this->str);
        }
    }
    
    function String($str)
    {
        return new String($str);
    }
    
    echo String('A')->replace('/^[^\pL]*|[^\pL]*$/','*').'<br />';//Outputs *A*
     //Why does this output *A* and not A?
     //Because it successfully matches an empty string
     //The easiest way to test for the presence of an empty string is like so:
    echo String('A')->replace('//','*').'<br />';//Outputs *A*
     //The engine begins by placing its internal pointer before the string like so:
     // A
     //^
     //It then tests the regular expression for the empty string ""
     //Most regular expressions will fail this test. But in our case matches it successfully.
     //Since we are preforming a search and replace the "" will get replaced by a "*" character
     //Then the internal pointer advances to the next character after its successful match
     // A
     // ^
     //It tests our regular expression for the A character and it fails.
     //Since we are performing a search and replace the searched "A" portion remains unchanged as "A"
     //The internal pointer advances to the next character
     // A
     //  ^
     //It tests our regular expression for the empty string ""
     //Again, most regular expressions will fail this test. But since ours successfully matched it,
     //The "" portion will get replaced by "*"
     //The engine then returns our output:
     //*A*
    echo '<hr />';
     //If we wanted to replace the A character too, we'd do this:
    echo String('A')->replace('/|A/','*').'<br />';//Outputs ***
     //Or we could do:
    echo String('A')->replace('/.*?/','*').'<br />';//Outputs ***
     //Thus we see for a 1 character string the engine will test for the empty spaces "" before and after the character as well
     //For a 19 character string it tests for all the gaps between each character like so:
    echo String('19 character string')->replace('//','*').'<br />';//Outputs *1*9* *c*h*a*r*a*c*t*e*r* *s*t*r*i*n*g*
     //For an empty string it would match once successfully like so:
    echo String('')->replace('//','*').'<br />';//Outputs *
    

    echo String('A')-&gt;replace('/^[^\pL]*|[^\pL]*$/','*');//Outputs *A*

    为什么上面的输出是*A* 而不是A?
    因为这个正则表达式将成功匹配空字符串""。
    使用空的正则表达式观察到相同的行为,如下所示:
    echo String('A')-&gt;replace('//','*');//Outputs *A*

    我现在将解释 为什么正则表达式engine实现会产生这些奇怪的结果。之后你会明白他们一点都不奇怪,但实际上是正确的行为。

    引擎首先将其 内部指针 放在字符串之前,如下所示:

      A
    _ _ _
    ^
    

    由于指针指向空字符串"",然后它会根据我们的正则表达式对其进行测试。
    大多数正则表达式将无法通过此测试,因为满足正则表达式所需的最少字符数通常是一个或多个。但在我们的例子中,匹配是成功的,因为 0 个字符是对我们正则表达式的有效匹配。
    由于我们正在执行搜索和替换,"" 将被 "*" 字符替换。
    然后内部指针在匹配成功后前进到下一个字符:

      A
    _ _ _
      ^
    

    它测试"A" 字符的正则表达式,它失败。
    由于我们正在执行搜索和替换,搜索到的"A" 部分保持不变为"A"
    内部指针前进到下一个字符:

      A
    _ _ _
        ^
    

    它测试我们的正则表达式是否为空字符串""
    同样,大多数正则表达式都无法通过此测试。
    但由于我们的正则表达式 成功 匹配它,"" 部分将被 "*" 替换
    引擎然后完成遍历我们的字符串"A"并返回我们的输出:"*A*"

    如果我们也想替换 A 字符,我们会这样做:
    echo String('A')-&gt;replace('/|A/','*');//Outputs ***

    或者我们可以这样做:
    echo String('A')-&gt;replace('/.*?/','*').'&lt;br /&gt;';//Outputs ***

    因此,我们看到对于 1 个字符的字符串,引擎也会在字符之前和之后测试 ""。

    对于 19 个字符的字符串,它会测试每个字符之间的所有间隙,如下所示:
    echo String('19 character string')-&gt;replace('//','*');
    //Outputs *1*9* *c*h*a*r*a*c*t*e*r* *s*t*r*i*n*g*

    对于空字符串,它会成功匹配一次,如下所示:
    echo String('')-&gt;replace('//','*');//Outputs *

    我的解释到此结束。要修复您的正则表达式,请按照之前的建议进行操作并使用:
    /^[^\pL]+|[^\pL]+$/
    这将使满足正则表达式所需的最少字符数,从而解决不需要的行为。

    最后,如果有人想知道\pL 在正则表达式中的作用,它的基本意思是:匹配任何类似字母的字符(而不是数字或符号)。这里解释: http://www.php.net/manual/en/regexp.reference.unicode.php

    【讨论】:

      【解决方案4】:
      /^[^\pL]*|[^\pL]*$/  
      ['A','-A-','---A---','-+*A*+-','------------A------------']
      

      也许我误解了问题或正则表达式,但它看起来与两个选项之一匹配

      选项 1:它匹配 /^ 换行符或字符串的开头。然后它匹配一个不是字母的字符零次或多次

      所以理论上-A、=A、-、=-+_+_==-=~````~!@#$A,甚至=-+_+_==-=~~!@# 都会匹配这个。

      选项2:匹配非字母零次或多次,然后匹配字符串或行的结尾

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-12-20
        • 1970-01-01
        • 2014-11-22
        • 2018-07-07
        • 1970-01-01
        • 1970-01-01
        • 2012-02-03
        • 1970-01-01
        相关资源
        最近更新 更多