41

我正在尝试同时接收模式和字符串并返回组名映射 - >匹配结果。

例子:

(?<user>.*)

我想返回一个包含“用户”作为键的映射,以及它匹配的任何值作为它的值。

问题是我似乎无法从 Java regex api 获取组名。我只能按名称或索引获取匹配的值。我没有组名列表,而且 Pattern 和 Matcher 似乎都没有公开这些信息。我检查了它的来源,似乎信息就在那里——只是没有向用户公开。

我尝试了 Java 的 java.util.regex 和 jregex。(并且真的不在乎是否有人建议任何其他支持此功能的良好,受支持和高性能的库)。

4

6 回答 6

51

Java 中没有 API 来获取命名捕获组的名称。我认为这是一个缺失的功能。

简单的方法是从模式中挑选出候选命名捕获组,然后尝试从匹配中访问命名组。换句话说,在插入与整个模式匹配的字符串之前,您不知道命名捕获组的确切名称。

Pattern捕获命名捕获组的名称是(\(\?<([a-zA-Z][a-zA-Z0-9]*)>基于Pattern类文档派生的)。

(困难的方法是为正则表达式实现解析器并获取捕获组的名称)。

示例实现:

import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;
import java.util.Iterator;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.regex.MatchResult;

class RegexTester {

    public static void main(String args[]) {
        Scanner scanner = new Scanner(System.in);

        String regex = scanner.nextLine();
        StringBuilder input = new StringBuilder();
        while (scanner.hasNextLine()) {
            input.append(scanner.nextLine()).append('\n');
        }

        Set<String> namedGroups = getNamedGroupCandidates(regex);

        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(input);
        int groupCount = m.groupCount();

        int matchCount = 0;

        if (m.find()) {
            // Remove invalid groups
            Iterator<String> i = namedGroups.iterator();
            while (i.hasNext()) {
                try {
                    m.group(i.next());
                } catch (IllegalArgumentException e) {
                    i.remove();
                }
            }

            matchCount += 1;
            System.out.println("Match " + matchCount + ":");
            System.out.println("=" + m.group() + "=");
            System.out.println();
            printMatches(m, namedGroups);

            while (m.find()) {
                matchCount += 1;
                System.out.println("Match " + matchCount + ":");
                System.out.println("=" + m.group() + "=");
                System.out.println();
                printMatches(m, namedGroups);
            }
        }
    }

    private static void printMatches(Matcher matcher, Set<String> namedGroups) {
        for (String name: namedGroups) {
            String matchedString = matcher.group(name);
            if (matchedString != null) {
                System.out.println(name + "=" + matchedString + "=");
            } else {
                System.out.println(name + "_");
            }
        }

        System.out.println();

        for (int i = 1; i < matcher.groupCount(); i++) {
            String matchedString = matcher.group(i);
            if (matchedString != null) {
                System.out.println(i + "=" + matchedString + "=");
            } else {
                System.out.println(i + "_");
            }
        }

        System.out.println();
    }

    private static Set<String> getNamedGroupCandidates(String regex) {
        Set<String> namedGroups = new TreeSet<String>();

        Matcher m = Pattern.compile("\\(\\?<([a-zA-Z][a-zA-Z0-9]*)>").matcher(regex);

            while (m.find()) {
                namedGroups.add(m.group(1));
            }

            return namedGroups;
        }
    }
}

但是,此实现有一个警告。它目前不适用于Pattern.COMMENTS模式下的正则表达式。

于 2013-03-23T16:13:10.023 回答
22

这是解决该问题的第二种简单方法:我们将调用 Pattern 类中的非公共方法namedGroups()来获得一个Map<String, Integer>通过Java Reflection API将组名映射到组号的方法。这种方法的优点是我们不需要包含与正则表达式匹配的字符串来查找确切的命名组。

就个人而言,我认为这并没有太大的优势,因为知道输入字符串中不存在与正则表达式匹配的正则表达式的命名组是没有用的。

