【问题标题】:Strange Java Unicode Regular Expression StringIndexOutOfBoundsException奇怪的 Java Unicode 正则表达式 StringIndexOutOfBoundsException
【发布时间】:2013-04-07 04:49:35
【问题描述】:

我的问题很简单但令人费解。可能是有一个简单的开关可以解决这个问题,但我对 Java 正则表达式的经验并不多......

String line = "????????????";
line.replaceAll("(?i)(.)\\1{2,}", "$1");

这会崩溃。如果我删除(?i) 开关,它就可以工作。这三个 unicode 字符不是随机的,它们是在一个大韩文文本中找到的,但我不知道它们是否有效。

奇怪的是,正则表达式适用于除此之外的所有其他文本。 为什么我会收到错误消息?

这是我得到的例外

Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 6
    at java.lang.String.charAt(String.java:658)
    at java.lang.Character.codePointAt(Character.java:4668)
    at java.util.regex.Pattern$CIBackRef.match(Pattern.java:4846)
    at java.util.regex.Pattern$Curly.match(Pattern.java:4125)
    at java.util.regex.Pattern$GroupTail.match(Pattern.java:4615)
    at java.util.regex.Pattern$CharProperty.match(Pattern.java:3694)
    at java.util.regex.Pattern$GroupHead.match(Pattern.java:4556)
    at java.util.regex.Pattern$Start.match(Pattern.java:3408)
    at java.util.regex.Matcher.search(Matcher.java:1199)
    at java.util.regex.Matcher.find(Matcher.java:592)
    at java.util.regex.Matcher.replaceAll(Matcher.java:902)
    at java.lang.String.replaceAll(String.java:2162)
    at tokenizer.Test.main(Test.java:51)

【问题讨论】:

  • 它是如何崩溃的?有没有例外?
  • 请定义崩溃或“错误”是什么意思?您没有向我们显示任何错误。
  • 恭喜,您在 Java 的正则表达式实现中发现了一个错误。报告它;)
  • @binit 好吧,至少对我来说这并不明显。

标签: java regex unicode


【解决方案1】:

其实只是个bug。

这就是堆栈跟踪和开源的用途。

CIBackRef(用于不区分大小写的反向引用)与组进行比较时,它不会正确地碰撞循环索引。这显示了修复:

        // Check each new char to make sure it matches what the group
        // referenced matched last time around
        int x = i;
        for (int index=0; index<groupSize; ) {
            int c1 = Character.codePointAt(seq, x);
            int c2 = Character.codePointAt(seq, j);
            if (c1 != c2) {
                if (doUnicodeCase) {
                    int cc1 = Character.toUpperCase(c1);
                    int cc2 = Character.toUpperCase(c2);
                    if (cc1 != cc2 &&
                        Character.toLowerCase(cc1) !=
                        Character.toLowerCase(cc2))
                        return false;
                } else {
                    if (ASCII.toLower(c1) != ASCII.toLower(c2))
                        return false;
                }
            }
            int n = Character.charCount(c1);
            x += n;
            index += n;  // was index++
            j += Character.charCount(c2);
        }

groupSize 是组的总字符数。 j 是引用组的索引。

测试

  //9ff0 9592 9ff0 9592 9ff0 9592
  val line = "\ud83d\udc95\ud83d\udc95\ud83d\udc95"
  Console println Try(line.replaceAll("(?ui)(.)\\1{2,}", "$1"))

正常失败

apm@mara:~/tmp$ skalac kcharex.scala ; skala kcharex.Test
Failure(java.lang.StringIndexOutOfBoundsException: String index out of range: 6)

但修复成功

apm@mara:~/tmp$ skala -J-Xbootclasspath/p:../bootfix kcharex.Test
Success(?)

原始示例代码中的另一个错误是内联标志应包含?uiPattern.CASE_INSENSITIVE 上的 javadoc 说:

默认情况下,不区分大小写的匹配假定只有 正在匹配 US-ASCII 字符集。 Unicode 感知大小写不敏感 可以通过指定 UNICODE_CASE 标志来启用匹配 结合这个标志。

从代码 sn-p 中可以看出,没有u,只有当 ASCII.toLower 不比较相等时才会失败,这不是故意的。我不够老练,不知道有一个补充字符会在不编写代码的情况下通过测试。

