按空格和冒号分割字符串,但不在引号内进行分割。

5
有一个类似这样的字符串:
$str = "dateto:'2015-10-07 15:05' xxxx datefrom:'2015-10-09 15:05' yyyy asdf"

期望的结果是:

[0] => Array (
    [0] => dateto:'2015-10-07 15:05'
    [1] => xxxx
    [2] => datefrom:'2015-10-09 15:05'
    [3] => yyyy
    [4] => asdf
)

我能得到什么:

preg_match_all("/\'(?:[^()]|(?R))+\'|'[^']*'|[^(),\s]+/", $str, $m);

is:

[0] => Array (
    [0] => dateto:'2015-10-07
    [1] => 15:05'
    [2] => xxxx
    [3] => datefrom:'2015-10-09
    [4] => 15:05'
    [5] => yyyy
    [6] => asdf
)

我也尝试了 preg_split("/[\s]+/", $str),但不知道如何在值被引号包围时进行转义。请问有人可以展示并解释一下这个正则表达式吗?谢谢!

3个回答

5
我会使用PCRE动词(*SKIP)(*F)
preg_split("~'[^']*'(*SKIP)(*F)|\s+~", $str);

DEMO


谢谢!您介意解释一下"~'[^']*'(*SKIP)(*F)|\s+~"吗?我只能理解部分内容,我想要完全了解它。 - caramba
'[^']*' 匹配所有单引号块和 (*SKIP)(*F) 使匹配失败。而 |\s+ 则匹配其余的所有空格。 - Avinash Raj

2

通常,在您想要拆分字符串时,使用 preg_split 并不是最好的方法(这似乎有点违反直觉,但大多数情况下都是如此)。更有效的方法是使用描述非分隔符(此处为空格)的模式,找到所有项目(使用 preg_match_all ):

$pattern = <<<'EOD'
~(?=\S)[^'"\s]*(?:'[^']*'[^'"\s]*|"[^"]*"[^'"\s]*)*~
EOD;

if (preg_match_all($pattern, $str, $m))
    $result = $m[0];

模式细节:

~                    # pattern delimiter

(?=\S)               # the lookahead assertion only succeeds if there is a non-
                     # white-space character at the current position.
                     # (This lookahead is useful for two reasons:
                     #    - it allows the regex engine to quickly find the start of
                     #      the next item without to have to test each branch of the
                     #      following alternation at each position in the strings
                     #      until one succeeds.
                     #    - it ensures that there's at least one non-white-space.
                     #      Without it, the pattern may match an empty string.
                     # )

[^'"\s]*          #"'# all that is not a quote or a white-space

(?:                  # eventual quoted parts
    '[^']*' [^'"\s]*  #"# single quotes
  |
    "[^"]*" [^'"\s]*    # double quotes
)*
~

演示

请注意,使用这个稍长的模式,你例子字符串中的五个项目仅需60步即可找到。你也可以使用这个更短/更简单的模式:

~(?:[^'"\s]+|'[^']*'|"[^"]*")+~

但它的效率稍微低一些。


谢谢您提供这么详细的答案!我还想了解一些事情: “但大多数情况下都是如此”是否有一个经验法则或一些链接可以阅读,以了解何时/为什么使用哪个?您是如何编写正则表达式的?您使用了工具还是只是知道正则表达式规则并将其写下来?如果只是写下来的话:您是如何学习正则表达式规则的? - caramba
@caramba:这更像是一个经验法则,但背后的思想相对简单:1)当分隔符必须考虑到这个环境时,模式会变得非常复杂和低效(特别是如果您需要检查前面的字符或者需要使用前瞻来检查字符串直到结尾)。2)有时候通过否定定义某些东西更容易。 - Casimir et Hippolyte
1
@caramba:关于我如何编写模式,这通常需要知识、实践和测试。例如像 (?:[^'\s]+|'[^']*')*+ 这样的模式,如果你“展开”它,就会更有效率,像这样:[^'\s]*(?:'[^']*'[^'\s]*)*+,你可以在 Friedl 的书中找到这些信息,但你也可以通过 regex101 或 regexbuddy 来查看所需步骤的数量。但即使有了知识和技巧,你仍然需要进行实验,特别是要充分了解你的敌人:字符串。 - Casimir et Hippolyte

0

对于您的示例,您可以使用preg_splitnegative lookbehind(?<!\d),即:

<?php
$str = "dateto:'2015-10-07 15:05' xxxx datefrom:'2015-10-09 15:05' yyyy asdf";
$matches = preg_split('/(?<!\d)(\s)/', $str);
print_r($matches);

输出:

    Array
    (
        [0] => dateto:'2015-10-07 15:05'
        [1] => xxxx
        [2] => datefrom:'2015-10-09 15:05'
        [3] => yyyy
        [4] => asdf
    )

演示:

http://ideone.com/EP06Nt


正则表达式解释:

(?<!\d)(\s)

Assert that it is impossible to match the regex below with the match ending at this position (negative lookbehind) «(?<!\d)»
   Match a single character that is a “digit” «\d»
Match the regex below and capture its match into backreference number 1 «(\s)»
   Match a single character that is a “whitespace character” «\s»

谢谢!好的,“负回顾”是什么意思,但是'在哪里定义?如果dateto:“has-double-quotes”,我该如何更改? - caramba

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