使用流计算正则表达式匹配次数

7

我正在尝试使用Java 8的lambda/streams解决方案来计算正则表达式模式的匹配次数。例如,对于这个模式/matcher:

final Pattern pattern = Pattern.compile("\\d+");
final Matcher matcher = pattern.matcher("1,2,3,4");

有一种方法叫做splitAsStream,它会按照给定的模式分割文本,而不是匹配模式。虽然这种方法很优雅并且保持了不可变性,但它并不总是正确的:

// count is 4, correct
final long count = pattern.splitAsStream("1,2,3,4").count();

// count is 0, wrong
final long count = pattern.splitAsStream("1").count();

我还尝试了滥用IntStream的方法。问题在于,我必须猜测我需要调用matcher.find()多少次,而不是一直调用直到返回false。

final long count = IntStream
        .iterate(0, i -> matcher.find() ? 1 : 0)
        .limit(100)
        .sum();

我熟悉传统的解决方案:while (matcher.find()) count++; 其中 count 是可变的。是否有一种使用Java 8 lambdas/streams 的简单方法来完成这个功能?


1
尝试查看 takeWhile:https://dev59.com/YmIi5IYBdhLWcg3w_QmH#20765715 - Tunaki
3
拆分并不等同于匹配。这就是为什么你得到了奇数的原因。你应该否定你的模式,以便检索数字并获得你想要的结果。 - Flown
@Tunaki takeWhile 看起来非常有趣。但显然它只能在 Java 9 中使用,而不是 Java 8。 - Manos Nikolaidis
@Flown 我知道 splitAsStream 做什么以及为什么它不按我使用的方式工作。我刚刚尝试了你的建议来否定模式,惊讶地看到对于 "1,2,3,4""1" 都得到了正确的结果。你想发表回答吗? - Manos Nikolaidis
3
在Java 9中:“matcher.results().count();” - Tagir Valeev
@Tagir 那就太完美了,但它是Java 9。在那之前我被困在while循环中,因为我无法让Flown的解决方案适用于每种情况。 - Manos Nikolaidis
5个回答

4

要正确使用 Pattern::splitAsStream ,您需要反转您的正则表达式。这意味着,您应该使用 \\D + 而不是 \\d + (这将在每个数字处拆分)。这将为您提供字符串中的每个数字。

final Pattern pattern = Pattern.compile("\\D+");
// count is 4
long count = pattern.splitAsStream("1,2,3,4").count();
// count is 1
count = pattern.splitAsStream("1").count();

这是我正在寻找的“简单”解决方案!但我更喜欢像这样否定模式 (?:\\d+),因为它更容易/可行地否定任何其他模式,而不仅仅是整数。 - Manos Nikolaidis
但并非总是有效。对于输入“a 2”,计数为2而不是1。 - Manos Nikolaidis
5
Java 9 提供了一种简单的解决方案:Pattern.compile("\\d+").matcher("1,2,3,4").results().count() 可以统计匹配到的数字数量。 - Holger
有更通用的解决方案吗?我有一个很长的正则表达式字符串,它不仅匹配数字。 - Jenna Kwon
@JennaKwon 这取决于你的使用情况。 - Flown

3

Pattern.splitAsStream 的 javadoc 中使用的语言有些矫揉造作,可能是原因之一。

此方法返回的流包含输入序列中每个由另一个与此模式匹配的子序列终止或由输入序列的结尾终止的子字符串。

如果您打印出 1,2,3,4 的所有匹配项,您可能会惊讶地发现它实际上返回的是逗号,而不是数字。

    System.out.println("[" + pattern.splitAsStream("1,2,3,4")
            .collect(Collectors.joining("!")) + "]");

输出 [!,!,!,]。奇怪的是为什么它给你的是 4 而不是 3

显然,这也解释了为什么 "1" 给出的是 0,因为在字符串中没有数字之间的字符串。

快速演示:

private void test(Pattern pattern, String s) {
    System.out.println(s + "-[" + pattern.splitAsStream(s)
            .collect(Collectors.joining("!")) + "]");
}

public void test() {
    final Pattern pattern = Pattern.compile("\\d+");
    test(pattern, "1,2,3,4");
    test(pattern, "a1b2c3d4e");
    test(pattern, "1");
}

