使用Java正则表达式匹配器查找最后一个匹配项

28

我正在尝试获取匹配结果中的最后一个,而不必通过循环 .find() 方法来实现

这是我的代码:

String in = "num 123 num 1 num 698 num 19238 num 2134";
Pattern p = Pattern.compile("num ([0-9]+)");
Matcher m = p.matcher(in);

if (m.find()) {
     in = m.group(1);
}

这将给我第一个结果。如何找到最后一个匹配项,而无需循环遍历潜在的大列表?


你能确定它是字符串中的最后一个吗?如果是,只需使用行尾锚点$ /(num ([0-9]+)$/,但这需要转换成Java。 - NorthGuard
你可以编写一个递归方法,但我怀疑它是否有意义。 - s106mo
11个回答

21
你可以在正则表达式前面加上.*,它会贪婪地匹配所有字符直到最后一个匹配为止:
import java.util.regex.*;

class Test {
  public static void main (String[] args) {
    String in = "num 123 num 1 num 698 num 19238 num 2134";
    Pattern p = Pattern.compile(".*num ([0-9]+)");
    Matcher m = p.matcher(in);
    if(m.find()) {
      System.out.println(m.group(1));
    }
  }
}

输出:

2134

您也可以反转字符串,然后更改正则表达式以匹配反转后的字符串:

import java.util.regex.*;

class Test {
  public static void main (String[] args) {
    String in = "num 123 num 1 num 698 num 19238 num 2134";
    Pattern p = Pattern.compile("([0-9]+) mun");
    Matcher m = p.matcher(new StringBuilder(in).reverse());
    if(m.find()) {
      System.out.println(new StringBuilder(m.group(1)).reverse());
    }
  }
}

但是,在我看来,没有一种解决方案比仅仅使用 while (m.find()) 循环遍历所有匹配更好。


3
是的,我认为那是作弊。:-) 将其扩展到一般情况将会非常困难。 - Mark Peters
3
赞成第二个方案,但反对你一开始提出的那个可怕的东西。 ;) - Alan Moore
1
我不想使用while(m.find())循环的原因是我正在解析HTML并且有很多结果。我试图尽可能地使我的代码高效。我的想法是,仅仅为了获取最后一个元素而无谓地循环整个数组会很慢。Java的正则表达式没有包含结果数量,这让人感到遗憾。我会尝试使用你的方法。 - kireol

15
为了获取最后一个匹配项,即使是这种方法也可以使用,但不确定为什么早先没有提到它:
String in = "num 123 num 1 num 698 num 19238 num 2134";
Pattern p = Pattern.compile("num '([0-9]+) ");
Matcher m = p.matcher(in);
if (m.find()) {
  in= m.group(m.groupCount());
}

你是对的!线程启动者并不想要索引信息,只需要内容。这看起来就是正确的答案。 - KFleischer
@KFleischer 你确定这个可行吗?正则表达式与输入字符串无关。 - necromancer
@necromancer 这是一段时间前的事情了,所以我简单地思考了一下发生了什么:使用的模式是主题发起者说对他有效的模式,查找第一个匹配项。对主题发起者代码唯一的更改是使用发现数量来寻址最后一个组。这很简单,我相信当我写下我的评论时它对我起作用了。 - KFleischer
6
顺带一提,我意识到你可能误解了 m.groupCount() 的语义 -- 它与找到多少匹配项无关。它是正则表达式中有多少组的计数。在你的示例代码中,它始终为1,因为在你的正则表达式中只有1个组。 - necromancer
1
@KFleischer 我知道你不是回答这个问题的人;)这个答案实际上很奇怪。我将其插入到一个主类中,in 的值是 num 123 num 1 num 698 num 19238 num 2134,哈哈:v - necromancer
这实际上并不起作用,正如@necromancer指出的那样,但我知道araut的想法在哪里。也许他想到了像这样的东西,它确实起作用:`String result2=null; for (int index = in.length()-1; result2==null&&index>=0;index--){ System.out.println("index is "+index); if (m.find(index)) { result2= m.group(1); System.out.println("result2 is "+result2); } }` - michaelok

6
为什么不保持简单?
in.replaceAll(".*[^\\d](\\d+).*", "$1")

