【问题标题】:Regex matching unescaped commas in Java正则表达式匹配Java中的非转义逗号
【发布时间】:2014-03-20 11:47:26
【问题描述】:

问题描述

我正在尝试使用 String 类提供的 split() 方法将 a 拆分为单独的字符串。文档告诉我它将围绕参数的匹配进行拆分,这是一个正则表达式。我使用的分隔符是逗号,但也可以转义逗号。我使用的转义字符是正斜杠 / (只是为了不使用反斜杠使事情变得更容易,因为这需要在 Java 和正则表达式中的字符串文字中进行额外的转义)。

例如,输入可能是这样的:

a,b/,b//,c///,//,d///,

输出应该是:

a
b,b/
c/,/
d/,

因此,字符串应该在每个逗号处拆分,除非该逗号前面有奇数个斜杠 (1, 3, 5, 7, ..., ∞),因为这意味着逗号被转义。

可能的解决方案

我最初的猜测是这样拆分:

String[] strings = longString.split("(?<![^/](//)*/),");

但这是不允许的,因为 Java 不允许无限的后视组。我可以通过将 * 替换为 {0,2000} 来将重复时间限制在 2000 年:

String[] strings = longString.split("(?<![^/](//){0,2000}/),");

但这仍然对输入施加了限制。所以我决定把复现从look-behind组中去掉,然后想出了这个:

String[] strings = longString.split("(?<!/)(?:(//)*),");

但是,它的输出是以下字符串列表:

a
b,b (the final slash is lacking in the output)
c/, (the final slash is lacking in the output)
d/,

为什么在第二个和第三个字符串中省略了那些斜杠,我该如何解决(在 Java 中)?

【问题讨论】:

    标签: java regex


    【解决方案1】:

    您可以使用正视图来实现拆分,以查看逗号前 偶数 个斜线:

    String[] strings = longString.split("(?<=[^/](//){0,999999999}),");
    

    但要显示您想要的输出,您需要进一步删除剩余的转义:

    String longString = "a,b/,b//,c///,//,d///,";
    String[] strings = longString.split("(?<=[^/](//){0,999999999}),");
    for (String s : strings)
        System.out.println(s.replaceAll("/(.)", "$1"));
    

    输出:

    a
    b,b/
    c/,/
    d/,
    

    【讨论】:

    • 不幸的是,如果输入有 20 个连续的斜线,那将会中断,不是吗?我已经提到我可以通过将重复次数限制为 2000 次来解决这个问题,但这仍然会对输入产生限制,即使 2000 次通常就足够了。
    • 我已经更改了正则表达式以适应多达 10 亿个斜杠。够了吗?
    • 我看到你编辑了你的答案。 {0,999999999} 几乎是无限的,但我不确定即使 Java 编译器没有抱怨它是否能保证工作。它真的适用于 999999998 个连续斜线的序列吗? :) 我同意这种解决方法绝对是可用的,但我希望找到一个不会限制这种重复的解决方案。在 Java 中是不可能的,还是非常困难?
    • 是的,它会起作用的。任何量词的实现限制为2147483647(请参阅Integer.MAX_VALUE)。 AFAIK,此限制适用于所有正则表达式实现。
    【解决方案2】:

    你很接近。要克服后视错误,您可以使用以下解决方法:

    String[] strings = longString.split("(?<![^/](//){0,99}/),")
    

    【讨论】:

    • 与 Bohemian 的回答一样,如果输入有 200 个连续的斜杠,那将会中断。我正在寻找一种不以任何方式限制此连续斜杠数量的方法。
    • 99 已作为解决方法提供,您可以将其设为一个很大的数字以涵盖所有实际可能性。您不能有可变长度的 lokbehind 断言,并且该限制是由 Java 中的正则表达式引擎实现的。此外,我很惊讶您选择忽略原始问题中的此错误。
    【解决方案3】:

    如果您不介意使用正则表达式的其他方法,我建议使用.matcher

    Pattern pattern = Pattern.compile("(?:[^,/]+|/.)+");
    String test = "a,b/,b//,c///,//,d///,";
    Matcher matcher = pattern.matcher(test);
    while (matcher.find()) {
        System.out.println(matcher.group().replaceAll("/(.)", "$1"));
    }
    

    输出:

    a
    b,b/
    c/,/
    d/,
    

    ideone demo

    此方法将匹配除定界逗号(有点相反)之外的所有内容。优点是它不依赖环视。

    【讨论】:

    • 谢谢。这是一个优雅的解决方案。但是,我不清楚使用非捕获组。在这种情况下需要吗?
    • @Franklin 肯定需要一个群,这样群[^,/]+|/.可以通过+重复。好吧,您可以使用([^,/]+|/.),但这会将某些内容存储在变量中,因此需要更多内存。它没有多大作用,但我更喜欢尽可能避免捕获组。如果你有很多事情要做,他们往往会放慢速度。
    • 再次感谢杰瑞。这听起来很有道理。 :-) 关于该组的另一个问题:我们不能只删除[^,/] 部分的内部重复吗? (自从我玩正则表达式以来已经很长时间了......)我接受你的回答,因为它允许“无限”(当然不是实际上无限)重复转义字符的次数。你能告诉我这与 String.split() 相比是否更快/更慢?
    • @Franklin 嗯,恐怕我不确定你删除内部重复是什么意思。如果我做对了,我猜你可以通过扭转事情来缩短它:(?:/.|[^,])+
    • @Franklin 哦,对不起。我不知道如何在 C# 中为函数计时(反正还不知道 ^^;)但可能值得一提的是,lookbehinds 往往很慢。
    【解决方案4】:

    我喜欢正则表达式,但是在这里手动编写代码不是很容易,即

    boolean escaped = false;
    for(int i = 0, len = s.length() ; i < len ; i++){
        switch(s.charAt(i)){
        case "/": escaped = !escaped; break;            
        case ",": 
          if(!escaped){
             //found a segment, do something with it
          }
          //Fallthrough!
        default:
          escaped = false;
        }
    }
    // handle last segment
    

    【讨论】:

    • 我确实已经“手动”完成了此操作,但我现在正在专门寻找带有正则表达式的解决方案。
    猜你喜欢
    • 2013-01-04
    • 2021-07-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多