【讨论】:

    【解决方案2】:

    Santosh 在this answer 中的解释不正确。这可以通过运行来证明

    String str = "???";
    System.out.println("code point: " + .codePointAt(0));
    

    这将输出(至少对我而言)值 128149,this page 确认该值是正确的。所以Java不会以错误的方式解释字符串。使用 getBytes() 方法时,它确实解释错了。

    但是,正如 OP 所解释的那样,正则表达式似乎崩溃了。我没有其他解释,因为它是java中的一个错误。要么这样,要么它在设计上不完全支持 UTF-16。

    编辑:

    基于this answer

    正则表达式编译器在 UTF-16 上搞砸了。再一次,这永远不可能 已修复,否则它将更改旧程序。你甚至无法绕过 通过对 Java 的 Unicode-in-source-code 使用正常的解决方法来解决错误 使用 java -encoding UTF-8 编译会遇到麻烦,因为愚蠢 thing 将字符串存储为讨厌的 UTF-16,这必然会破坏 他们在字符类。哎呀!

    这似乎是java中正则表达式的限制。


    既然你这么评论了

    如果我可以简单地忽略 UTF-16 字符和 应用正则表达式而不是抛出异常。

    这当然可以做到。一种直接的方法是将您的正则表达式仅应用于某个范围。过滤 unicode 字符范围已在 this answer 中进行了说明。基于该答案,示例似乎并没有令人窒息,而只是将问题字符留在了原处:

    line.replaceAll("(?Ui)([\\u0000-\\uffff])\\1{2,}", "$1")    
    
    // "???" -> "???"
    // "foo ??? foo" -> "foo ??? foo"
    // "foo aAa foo" -> "foo a foo"
    

    【讨论】:

    • line.replaceAll("(?Ui)([\\u0000-\\uffff])\\1{2,}", "$1");这似乎是要走的路,绕过错误。谢谢。
    • @binit 没问题。实际上,作为附加信息,this link 告诉 java regex 应该能够处理补充字符,所以我认为这证实了你正在处理一个错误。
    【解决方案3】:

    你提到的字符实际上是“Double byte characters”。这意味着两个字节形成一个字符。但是要让 Java 解释这一点,编码信息(当它与默认平台编码不同时)需要显式传递(否则将使用默认平台编码) .

    为了证明这一点,请考虑以下

    String line = "???";
    System.out.println(line.length());
    

    这会将 长度打印为 6 !而我们只有三个字符,

    下面的代码

    String line1 = new String("???".getBytes(),"UTF-8");
    System.out.println(line1.length());
    

    按预期打印 length as 3

    如果换行

    String line = "???";
    

     String line1 = new String("???".getBytes(),"UTF-8");
    

    它可以工作并且正则表达式不会失败。我在这里使用了 UTF-8。请使用您预期平台的适当编码。

    Java 正则表达式库严重依赖Character Sequence,而Character Sequence 又依赖于编码方案。对于字符编码与默认编码不同的字符串,无法正确解码字符(它显示 6 个字符而不是 3 个!),因此正则表达式失败。

    【讨论】:

    • 嘿桑托什,你的修复对我来说不起作用。我试过: new String("???".getBytes(),"UTF-8").replaceAll("(?i)(.)\\1{2,}", "$1");它仍然崩溃......还有 new String("???".getBytes(),"UTF-8").length() 向我显示 6(你提到了 3)!
    • 在我的机器(Win XP SP2,jdk1.6.0_14)上显示 3 个字符。您使用的操作系统/JDK 是什么?您可以尝试一些不同的编码(例如 UTF-16)吗?你机器的默认字符集是什么?
    • line1.length() 只能是 3 如果您的平台默认编码不支持字符,因此编码 ? 代替它们。所以你看到了字符串"???" 的长度,不知道这是怎么回事。如果您的平台编码是UTF-8,您将获得无用的往返。
    • line1.length()=3 仅适用于单字节字符。当我在不编码的情况下打印字符串时,它会打印??????,即一个字符对应一个字节。
    • @Santosh 那么 op 对“工作”的定义非常糟糕,充其量他在使用正则表达式时不会得到异常,但他不会得到可用的结果。如果您只想计算实际字符,您可以使用Character.codePointCount - 无需将字符串变成垃圾:)
    猜你喜欢
    • 1970-01-01
    • 2013-03-02
    • 1970-01-01
    • 2018-06-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-05-15
    相关资源
    最近更新 更多