【问题标题】:How can I detect space that is not quoted or double quoted如何检测未引用或双引号的空格
【发布时间】:2016-03-24 10:59:26
【问题描述】:

我正在尝试创建一个 Java 正则表达式,它将用单个空格替换字符串中所有出现的空格,除非该空格出现在引号之间(单引号或双引号)

如果我只是在寻找双引号,我可以使用前瞻:

text.replaceAll("\\s+ (?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", " ");

如果我只是在寻找单引号,我可以使用类似的模式。

诀窍是找到两者。

我有一个好主意,先运行双引号模式,然后运行单引号模式,但当然,不管引号如何,最终都会替换所有空格。

所以这里有一些测试和预期结果

a   b   c    d   e   -->  a b c d e
a   b   "c    d"   e -->  a b "c    d" e
a   b   'c    d'   e -->  a b 'c    d' e
a   b   "c    d'   e -->  a b "c d' e    (Can't mix and match quotes)

有没有办法在 Java 正则表达式中实现这一点?

假设无效输入已经单独验证。所以以下情况都不会发生:

a "b c ' d
a 'b " c' d
a 'b c d

【问题讨论】:

  • 这是一个您无法提前解决的常见问题。它唯一的解决方案是您必须匹配引号部分才能越过它们。
  • " ' ' " " ' " " ' 怎么样?哪个可以更换,哪个不可以?我不认为正则表达式可以做到这一点。这似乎不是一种常规语言。
  • @tobias_k 它确实不需要那么健壮,但是在您的示例中,双引号不平衡。我假设不会有嵌套并且引号将是平衡的
  • "我假设不会有嵌套并且引号将被平衡"这不是您在上一个示例中显示的 a b "c d' e 其中"'没有配对。
  • 还有逃跑怎么办?您的输入是否可以包含一些转义引号,例如a "b \"c d" e

标签: java regex regex-negation regex-lookarounds


【解决方案1】:

编辑 - 注意 - 这个答案有错误/缺陷