14
你能解释一下它的作用吗? - KFleischer
替换模式也是贪婪的,因此它寻找:'任何符号后跟非数字符号,后跟任意数量的数字(这是我们的最后一个数字),后跟末尾的任何符号(虽然没有要求但仍然有用)',并将其替换为第一组。第一组是括号中的内容,它是我们的最后一个数字。 - Vova Yatsyk
如果最后一个数字在字符串开头,则该解决方案将无法正常工作:123 num。对于最通用的答案,请检查负向先行断言。 - Vova Yatsyk

4
使用负向先行断言:
String in = "num 123 num 1 num 698 num 19238 num 2134";
Pattern p = Pattern.compile("num (\\d+)(?!.*num \\d+)");
Matcher m = p.matcher(in);

if (m.find()) {
    in= m.group(1);
}

正则表达式的意思是“数字后跟至少一个数字,且在它之后没有任何数字和至少一个数字的组合”。如果加上正向回顾匹配,则可以更进一步。
String in = "num 123 num 1 num 698 num 19238 num 2134";
Pattern p = Pattern.compile("(?<=num )\\d+(?!.*num \\d+)");
Matcher m = p.matcher(in);

if (m.find()) {
    in = m.group();
}

那个读作“至少一个数字前面是(数字和一个空格),且在其之后的任何地方都不跟随(数字后跟一个空格和至少一个数字)”。这样你就不必担心分组并为Matcher.group(int)可能抛出的IndexOutOfBoundsException而烦恼了。

3

Java没有提供这样的机制。我唯一能建议的是使用二分查找来寻找最后一个索引。

具体实现如下:

N = haystack.length();
if ( matcher.find(N/2) ) {
    recursively try right side
else
    recursively try left side

编辑

以下是代码,因为我觉得这是一个有趣的问题:

import org.junit.Test;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.junit.Assert.assertEquals;

public class RecursiveFind {
    @Test
    public void testFindLastIndexOf() {
        assertEquals(0, findLastIndexOf("abcdddddd", "abc"));
        assertEquals(1, findLastIndexOf("dabcdddddd", "abc"));
        assertEquals(4, findLastIndexOf("aaaaabc", "abc"));
        assertEquals(4, findLastIndexOf("aaaaabc", "a+b"));
        assertEquals(6, findLastIndexOf("aabcaaabc", "a+b"));
        assertEquals(2, findLastIndexOf("abcde", "c"));
        assertEquals(2, findLastIndexOf("abcdef", "c"));
        assertEquals(2, findLastIndexOf("abcd", "c"));
    }

    public static int findLastIndexOf(String haystack, String needle) {
        return findLastIndexOf(0, haystack.length(), Pattern.compile(needle).matcher(haystack));
    }

    private static int findLastIndexOf(int start, int end, Matcher m) {
        if ( start > end ) {
            return -1;
        }

        int pivot = ((end-start) / 2) + start;
        if ( m.find(pivot) ) {
            //recurse on right side
            return findLastIndexOfRecurse(end, m);
        } else if (m.find(start)) {
            //recurse on left side
            return findLastIndexOfRecurse(pivot, m);
        } else {
            //not found at all between start and end
            return -1;
        }
    }

    private static int findLastIndexOfRecurse(int end, Matcher m) {
        int foundIndex = m.start();
        int recurseIndex = findLastIndexOf(foundIndex + 1, end, m);
        if ( recurseIndex == -1 ) {
            return foundIndex;
        } else {
            return recurseIndex;
        }
    }

}

我还没有找到一个能够引起故障的测试用例。


我发现了一个特殊情况,它不能正常工作:制作一个由可选部分组成的模式。如果模式的一部分落在二分搜索的一侧,而另一部分落在另一侧,搜索将仅找到整个模式的一小部分。你的代码没有找到最大匹配。 - KFleischer
@KFleischer:在这种情况下,不是这样更好吗? aaaa[a]+ 的最后一个出现应该在索引4处,而不是索引0处吗?当你查找某个东西的最后一个索引时,如果它导致更大的索引,接受最小匹配似乎是合理的。如果您认为这不是期望的行为,也许您可以给出一个具体的例子。 - Mark Peters

2

Java默认采用贪婪模式,以下代码可以解决此问题。

    String in = "num 123 num 1 num 698 num 19238 num 2134";
    Pattern p = Pattern.compile( ".*num ([0-9]+).*$" );
    Matcher m = p.matcher( in );

    if ( m.matches() )
    {
        System.out.println( m.group( 1 ));
    }

为什么要在结尾使用 .*$ - ArtOfWarfare
@ArtOfWarfare,这并不是必要的。 - krico

0

这似乎是一个更加平衡合理的方法。

    public class LastMatchTest {
        public static void main(String[] args) throws Exception {
            String target = "num 123 num 1 num 698 num 19238 num 2134";
            Pattern regex = Pattern.compile("(?:.*?num.*?(\\d+))+");
            Matcher regexMatcher = regex.matcher(target);

            if (regexMatcher.find()) {
                System.out.println(regexMatcher.group(1));
            }
        }
    }

.*? 是一种勉强匹配,因此它不会吞噬所有内容。 ?: 强制使用非捕获组,因此内部组是第1组。 贪婪地匹配多个项目会导致它跨越整个字符串进行匹配,直到所有匹配项都用完为止,留下第1组具有最后匹配值的结果。


0

相较于目前被接受的答案,这个方法不会盲目地使用".*"前缀去丢弃列表元素。取而代之的是,它使用"(元素分隔符)*(元素)"来挑选最后一个元素,使用.group(2)。请参见下面代码中的magic_last函数。

为了展示此方法的优点,我还包括了一个选择第n个元素的函数,该函数足够健壮,可以接受少于n个元素的列表。请参见下面代码中的magic函数。

过滤掉文本中的"num ",并仅获取数字的部分留给读者进行练习(只需在数字模式周围添加额外的组:([0-9]+),并选择组4而不是组2)。

package com.example;

import static java.lang.System.out;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Foo {

  public static void main (String [] args) {
    String element = "num [0-9]+";
    String delimiter = ", ";
    String input;
    input = "here is a num bro: num 001; hope you like it";
    magic_last(input, element, delimiter);
    magic(1, input, element, delimiter);
    magic(2, input, element, delimiter);
    magic(3, input, element, delimiter);
    input = "here are some nums bro: num 001, num 002, num 003, num 004, num 005, num 006; hope you like them";
    magic_last(input, element, delimiter);
    magic(1, input, element, delimiter);
    magic(2, input, element, delimiter);
    magic(3, input, element, delimiter);
    magic(4, input, element, delimiter);
    magic(5, input, element, delimiter);
    magic(6, input, element, delimiter);
    magic(7, input, element, delimiter);
    magic(8, input, element, delimiter);
  }

  public static void magic_last (String input, String element, String delimiter) {
    String regexp = "(" + element + delimiter + ")*(" + element + ")";
    Pattern pattern = Pattern.compile(regexp);
    Matcher matcher = pattern.matcher(input);
    if (matcher.find()) {
        out.println(matcher.group(2));
    }
  }

  public static void magic (int n, String input, String element, String delimiter) {
    String regexp = "(" + element + delimiter + "){0," + (n - 1) + "}(" + element + ")(" + delimiter + element + ")*";
    Pattern pattern = Pattern.compile(regexp);
    Matcher matcher = pattern.matcher(input);
    if (matcher.find()) {
        out.println(matcher.group(2));
    }
  }

}

输出:

num 001
num 001
num 001
num 001
num 006
num 001
num 002
num 003
num 004
num 005
num 006
num 006
num 006

0

正则表达式是贪婪的:

Matcher m=Pattern.compile(".*num '([0-9]+) ",Pattern.DOTALL).matcher("num 123 num 1 num 698 num 19238 num 2134");

会给你最后一次匹配的Matcher,你可以在大多数正则表达式中加上 ".*" 来应用它。当然,如果无法使用DOTALL,您可能想要使用(?:\d|\D)或类似的内容作为通配符。


0
String in = "num 123 num 1 num 698 num 19238 num 2134";  
Pattern p = Pattern.compile("num '([0-9]+) ");  
Matcher m = p.matcher(in);  
String result = "";

while (m.find())
{
     result = m.group(1);
}

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