如何添加 Java 正则表达式实现中缺少的功能?“在下一版本中已修复!”警告 Emptor

2022-09-01 12:19:11

我是Java的新手。作为一名 .Net 开发人员,我非常习惯于 .Net 中的类。(正则表达式)的Java实现还不错,但它缺少一些关键功能。RegexRegex

我想为Java创建自己的帮助器类,但我想也许已经有一个可用的。那么,是否有任何免费且易于使用的产品可用于Java中的正则表达式,或者我应该自己创建一个?

如果我想写我自己的课程,你认为我应该在哪里分享它,让其他人使用它?


[编辑]

有人抱怨我没有解决当前班级的问题。我会试着澄清我的问题。Regex

在 .Net 中,正则表达式的使用比在 Java 中更容易。由于这两种语言都是面向对象的,并且在许多方面都非常相似,因此我希望在两种语言中使用正则表达式时都有类似的体验。不幸的是,事实并非如此。


下面是一些在 Java 和 C# 中比较的代码。第一个是C#,第二个是Java:

在 C# 中:

string source = "The colour of my bag matches the color of my shirt!";
string pattern = "colou?r";

foreach(Match match in Regex.Matches(source, pattern))
{
    Console.WriteLine(match.Value);
}

在爪哇:

String source = "The colour of my bag matches the color of my shirt!";
String pattern = "colou?r";
Pattern p = Pattern.compile(pattern);
Matcher m = p.matcher(source);

while(m.find())
{
    System.out.println(source.substring(m.start(), m.end()));
}

我试图在上面的示例代码中对两种语言都公平。

您在这里注意到的第一件事是该类的成员(与使用和在Java中相比)。.ValueMatch.start().end()

当我可以调用像 or 等静态函数时,为什么要创建两个对象?Regex.MatchesRegex.Match

在更高级的用法中,差异会更加明显。看看方法,字典长度,,,,等。这些都是非常必要的功能,在我看来也应该可用于Java。GroupsCaptureIndexLengthSuccess

当然,所有这些功能都可以由自定义代理(帮助程序)类手动添加。这就是我问这个问题的主要原因。我们没有Perl的轻而易举的,但至少我们可以使用我认为设计得非常巧妙的.Net方法。RegexRegex


答案 1

从您编辑的示例中,我现在可以看到您想要的内容。在这方面,你也有我的同情。Java的正则表达式距离Ruby或Perl中的便利性还有很长的路要走。他们几乎总是这样;这是无法修复的,所以我们永远被困在这个混乱中 - 至少在Java中。其他JVM语言在这方面做得更好,尤其是Groovy。但他们仍然遭受一些固有的缺陷,只能走这么远。

