【问题标题】:Split string by delimiter, but not if it is escaped通过分隔符拆分字符串,但如果它被转义则不会
【发布时间】:2011-09-08 18:47:56
【问题描述】:

如何通过分隔符拆分字符串,但如果转义则不行?比如我有一个字符串:

1|2\|2|3\\|4\\\|4

分隔符为|,转义分隔符为\|。此外,我想忽略转义的反斜杠,因此在 \\| 中,| 仍然是分隔符。

所以使用上面的字符串,结果应该是:

[0] => 1
[1] => 2\|2
[2] => 3\\
[3] => 4\\\|4

【问题讨论】:

    标签: php regex preg-split


    【解决方案1】:

    使用黑魔法:

    $array = preg_split('~\\\\.(*SKIP)(*FAIL)|\|~s', $string);
    

    \\\\. 匹配后跟一个字符的反斜杠,(*SKIP)(*FAIL) 跳过它,\| 匹配您的分隔符。

    【讨论】:

    • (*SKIP)(*FAIL)的文档吗?
    • @eyelidlessness:你可以看看PCRE documentation。搜索(*SKIP)。您可以在此处找到所有回溯控制动词(如 *SKIP、*FAIL、*ACCEPT、*PRUNE 等)的文档。
    • +1 @NikiC 提供了 PCRE 文档的链接并让我想阅读它。
    • 我试图在不回溯控制动词的情况下做到这一点。但是,由于不可能使用具有非固定长度的否定后向断言,因此该断言变得非常困难。 (好吧,除非你在下面使用 Anton 的解决方案。)猜猜你最好做点魔法,就像 NikiC 所做的那样。 ;-)
    • @AmalMurali a\\\a 在 PHP 解析字符串后将是 两个 反斜杠 ;) a\\\aa\\\\a 到 PHP 相同 :)
    【解决方案2】:

    最近我设计了一个解决方案:

    $array = preg_split('~ ((?<!\\\\)|(?<=[^\\\\](\\\\\\\\)+)) \| ~x', $string);
    

    但是黑魔法解决方案还是快了三倍。

    【讨论】:

    • 您介意分享一下您是如何衡量正则表达式与黑魔法解决方案的性能的吗?
    【解决方案3】:

    split(...) 不同,IMO 更直观的是使用某种“扫描”功能,其操作类似于词法标记器。在 PHP 中,这将是 preg_match_all 函数。你只是说你想匹配:

    1. 不是\|
    2. \ 后跟 \|
    3. 至少重复 #1 或 #2 一次

    以下演示:

    $input = "1|2\\|2|3\\\\|4\\\\\\|4";
    echo $input . "\n\n";
    preg_match_all('/(?:\\\\.|[^\\\\|])+/', $input, $parts);
    print_r($parts[0]);
    

    将打印:

    1|2\|2|3\\|4\\\|4
    
    Array
    (
        [0] => 1
        [1] => 2\|2
        [2] => 3\\
        [3] => 4\\\|4
    )
    

    【讨论】:

    • 在尝试匹配而不是拆分时,我总是遇到奇怪的边缘情况。例如,使用+,您可以删除空元素:a||c。使用*,您可能会得到您并不真正想要的空元素(尽管我认为不是在这里)。不知何故,它永远不会完全一样......
    • 对不起,我的错,我以为你在 [^\\|] 中逃脱了管道。我有点走神了。
    • 你提出了一个很好的观点 Kobi。不,我不会将 + 更改为 * (我猜会匹配许多空字符串)。如果管道之间可能出现空字符串的极端情况,我会像这样处理它...|(?&lt;=^|\|)(?=$|\|),其中... 是现有的正则表达式。
    • +1 这可能是解决这个特定问题的更直观的方法。但我发现黑魔法方法还有许多其他不错的用例(尤其是在快速和肮脏的正则表达式中)。例如,如果您想扫描一些文件但不想在字符串和 cmets 中匹配,您可以简单地跳过这些文件。 (显然空元素存在问题,但 Kobi 已经说过了。)
    【解决方案4】:

    对于未来的读者,这是一个通用的解决方案。它基于 NikiC 的想法与(*SKIP)(*FAIL):

    function split_escaped($delimiter, $escaper, $text)
    {
        $d = preg_quote($delimiter, "~");
        $e = preg_quote($escaper, "~");
        $tokens = preg_split(
            '~' . $e . '(' . $e . '|' . $d . ')(*SKIP)(*FAIL)|' . $d . '~',
            $text
        );
        $escaperReplacement = str_replace(['\\', '$'], ['\\\\', '\\$'], $escaper);
        $delimiterReplacement = str_replace(['\\', '$'], ['\\\\', '\\$'], $delimiter);
        return preg_replace(
            ['~' . $e . $e . '~', '~' . $e . $d . '~'],
            [$escaperReplacement, $delimiterReplacement],
            $tokens
        );
    }
    

    试一试:

    // the base situation:
    $text = "asdf\\,fds\\,ddf,\\\\,f\\,,dd";
    $delimiter = ",";
    $escaper = "\\";
    print_r(split_escaped($delimiter, $escaper, $text));
    
    // other signs:
    $text = "dk!%fj%slak!%df!!jlskj%%dfl%isr%!%%jlf";
    $delimiter = "%";
    $escaper = "!";
    print_r(split_escaped($delimiter, $escaper, $text));
    
    // delimiter with multiple characters:
    $text = "aksd()jflaksd())jflkas(('()j()fkl'()()as()d('')jf";
    $delimiter = "()";
    $escaper = "'";
    print_r(split_escaped($delimiter, $escaper, $text));
    
    // escaper is same as delimiter:
    $text = "asfl''asjf'lkas'''jfkl''d'jsl";
    $delimiter = "'";
    $escaper = "'";
    print_r(split_escaped($delimiter, $escaper, $text));
    

    输出:

    Array
    (
        [0] => asdf,fds,ddf
        [1] => \
        [2] => f,
        [3] => dd
    )
    Array
    (
        [0] => dk%fj
        [1] => slak%df!jlskj
        [2] => 
        [3] => dfl
        [4] => isr
        [5] => %
        [6] => jlf
        )
    Array
    (
        [0] => aksd
        [1] => jflaksd
        [2] => )jfl'kas((()j
        [3] => fkl()
        [4] => as
        [5] => d(')jf
    )
    Array
    (
        [0] => asfl'asjf
        [1] => lkas'
        [2] => jfkl'd
        [3] => jsl
    )
    

    注意:存在理论层面的问题:implode('::', ['a:', ':b'])implode('::', ['a', '', 'b']) 结果相同的字符串:'a::::b'。内爆也是一个有趣的问题。

    【讨论】:

      【解决方案5】:

      正则表达式非常缓慢。更好的方法是在拆分之前从字符串中删除转义字符,然后将它们放回:

      $foo = 'a,b|,c,d||,e';
      
      function splitEscaped($str, $delimiter,$escapeChar = '\\') {
          //Just some temporary strings to use as markers that will not appear in the original string
          $double = "\0\0\0_doub";
          $escaped = "\0\0\0_esc";
          $str = str_replace($escapeChar . $escapeChar, $double, $str);
          $str = str_replace($escapeChar . $delimiter, $escaped, $str);
      
          $split = explode($delimiter, $str);
          foreach ($split as &$val) $val = str_replace([$double, $escaped], [$escapeChar, $delimiter], $val);
          return $split;
      }
      
      print_r(splitEscaped($foo, ',', '|'));
      

      在 ',' 上分割,但如果用“|”转义则不会。它还支持双重转义,所以“||”变成一个“|”分裂发生后:

      Array ( [0] => a [1] => b,c [2] => d| [3] => e ) 
      

      【讨论】:

      • 您进行了基准测试吗?还有“...你希望不会出现在原始字符串中的标记”
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-03-02
      • 2013-03-03
      • 2013-10-30
      • 1970-01-01
      • 2018-09-21
      相关资源
      最近更新 更多