但是,请注意缺点

  • 如果代码在具有安全限制以拒绝任何尝试访问非公共方法(无修饰符、受保护和私有方法)的系统中运行,则此方法可能不适用。
  • 该代码仅适用于 Oracle 或 OpenJDK 的 JRE。
  • 该代码也可能在未来的版本中中断,因为我们正在调用非公共方法。
  • 通过反射调用函数也可能会影响性能。(在这种情况下,性能损失主要来自反射开销,因为namedGroups()方法中没有太多事情发生)。我不知道性能影响如何影响整体性能,所以请在您的系统上进行测量。

import java.util.Collections;
import java.util.Map;
import java.util.Scanner;
import java.util.regex.Pattern;

import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;

class RegexTester {
  public static void main(String args[]) {
    Scanner scanner = new Scanner(System.in);

    String regex = scanner.nextLine();
    // String regex = "(?<group>[a-z]*)[trick(?<nothing>ha)]\\Q(?<quoted>Q+E+)\\E(.*)(?<Another6group>\\w+)";
    Pattern p = Pattern.compile(regex);

    Map<String, Integer> namedGroups = null;
    try {
      namedGroups = getNamedGroups(p);
    } catch (Exception e) {
      // Just an example here. You need to handle the Exception properly
      e.printStackTrace();
    }

    System.out.println(namedGroups);
  }


  @SuppressWarnings("unchecked")
  private static Map<String, Integer> getNamedGroups(Pattern regex)
      throws NoSuchMethodException, SecurityException,
             IllegalAccessException, IllegalArgumentException,
             InvocationTargetException {

    Method namedGroupsMethod = Pattern.class.getDeclaredMethod("namedGroups");
    namedGroupsMethod.setAccessible(true);

    Map<String, Integer> namedGroups = null;
    namedGroups = (Map<String, Integer>) namedGroupsMethod.invoke(regex);

    if (namedGroups == null) {
      throw new InternalError();
    }

    return Collections.unmodifiableMap(namedGroups);
  }
}
于 2013-03-24T07:28:23.007 回答
6

您想使用小名称正则表达式库。它是一个瘦包装器,java.util.regex带有对 Java 5 或 6 用户的命名捕获组支持。

示例用法:

Pattern p = Pattern.compile("(?<user>.*)");
Matcher m = p.matcher("JohnDoe");
System.out.println(m.namedGroups()); // {user=JohnDoe}

马文:

<dependency>
  <groupId>com.github.tony19</groupId>
  <artifactId>named-regexp</artifactId>
  <version>0.2.3</version>
</dependency>

参考:

于 2015-04-14T13:41:32.037 回答
2

我在“真实”模式中使用了一组正则表达式来获取组的名称,如下所示:

        List<String> namedGroups = new ArrayList<String>();
    {
        String normalized = matcher.pattern().toString();
        Matcher mG = Pattern.compile("\\(\\?<(.+?)>.*?\\)").matcher(normalized);

        while (mG.find()) {
            for (int i = 1; i <= mG.groupCount(); i++) {
                namedGroups.add(mG.group(i));
            }
        }
    }

然后,我将带有值的名称添加到 HashMap<String, String> 中:

        Map<String, String> map = new HashMap<String, String>(matcher.groupCount());
        
        namedGroups.stream().forEach(name -> {      
            if (matcher.start(name) > 0) {
                map.put(name, matcher.group(name));
            } else {
                map.put(name, "");
            }
        });
于 2020-11-25T20:52:00.920 回答
0

使用标准 API 无法做到这一点。您可以使用反射来访问这些:

final Field namedGroups = pattern.getClass().getDeclaredField("namedGroups");
namedGroups.setAccessible(true);
final Map<String, Integer> nameToGroupIndex = (Map<String, Integer>) namedGroups.get(pattern);

如果您不关心索引,请使用地图的键集。

于 2018-11-08T09:30:04.730 回答
0

目前没有办法通过标准 API 实现这一点。

JDK 错误系统中有一个未解决的问题(JDK-7032377“MatchResult and Pattern 应该提供一种查询命名捕获组名称的方法”),要求通过MatchResultPatternAPI 公开命名捕获组。

这个错误是在十年前的 2011 年创建的,并在 2020 年更新,但我看不到任何迹象表明这是否会很快实施。

于 2022-01-11T20:00:30.267 回答