从哪里开始?有 String 类的所谓方便方法:、 、 、 和 。这些有时在小程序中是可以的,这取决于你如何使用它们。但是,他们确实有几个问题,看来您已经发现了这些问题。以下是这些问题的部分列表,以及可以和不能对它们做些什么。matchesreplaceAllreplaceFirstsplit

  1. 不便的方法被非常奇怪地命名为“matches”,但它需要您在两侧填充正则表达式以匹配整个字符串。这种反直觉的含义与以前任何语言中使用的“匹配”这个词的任何含义相反,并且不断咬人。传递给其他3种不便方法的模式的工作方式与此方法非常不同,因为在其他3种方法中,它们的工作方式与其他任何地方的正常模式一样;只是不在.这意味着你不能只是复制你的模式,即使是在同一个的类中的方法中,为了善良的缘故!而且没有方便的方法可以做世界上所有其他匹配器所做的事情。该方法应该被称为 类似 ,并且应该有一个 or 方法添加到 String 类中。matchesfindmatchesFullMatchPartialMatchfind

  2. 没有 API 允许您传入标志以及用于 String 类的 4 个与模式相关的便利方法的字符串。这意味着您必须依赖像 和 这样的字符串版本,但对于所有可能的 Pattern 编译标志,这些版本并不存在。至少可以说这是非常不方便的。Pattern.compile(?i)(?x)

  3. 该方法在边缘情况下返回的结果与 Java 借用拆分的语言中的返回结果不同。这是一个偷偷摸摸的小问题。如果您拆分空字符串,您认为应该在返回列表中返回多少个元素,嗯?Java制造了一个假的返回元素,其中应该有一个,这意味着您无法区分合法结果和虚假结果。这是一个严重的设计缺陷,在 上拆分,您无法区分 的输入与 的。啊,哎呀!人们从来没有测试过这些东西吗?再说一遍,破碎的、从根本上不可靠的行为是无法修复的:你永远不能改变事物,即使是破碎的事物。在Java中打破破碎的东西是不行的,就像在其他任何地方一样。破碎永远在这里。splitsplit":"""":"

  4. 正则表达式的反斜杠表示法与字符串中使用的反斜杠表示法冲突。这使得它变得超级笨拙,也容易出错,因为你必须不断地向所有内容添加大量反斜杠,而且很容易忘记一个,既没有得到警告,也没有得到成功。简单的模式,如成为印刷过剩的噩梦:.祝你好运。有些人在他们的模式上使用斜杠逆变器功能,这样他们就可以把它写成这样。除了从字符串中读取模式外,没有办法以所见即所得的字面方式构建您的模式;它总是充满反斜杠。您是否在正确的地方得到了它们,并且已经足够了?如果是这样,它真的很难阅读。如果不是,您可能还没有全部得到它们。至少像Groovy这样的JVM语言已经在这里找到了正确的答案:给人们一等正则表达式,这样你就不会发疯了。这是一个公平的Groovy正则表达式示例集合,展示了它可以和应该是多么简单。\b\w+\b"\\b\\w+\\b""/b/w+/b"

  5. 该模式存在严重缺陷。它不接受 Java 样式的注释,而是 shell 样式的 。它不适用于多行字符串。它不接受文字作为文字,强制上面列出的反斜杠问题,这从根本上损害了任何排列事情的尝试,比如让所有评论都在同一列上开始。由于反斜杠,您可以使它们从源代码字符串中的同一列开始,并在打印出来时将它们搞砸,反之亦然。这么多的易读性!(?x)// COMMENT# COMMENT

  6. 在正则表达式中输入 Unicode 字符是非常困难的 - 实际上,从根本上说是不可修复的。不支持以符号命名的字符,如 、 或 。这意味着你被困在无法维护的幻数中。您甚至无法通过码位输入它们。不能用于第一个,因为 Java 预处理器会将其设置为语法错误。因此,然后您移动到,这在您到达下一个之前一直有效,该方法无法以这种方式输入,否则它将破坏标志。最后一个是纯粹的噩梦:它的代码点是U + 1D402,但是Java不支持使用正则表达式中的码位编号的完整Unicode集,迫使你拿出计算器来弄清楚这是或(但不是),疯狂地。但是由于设计错误,您无法在字符类中使用它们,从而无法匹配,因为正则表达式编译器在UTF-16上搞砸了。同样,这永远无法修复,否则它将更改旧程序。您甚至无法通过使用 Java 的 Unicode 源代码问题(通过编译 )来解决该错误,因为愚蠢的事情是将字符串存储为令人讨厌的 UTF-16,这必然会在字符类中破坏它们。哎呀!\N{QUOTATION MARK}\N{LATIN SMALL LETTER E WITH GRAVE}\N{MATHEMATICAL BOLD CAPITAL C}\u0022\\u0022\\u00E8CANON_EQ\uD835\uDC02\\uD835\\uDC02\\uD835\uDC02[\N{MATHEMATICAL BOLD CAPITAL A}-\N{MATHEMATICAL BOLD CAPITAL Z}]java -encoding UTF-8

  7. 我们在其他语言中依赖的许多正则表达式的东西在Java中都缺失了。例如,没有命名组,甚至没有相对编号的组。这使得从较小的模式中构建较大的模式从根本上容易出错。有一个前端库,允许您拥有简单的命名组,实际上这最终将到达生产版JDK7。但即便如此,也没有机制可以处理多个同名的组。而且您仍然没有相对编号的缓冲区。我们又回到了糟糕的旧时代,那些很久以前就已经解决了的事情。

  8. 不支持换行序列,这是标准中仅有的两个“强烈建议”部分之一,这表明应该用于此类。这很难模仿,因为它的可变长度性质和Java缺乏对字素的支持。\R

  9. 字符类转义不适用于 Java 的本机字符集!是的,没错:像 and(或者更确切地说,和 )这样的常规内容在 Java 中的 Unicode 上不起作用!这不是那种很酷的复古。更糟糕的是,Java的(使那个,这与)确实有一些Unicode敏感性,尽管不是标准所说的它必须具有的。例如,像 Java 中的字符串永远不会匹配该模式,并且不仅仅是整个 per ,而且实际上永远不会像您可能从中得到的那样。这简直是搞砸了,以至于乞丐信仰。它们已经破坏了 和 之间的固有联系,然后错误地将它们定义为引导!!它甚至不知道Unicode字母代码点是什么。这是非常破碎的,他们永远无法修复它,因为这会改变现有代码的行为,这在Java宇宙中是严格禁止的。你能做的最好的事情就是创建一个重写库,在它进入编译阶段之前充当前端;通过这种方式,您可以强行将模式从 20 世纪 60 年代迁移到 21 世纪的文本处理。\w\s"\\w""\\b"\b"\\b""\b""élève"\b\w+\bPattern.matchesPattern.find\w\b

  10. 唯一支持的两个 Unicode 属性是“常规类别”和“块”属性。一般类别属性仅支持缩写,如 ,与标准相反,强烈建议也允许 , 等。您甚至无法获得标准规定的所需别名。这会使您的代码更加不可读和不可维护。您最终将获得对生产版 JDK7 中 Script 属性的支持,但这仍然严重低于标准规定您必须提供哪怕是最低级别的 Unicode 支持的 11 个基本属性的最小集合。\p{Sk}\p{Modifier Symbol}\p{Modifier_Symbol}

  11. Java确实提供的一些微薄的属性是的:它们与官方Unicode专有名称具有相同的名称,但它们所做的完全不同。例如,Unicode 要求它与 相同,但 Java 仅将其设置为过时且不再古怪的 7 位字母,这比 4 个数量级少得多。空格是另一个缺陷,因为您使用的Java版本伪装成Unicode空格,您的UTF-8解析器将因其NO-BREAK空格代码点而中断,Unicode规范地要求将其视为空格,但Java忽略了该要求,因此会破坏您的解析器。\p{alpha}\p{Alphabetic}

  12. 不支持字素,这是通常提供的方式。这使得您需要和想要使用正则表达式执行的无数常见任务变得不可能。扩展的字素簇不仅超出了您的范围,因为Java几乎不支持Unicode属性,您甚至无法使用标准来近似旧的遗留字素簇。由于无法使用字素,即使是最简单的 Unicode 文本处理也是不可能的。例如,在 Java 中,无论音调符号如何,您都不能匹配元音。在支持字素的语言中执行此操作的方式各不相同,但至少您应该能够将该内容放入NFD并进行匹配 。在Java中,你甚至不能做那么多:字素是你无法企及的。这意味着Java甚至无法处理自己的本机字符集。它为您提供了Unicode,然后使其无法使用它。\X(?:\p{Grapheme_Base}\p{Grapheme_Extend}]*)(?:(?=[aeiou])\X)

  13. String 类中的便利方法不缓存已编译的正则表达式。事实上,没有编译时模式这样的东西在编译时进行语法检查-编译时应该进行语法检查。这意味着你的程序,除了在编译时完全理解的常量正则表达式之外什么都不使用,如果你在这里或那里忘记了一个小的反斜杠,就会在运行过程中出现异常,因为前面讨论的缺陷是不会做的。即使是Groovy也把这部分做对了。正则表达式是一个太高级的构造,无法通过Java令人不快的事后螺栓固定在一边的模型来处理 - 而且它们对于常规文本处理来说太重要了,不容忽视。对于这些东西来说,Java是一门太低级的语言,它无法提供简单的机制,从中可以自己构建你需要的东西:你不能从这里到达那里。

  14. 和 类在 Java 中标记。这完全扼杀了使用适当的OO设计来扩展这些类的任何可能性。您无法通过子类化和替换来创建更好的方法版本。哎呀,你甚至不能子类!最终不是解决办法;最后是解决办法。最终是死刑判决,没有上诉。StringPatternfinalmatches

最后,为了向您展示Java的真正正则表达式是如何被大脑损坏的,请考虑这种多行模式,它显示了已经描述的许多缺陷:

   String rx =
          "(?= ^ \\p{Lu} [_\\pL\\pM\\d\\-] + \$)\n"
        + "   # next is a big can't-have set    \n"
        + "(?! ^ .*                             \n"
        + "    (?: ^     \\d+              $    \n"
        + "      | ^ \\p{Lu} - \\p{Lu}     $    \n"
        + "      | Invitrogen                   \n"
        + "      | Clontech                     \n"
        + "      | L-L-X-X    # dashes ok       \n"
        + "      | Sarstedt                     \n"
        + "      | Roche                        \n"
        + "      | Beckman                      \n"
        + "      | Bayer                        \n"
        + "    )      # end alternatives        \n"
        + "    \\b    # only on a word boundary \n"
        + ")          # end negated lookahead   \n"
        ;

你看这有多不自然吗?您必须在字符串中放置文字换行符;你必须使用非Java注释;由于额外的反斜杠,您无法使任何内容对齐;你必须使用在Unicode上不起作用的东西的定义。除此之外,还有更多问题。

不仅没有计划修复几乎任何这些严重的缺陷,而且确实不可能修复几乎任何一个,因为你改变了旧程序。即使是OO设计的正常工具也是禁止的,因为它都被死刑判决的最终性锁定了,并且无法修复。

所以 Alireza Noori,如果你觉得 Java 笨拙的正则表达式太笨拙了,无法在 Java 中实现可靠和方便的正则表达式处理,我不能说你。很抱歉,但事实就是如此。

“在下一版本中已修复!”

仅仅因为有些事情永远无法修复,并不意味着没有什么可以修复。它只需要非常小心地完成。以下是我所知道的在当前JDK7或建议的JDK8版本中已经修复的内容:

  1. 现在支持 Unicode 脚本属性。您可以使用任何等效的形式 、 、 或 。这本质上优于旧的笨重块属性。这意味着你可以做这样的事情,这非常重要。\p{Script=Greek}\p{sc=Greek}\p{IsGreek}\p{Greek}[\p{Latin}\p{Common}\p{Inherited}]

  2. UTF-16 错误有一个解决方法。现在,您可以使用表示法按数字指定任何 Unicode 码位,例如 。这甚至在字符类中也有效,最终允许正常工作。不过,您仍然必须对它进行双反斜杠,并且它仅适用于正则表达式,而不是一般的字符串,因为它确实应该如此。\x{⋯}\x{1D402}[\x{1D400}-\x{1D419}]

  3. 现在支持命名组通过标准表示法来创建它并反向引用它。这些仍然有助于数字组数。但是,您不能以相同的模式获得多个它们,也不能将它们用于递归。(?<NAME>⋯)\k<NAME>

  4. 一个新的 Pattern 编译标志和关联的嵌入式开关 现在将围绕 、 、 和 等内容的所有定义进行交换,以便它们现在符合 Unicode 标准所要求的那些内容的定义Pattern.UNICODE_CHARACTER_CLASSES(?U)\w\b\p{alpha}\p{punct}

  5. 缺少或错误定义的二进制属性 ,现在将受支持,这些属性对应于类中的方法。这很重要,因为 Unicode 在单纯字母和大小写或字母代码点之间做出了重要而普遍的区分。这些关键属性是 1 级符合 UTS#18 “Unicode Regular Expresions” 绝对需要的 11 个基本属性之一,没有这些属性,您就无法使用 Unicode。\p{IsLowercase}\p{IsUppercase}\p{IsAlphabetic}Character

这些增强功能和修复程序最终非常重要,因此我很高兴,甚至很兴奋。

但是对于工业级的、最先进的正则表达式和/或Unicode工作,我不会使用Java。Java的20年后仍然不完整的Unicode模型中缺少太多了,如果你敢于使用Java提供的字符集,就无法完成真正的工作。而螺栓固定在一边的模型永远不会起作用,这就是Java正则表达式的全部。你必须从第一原则重新开始,就像Groovy所做的那样。

当然,它可能适用于非常有限的应用程序,这些应用程序的客户群很小,仅限于爱荷华州农村的英语单语,没有外部交互,也不需要旧式电报可以发送的字符。但是,对于多少项目来说,这是真的吗?事实证明,即使你认为的更少。

正是出于这个原因,最近一个特定(且显而易见的)数十亿美元取消了一个重要应用程序的国际部署。Java的Unicode支持 - 不仅在正则表达式中,而且在整个过程中 - 被证明太弱,无法在Java中可靠地完成所需的国际化。正因为如此,他们被迫从原计划的全球部署缩减到仅仅是美国的部署。这是积极的狭隘的。不,有Nᴏᴛ Hᴀᴘᴘʏ;你会吗?

Java已经有20年的时间把它做好了,到目前为止,他们显然还没有这样做,所以我不会屏住呼吸。或者投善钱后再投善钱;这里的教训是忽略炒作,而是进行尽职调查,以确保在您投入太多之前所有必要的基础设施支持都在那里。否则,一旦你太深入,无法挽救你的项目,你也可能会陷入没有任何真正选择的困境。

警告 Emptor


答案 2

一个人可以咆哮,或者可以简单地写:

public class Regex {

    /**
     * @param source 
     *        the string to scan
     * @param pattern
     *        the regular expression to scan for
     * @return the matched 
     */
    public static Iterable<String> matches(final String source, final String pattern) {
        final Pattern p = Pattern.compile(pattern);
        final Matcher m = p.matcher(source);
        return new Iterable<String>() {
            @Override
            public Iterator<String> iterator() {
                return new Iterator<String>() {
                    @Override
                    public boolean hasNext() {
                        return m.find();
                    }
                    @Override
                    public String next() {
                        return source.substring(m.start(), m.end());
                    }    
                    @Override
                    public void remove() {
                        throw new UnsupportedOperationException();
                    }
                };
            }
        };
    }

}

根据需要使用:

public class RegexTest {

    @Test
    public void test() {
       String source = "The colour of my bag matches the color of my shirt!";
       String pattern = "colou?r";
       for (String match : Regex.matches(source, pattern)) {
           System.out.println(match);
       }
    }
}