打印
1,2,3,4-[!,!,!,]
a1b2c3d4e-[a!b!c!d!e]
1-[]

谢谢。我其实知道 splitAsStream 做什么以及为什么它不按我使用的方式工作。但我仍然不知道如何计算匹配项。尽管如此,你的答案非常详细和清晰,所以你得到了一个 +1。 - Manos Nikolaidis

3
你可以扩展 AbstractSpliterator 来解决这个问题:
static class SpliterMatcher extends AbstractSpliterator<Integer> {
    private final Matcher m;

    public SpliterMatcher(Matcher m) {
        super(Long.MAX_VALUE, NONNULL | IMMUTABLE);
        this.m = m;
    }

    @Override
    public boolean tryAdvance(Consumer<? super Integer> action) {
        boolean found = m.find();
        if (found)
            action.accept(m.groupCount());
        return found;
    }
}

final Pattern pattern = Pattern.compile("\\d+");

Matcher matcher = pattern.matcher("1");
long count = StreamSupport.stream(new SpliterMatcher(matcher), false).count();
System.out.println("Count: " + count); // 1

matcher = pattern.matcher("1,2,3,4");
count = StreamSupport.stream(new SpliterMatcher(matcher), false).count();
System.out.println("Count: " + count); // 4


matcher = pattern.matcher("foobar");
count = StreamSupport.stream(new SpliterMatcher(matcher), false).count();
System.out.println("Count: " + count); // 0

我刚试了一下,它确实产生了正确的结果。它也非常有启发性!我不确定它是否符合“简单”解决方案的标准!那么我想,我只需要编写一次SpliterMatcher,然后使用不同的匹配器进行重用。 - Manos Nikolaidis
1
每个流创建一个新的spliterator没有问题,这实际上就是在幕后发生的事情。这也是实现一种尚不存在的流的直接方式,在这方面,它确实很简单,它由一个包含一个具体方法和一个委托对象的单个类组成。还能有多简单呢?但是,当您流式处理整数而不是MatchResult时,实现Spliterator.OfInt而不是Spliterator<Integer>并创建一个IntStream更高效。为了确保可重用性,它应该报告ORDERED... - Holger
我建议如果有一个简单、直接的实现方法(这里就是这种情况),最好覆盖forEachRemaining - Holger

1
简单来说,你有一个 String 流 和一个 String 模式:有多少个字符串与该模式匹配?
final String myString = "1,2,3,4";
Long count = Arrays.stream(myString.split(","))
      .filter(str -> str.matches("\\d+"))
      .count();

第一行可以是另一种流式处理方式 List<String>().stream(), ...

我错了吗?


这需要两个不同的正则表达式模式。一个用于分隔符,另一个用于匹配数据。我想避免这种情况。否则它会产生正确的结果。 - Manos Nikolaidis

0

Java 9

您可以使用Matcher#results()来获取所有匹配项:

Stream<MatchResult>    results()
返回与模式匹配的输入序列的每个子序列的匹配结果流。匹配结果按照输入序列中匹配子序列的顺序出现。

Java 8及以下版本

另一个基于使用反向模式的简单解决方案:

String pattern = "\\D+";
System.out.println("1".replaceAll("^" + pattern + "|" + pattern + "$", "").split(pattern, 0).length); // => 1

在这里,从字符串的开头和结尾删除所有非数字字符,然后将该字符串按非数字序列拆分,不报告任何空白尾随元素(因为向split函数传递了0作为limit参数)。
请参见此演示
String pattern = "\\D+";
System.out.println("1".replaceAll("^" + pattern + "|" + pattern + "$", "").split(pattern, 0).length);    // => 1
System.out.println("1,2,3".replaceAll("^" + pattern + "|" + pattern + "$", "").split(pattern, 0).length);// => 3
System.out.println("hz 1".replaceAll("^" + pattern + "|" + pattern + "$", "").split(pattern, 0).length); // => 1
System.out.println("1 hz".replaceAll("^" + pattern + "|" + pattern + "$", "").split(pattern, 0).length); // => 1
System.out.println("xxx 1 223 zzz".replaceAll("^" + pattern + "|" + pattern + "$", "").split(pattern, 0).length);//=>2

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