【问题标题】:A preg_replace puzzle: replacing zero or more of a char at the end of the subjectpreg_replace 谜题:在主题末尾替换零个或多个字符
【发布时间】:2011-03-26 01:36:34
【问题描述】:

假设 $d 是一个目录路径,我想确保它以一个斜杠 (/) 开头和结尾。它最初可能有零个、一个或多个前导和/或尾随斜杠。

我试过了:

preg_replace('%^/*|/*$', '/', $d);

它适用于前导斜杠,但令我惊讶的是,如果 $d 至少有一个斜杠,则会产生 两个 斜杠。如果主题是,例如,'foo///',那么 preg_replace() 首先匹配并用一个斜线替换三个尾随斜线,然后匹配末尾的零个斜线并用斜线替换它。 (您可以通过将第二个参数替换为 '[$0]' 来验证这一点。)我觉得这很违反直觉。

虽然有许多其他方法可以解决基本问题(我实现了一个),但这对我来说成了一个 PCRE 难题:单个 preg_replace 中的什么(标量)模式可以完成这项工作?

附加问题(编辑)

谁能解释为什么这个模式与它在字符串末尾的方式匹配,但在开头却没有类似的行为?

【问题讨论】:

    标签: php regex preg-replace pcre


    【解决方案1】:

    给定一个像/* 这样可以合法匹配零个字符的正则表达式,正则表达式引擎必须确保它不会在同一个位置匹配超过一次,否则它会陷入无限循环。因此,如果它确实消耗了零个字符,那么引擎会在尝试另一个匹配之前向前跳转一个位置。据我所知,这是正则表达式引擎主动执行任何操作的唯一情况。

    您看到的是相反的情况:正则表达式消耗一个或多个字符,然后在下一次循环中,它会尝试从中断的位置开始匹配。没关系,这个特定的正则表达式只能匹配一个字符,并且它已经尽可能多地匹配了这些字符;它仍然可以选择不匹配,所以它就是这样做的。

    那么,为什么您的正则表达式在开头不匹配两次,就像在结尾处一样?因为起始锚点 (^)。如果主题以一个或多个斜杠开头,它会消耗它们,然后尝试匹配 zero 个斜杠,但它会失败,因为它不再位于字符串的开头。如果开头没有斜线,手动颠簸也有同样的效果。

    在主题的结尾,这是一个不同的故事。如果那里没有斜线,则它不匹配,尝试碰撞并失败;故事结局。但如果它确实匹配一个或多个斜线,它会消耗它们并尝试再次匹配——并且成功,因为$ 锚仍然匹配。

    所以一般来说,如果你想防止这种双重匹配,你可以在匹配的 beginning 中添加一个条件来防止它,就像^ 锚对第一种选择:

    preg_replace('%^/*|(?<!/)/*$%', '/', $d);
    

    ...或确保正则表达式的一部分必须使用至少一个字符:

    preg_replace('%^/*|([^/])/*$%', '$1/', $d);
    

    但在这种情况下,您有一个更简单的选择,正如 John Kugelman 所展示的那样:只需捕获您想要保留的部分并丢弃其余部分。

    【讨论】:

    • +1 很好的解释。尽管对我来说,在这种情况下,正则表达式引擎的行为仍然不直观。
    • 精美展示。谢谢你,艾伦。虽然正如你所描述的那样有道理,但我怀疑下次出现这样的事情时我是否能够记住这一点 - 违反直觉的事情。但它在这里供将来参考。
    【解决方案2】:

    对您的模式的一个小改动是在字符串末尾分离出两个关键问题:

    1. 用一个斜线替换多个斜线
    2. 用一个斜线替换没有斜线

    该模式(以及匹配字符串开头的现有部分)如下所示:

    #^/*|/+$|$(?<!/)#
    

    一个稍微不那么简洁但更精确的选项是非常明确地只匹配零个或两个或多个斜杠;概念是,为什么要用一个斜线代替一个斜线?

    #^(?!/)|^/{2,}|/{2,}$|$(?<!/)#
    

    除此之外:nikic's suggestion 使用 trim(删除前导/尾随斜杠,然后添加自己的)是一个不错的选择。

    【讨论】:

    • 非常好。这个答案最直接地解决了我在第一次尝试时在字符串末尾看到的惊喜。目前我在接受这个和约翰库格曼的回答之间左右为难。我同意,你的第二个版本是精确的,约翰有时会做不必要的工作。但约翰的很简单,接近尼克的。
    【解决方案3】:

    可以在单个 preg_replace 中完成

    preg_replace('/^\/{2,}|\/{2,}$|^([^\/])|([^\/])$/', '\2/\1', $d);
    

    【讨论】:

    • 不错。如何通过去掉所有反斜杠来提高可读性:return preg_replace('!^/{2,}|/{2,}$|^([^/])|([^/])$!', '$2/$1', $d);
    • yes ofc,但我只是习惯了 perl,它只允许斜杠作为分隔符
    • 我花了一些力气才明白这一点。它类似于salathe的答案,但使用^([^/])([^/])$而不是断言添加捕获的字符。我很欣赏它的复杂性。
    【解决方案4】:
    preg_replace('%^/*(.*?)/*$%', '/\1/', $d)
    

    【讨论】:

    • 这个也不错。 $ 而不是 \ 更容易阅读 imo:preg_replace('%^/*(.*?)/*$%', '/$1/', $d);
    • 颠倒思维,即捕捉你想要保留的东西而不是你想要替换的东西,突然间就很明显了。太棒了!
    【解决方案5】:
    $path = '/' . trim($path, '/') . '/';
    

    这首先删除开头或结尾的所有斜杠,然后再次添加单个斜杠。

    【讨论】:

    • 这可能比正则表达式更快,但他明确要求使用正则表达式而不是解决问题的另一种方法。
    • 虽然我同意确切的问题要求使用正则表达式,但在这种情况下 PHP 提供了比正则表达式更好的解决方案。无论哪种方式,OP 都可以获得正则表达式答案和一个好的 PHP 特定解决方案。 OP 可以通过选择一个答案而不是另一个答案来选择有效性。
    • 这可能是潜在问题的最佳解决方案。只是我对%^/*|/*$% 模式的结果感到惊讶,这与原始问题无关。这是关于提高正则表达式技能。把它想象成数独。
    猜你喜欢
    • 2017-08-02
    • 2012-02-03
    • 1970-01-01
    • 2016-08-14
    • 1970-01-01
    • 1970-01-01
    • 2016-02-06
    • 1970-01-01
    • 2012-03-18
    相关资源
    最近更新 更多