按分隔符拆分字符串,但如果有转义字符则不拆分

52

如何按分隔符分割字符串,但如果有转义字符则不分割?例如,我有一个字符串:

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

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

因此,对于上述字符串,结果应该是:

[0] => 1
[1] => 2\|2
[2] => 3\\
[3] => 4\\\|4
5个回答

107

使用黑魔法:

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

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


6
(*SKIP)(*FAIL) 有文档说明吗? - eyelidlessness
17
你可以查看PCRE文档(http://www.pcre.org/pcre.txt),搜索`(*SKIP)`。你会在那里找到所有回溯控制动词的文档,比如*SKIP、*FAIL、*ACCEPT、*PRUNE等。 - NikiC
5
+1 @NikiC,既为我提供了PCRE文档的链接,又让我想要阅读它。 - Peter
2
@AmalMurali 在 PHP 解析字符串后,a\\\a 将会变成 两个 反斜杠 ;) 对于 PHP 来说,a\\\aa\\\\a 是相同的 :) - NikiC
@MCEmperor 你可以研究一下\K转义序列。请参见demo - HamZa
显示剩余4条评论

11

我认为比起使用split(...)函数,更直观的方式是使用一种类似于词法解析器的“扫描”函数。在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
)

1
当我尝试匹配而不是拆分时,总会出现一些奇怪的边缘情况。例如,使用+可以删除空元素:a||c。使用*可能会获得您实际上不想要的空元素(虽然在这里不是)。不知何故,它从来没有完全达到相同的效果... - Kobi
抱歉,我的错,我以为你在[^\\|]中转义了管道符号。我有点失误。 - Kobi
你提出了一个很好的观点,Kobi。不,我不会将+改为*(我猜太多空字符串会被匹配)。如果在管道符之间出现空字符串的边界情况,我会像这样处理它:...|(?<=^|\|)(?=$|\|),其中...是现有的正则表达式。 - Bart Kiers
+1 这可能是解决这个特定问题更直观的方法。但我发现黑魔法方法还有许多其他不错的用例(特别是在快速和肮脏的 regexing 中)。例如,如果你想扫描一些文件但不想在字符串和注释中匹配,你可以简单地跳过那些。(当然还有空元素的问题,但 Kobi 已经说过了。) - NikiC

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'。这是一个有趣的implode问题。


3

最近我想出了一个解决方案:

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

但黑魔法解决方案仍然快三倍。

你介意分享一下你是如何衡量正则表达式性能与黑魔法解决方案的吗? - Peter
警告:preg_split():编译失败:回顾断言不是固定长度 - undefined

-1

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

$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 ) 

你进行过基准测试吗?另外,“...你希望不会出现在原始字符串中的标记”。 - Nev Stokes

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