它要求在结束引号("')和它后面的字符之间有一个空格,以正确匹配带引号的字符串。所以" "some-text 不会被这个答案正确处理。

它可能有更多错误 - 但仅此而已。

编辑 - 替代答案

我添加了another more well optimised answer,它没有错误。

把这个留给后代。

支持

这个支持通过\"\'转义引号和多行引号。

正则表达式

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

https://regex101.com/r/wT6tU2/1

替换

$1$2(是的,末尾有空格)

可视化

代码

try {
    String resultString = subjectString.replaceAll("([^\\s\"'\\\\]+)*(\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"|'[^'\\\\]*(?:\\\\.[^'\\\\]*)*')*(\\s+)", "$1$2 ");
} catch (PatternSyntaxException ex) {
    // Syntax error in the regular expression
} catch (IllegalArgumentException ex) {
    // Syntax error in the replacement text (unescaped $ signs?)
} catch (IndexOutOfBoundsException ex) {
    // Non-existent backreference used the replacement text
}

人类可读

// ([^\s"'\\]+)*("[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*')*(\s+)
// 
// Options: Case sensitive; Exact spacing; Dot doesn’t match line breaks; ^$ don’t match at line breaks; Default line breaks; Regex syntax only
// 
// Match the regex below and capture its match into backreference number 1 «([^\s"'\\]+)*»
//    Between zero and unlimited times, as many times as possible, giving back as needed (greedy) «*»
//       You repeated the capturing group itself.  The group will capture only the last iteration.  Put a capturing group around the repeated group to capture all iterations. «*»
//       Or, if you don’t want to capture anything, replace the capturing group with a non-capturing group to make your regex more efficient.
//    Match any single character NOT present in the list below «[^\s"'\\]+»
//       Between one and unlimited times, as many times as possible, giving back as needed (greedy) «+»
//       A “whitespace character” (ASCII space, tab, line feed, carriage return, vertical tab, form feed) «\s»
//       A single character from the list “"'” «"'»
//       The backslash character «\\»
// Match the regex below and capture its match into backreference number 2 «("[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*')*»
//    Between zero and unlimited times, as many times as possible, giving back as needed (greedy) «*»
//       You repeated the capturing group itself.  The group will capture only the last iteration.  Put a capturing group around the repeated group to capture all iterations. «*»
//       Or, if you don’t want to capture anything, replace the capturing group with a non-capturing group to make your regex more efficient.
//    Match this alternative (attempting the next alternative only if this one fails) «"[^"\\]*(?:\\.[^"\\]*)*"»
//       Match the character “"” literally «"»
//       Match any single character NOT present in the list below «[^"\\]*»
//          Between zero and unlimited times, as many times as possible, giving back as needed (greedy) «*»
//          The literal character “"” «"»
//          The backslash character «\\»
//       Match the regular expression below «(?:\\.[^"\\]*)*»
//          Between zero and unlimited times, as many times as possible, giving back as needed (greedy) «*»
//          Match the backslash character «\\»
//          Match any single character that is NOT a line break character (line feed, carriage return, next line, line separator, paragraph separator) «.»
//          Match any single character NOT present in the list below «[^"\\]*»
//             Between zero and unlimited times, as many times as possible, giving back as needed (greedy) «*»
//             The literal character “"” «"»
//             The backslash character «\\»
//       Match the character “"” literally «"»
//    Or match this alternative (the entire group fails if this one fails to match) «'[^'\\]*(?:\\.[^'\\]*)*'»
//       Match the character “'” literally «'»
//       Match any single character NOT present in the list below «[^'\\]*»
//          Between zero and unlimited times, as many times as possible, giving back as needed (greedy) «*»
//          The literal character “'” «'»
//          The backslash character «\\»
//       Match the regular expression below «(?:\\.[^'\\]*)*»
//          Between zero and unlimited times, as many times as possible, giving back as needed (greedy) «*»
//          Match the backslash character «\\»
//          Match any single character that is NOT a line break character (line feed, carriage return, next line, line separator, paragraph separator) «.»
//          Match any single character NOT present in the list below «[^'\\]*»
//             Between zero and unlimited times, as many times as possible, giving back as needed (greedy) «*»
//             The literal character “'” «'»
//             The backslash character «\\»
//       Match the character “'” literally «'»
// Match the regex below and capture its match into backreference number 3 «(\s+)»
//    Match a single character that is a “whitespace character” (ASCII space, tab, line feed, carriage return, vertical tab, form feed) «\s+»
//       Between one and unlimited times, as many times as possible, giving back as needed (greedy) «+»

【讨论】:

  • 仅供参考,这个([^\s"'\\]+)* 毫无用处。此外,这个正则表达式只会匹配空格旁边的引号,这意味着它将匹配此处突出显示的 -> '<spaces>'asdf.不错的展开循环。
  • 不错的地方@sln 我可以确认这确实需要在结尾引号后留一个空格。所以" "some-text 不匹配。 @sli ([^\s"'\\]+)* 元素实际上是一个性能元素,在我对 40MB 文件的快速测试套件中,导致处理时间减半 - 可能是侥幸 - 在前几次迭代之后我并没有那么关注正则表达式。
  • 您好,院长。好吧,由于除了最后的空格之外所有内容都是可选的,因此与这些选项不匹配的任何内容都不会匹配。这意味着它会一直跳到带引号的字符串中。然后,与" "\S 匹配的引号之间的所有空格都将成为目标。这一切都假设引号是正确平衡的,正则表达式中没有规定。
  • 我添加了一个2nd separate answer 作为替代方案,还进行了编辑以使这个答案的主要错误显而易见。
【解决方案2】:

我建议标准化您的字符串封装。 使用正则表达式替换标准的替代项。 假设您选择双引号“ 然后你可以在 " 和所有奇怪的元素上分割你的字符串 是引用的内容,您的偶数元素将不被引用, 仅在偶数元素上运行您的正则表达式替换并重建您的字符串 来自改变的数组。

【讨论】:

    【解决方案3】:

    支持

    • 通过\"\' 和多行引号转义引号。
    • 不匹配的引号,其中引号以字符串结尾终止。
    • 针对大文件的其他优化

    优化

    多项优化以减少步骤数:

    示例 1 - 用于字符串 Word1 Word2(单词之间有两个空格)

    示例 2 - 用于字符串 'example' another_word(单词之间有两个空格)

    示例 3 - 用于 WordPress 的 /wp-includes/media.php 文件

    正则表达式

    \G((?:[^\s"']+| (?!\s)|"[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*')*+)(\s+)
    

    https://regex101.com/r/wT6tU2/4

    替换

    $1(是的,最后有一个空格)

    可视化

    代码

    try {
        String resultString = subjectString.replaceAll("\\G((?:[^\\s\"']+| (?!\\s)|\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"|'[^'\\\\]*(?:\\\\.[^'\\\\]*)*')*+)(\\s+)", "$1 ");
    } catch (PatternSyntaxException ex) {
        // Syntax error in the regular expression
    } catch (IllegalArgumentException ex) {
        // Syntax error in the replacement text (unescaped $ signs?)
    } catch (IndexOutOfBoundsException ex) {
        // Non-existent backreference used the replacement text
    }
    

    人类可读

    // \G((?:[^\s"']+| (?!\s)|"[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*')*+)(\s+)
    // 
    // Options: Case sensitive; Exact spacing; Dot doesn’t match line breaks; ^$ don’t match at line breaks; Default line breaks; Regex syntax only
    // 
    // Assert position at the end of the previous match (the start of the string for the first attempt) «\G»
    // Match the regex below and capture its match into backreference number 1 «((?:[^\s"']+| (?!\s)|"[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*')*+)»
    //    Match the regular expression below «(?:[^\s"']+| (?!\s)|"[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*')*+»
    //       Between zero and unlimited times, as many times as possible, without giving back (possessive) «*+»
    //       Match this alternative (attempting the next alternative only if this one fails) «[^\s"']+»
    //          Match any single character NOT present in the list below «[^\s"']+»
    //             Between one and unlimited times, as many times as possible, giving back as needed (greedy) «+»
    //             A “whitespace character” (ASCII space, tab, line feed, carriage return, vertical tab, form feed) «\s»
    //             A single character from the list “"'” «"'»
    //       Or match this alternative (attempting the next alternative only if this one fails) « (?!\s)»
    //          Match the character “ ” literally « »
    //          Assert that it is impossible to match the regex below starting at this position (negative lookahead) «(?!\s)»
    //             Match a single character that is a “whitespace character” (ASCII space, tab, line feed, carriage return, vertical tab, form feed) «\s»
    //       Or match this alternative (attempting the next alternative only if this one fails) «"[^"\\]*(?:\\.[^"\\]*)*"»
    //          Match the character “"” literally «"»
    //          Match any single character NOT present in the list below «[^"\\]*»
    //             Between zero and unlimited times, as many times as possible, giving back as needed (greedy) «*»
    //             The literal character “"” «"»
    //             The backslash character «\\»
    //          Match the regular expression below «(?:\\.[^"\\]*)*»
    //             Between zero and unlimited times, as many times as possible, giving back as needed (greedy) «*»
    //             Match the backslash character «\\»
    //             Match any single character that is NOT a line break character (line feed, carriage return, next line, line separator, paragraph separator) «.»
    //             Match any single character NOT present in the list below «[^"\\]*»
    //                Between zero and unlimited times, as many times as possible, giving back as needed (greedy) «*»
    //                The literal character “"” «"»
    //                The backslash character «\\»
    //          Match the character “"” literally «"»
    //       Or match this alternative (the entire group fails if this one fails to match) «'[^'\\]*(?:\\.[^'\\]*)*'»
    //          Match the character “'” literally «'»
    //          Match any single character NOT present in the list below «[^'\\]*»
    //             Between zero and unlimited times, as many times as possible, giving back as needed (greedy) «*»
    //             The literal character “'” «'»
    //             The backslash character «\\»
    //          Match the regular expression below «(?:\\.[^'\\]*)*»
    //             Between zero and unlimited times, as many times as possible, giving back as needed (greedy) «*»
    //             Match the backslash character «\\»
    //             Match any single character that is NOT a line break character (line feed, carriage return, next line, line separator, paragraph separator) «.»
    //             Match any single character NOT present in the list below «[^'\\]*»
    //                Between zero and unlimited times, as many times as possible, giving back as needed (greedy) «*»
    //                The literal character “'” «'»
    //                The backslash character «\\»
    //          Match the character “'” literally «'»
    // Match the regex below and capture its match into backreference number 2 «(\s+)»
    //    Match a single character that is a “whitespace character” (ASCII space, tab, line feed, carriage return, vertical tab, form feed) «\s+»
    //       Between one and unlimited times, as many times as possible, giving back as needed (greedy) «+»
    

    【讨论】:

    • Kudo 修复了它,Dean。我确实添加了一个原子组,它解决了我的问题并发布了一些新的测试链接。另外,我故意不使用展开循环方法来进行对比。您添加的空间优化并不能说明全部情况,他们列出的步骤并不能转化为定量的时间优势。我使用这两个版本来调整 WP 文件。该文件被复制到 12 MB。两者进行全局查找/替换大约需要 7 秒。
    【解决方案4】:

    编辑:由于@DeanTaylor 修复了他的正则表达式,我将修复(修改)这个,
    以防有人决定在不平衡的报价上使用它。

    平衡引号的原始测试有一个原子组。
    我从未将它添加到解析逻辑中。所以,这是添加的。就是这样。


    您可以交替匹配引号或空格,并且
    确定匹配的组以决定替换什么。

    OR 使用这个正则表达式来获得两者,避免决策。

    查找:\G((?>"(?:\\[\S\s]|[^"\\])*"|'(?:\\[\S\s]|[^'\\])*'|[^"'\s]+)*)\s+

    "\\G((?>\"(?:\\\\[\\S\\s]|[^\"\\\\])*\"|'(?:\\\\[\\S\\s]|[^'\\\\])*'|[^\"'\\s]+)*)\\s+"

    替换:$1<space>

    Formatted and tested:

     \G                            # Must match where last match left off
                                   # (This will stop the match if there is a quote unbalance)
     (                             # (1 start), quotes or non-whitespace 
          (?>                           # Atomic cluster to stop backtracking if quote unbalance
               "
               (?: \\ [\S\s] | [^"\\] )*     # Double quoted text
               "
            |                              # or,
               '
               (?: \\ [\S\s] | [^'\\] )*     # Single quoted text
               ' 
            |                              # or,
               [^"'\s]+                      # Not quotes nor whitespace
          )*                            # End Atomic cluster, do 0 to many times
     )                             # (1 end)
     \s+                           # The whitespaces outside of quotes
    

    注意 - 在使用上述正则表达式之前,您可以测试字符串是否平衡引号。
    这将测试字符串,如果它通过,它有平衡的引号。

    ^(?>(?:"(?:\\[\S\s]|[^"\\])*"|'(?:\\[\S\s]|[^'\\])*')|[^"']+)+$

    "^(?>(?:\"(?:\\\\[\\S\\s]|[^\"\\\\])*\"|'(?:\\\\[\\S\\s]|[^'\\\\])*')|[^\"']+)+$"


    更新@DeanTaylor 新答案测试。

    示例 1 - 对于字符串 Word1 Word2(单词之间有两个空格)

    • 此版本大约需要 27 个步骤
    • @DeanTaylor 的版本大约需要 29 步

    示例 2 - 用于字符串 'example' another_word(单词之间有两个空格)

    • 此版本大约需要 51 个步骤
    • @DeanTaylor 的版本需要大约 36 个步骤(可能是因为未滚动的循环)

    示例 3 - 用于 WordPress 的文件

    • 此版本需要约 315,647 步
    • @DeanTaylor 的版本需要 122,701 步(Dean 的版本不处理单个空格)

    Niether 示例 3 测试将在 regex101.com 上生成一个永久链接。
    页面变得无响应,显示它到底是个垃圾。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多