在java regex中获取组名

x33g5p2x  于 2023-06-07  发布在  Java
关注(0)|答案(8)|浏览(529)

我尝试接收一个模式和一个字符串,并返回一个组名->匹配结果的Map。
示例:

(?<user>.*)

我想返回一个包含“user”作为键的Map,以及它匹配的任何值。
问题是我似乎无法从Java regex API中获取组名。我只能通过名称或索引获取匹配的值。我没有组名列表,Pattern和Matcher似乎都没有公开这些信息。我已经检查了它的来源,似乎信息是存在的-它只是没有暴露给用户。
我尝试了Java的java.util.regex和jregex。(并且不在乎是否有人建议任何其他库,这些库是好的,受支持的,并且在支持此功能的性能方面很高)。

f0ofjuux

f0ofjuux1#

Java中没有API来获取命名捕获组的名称。我认为这是一个缺失的功能。
简单的方法是从模式中挑选出候选的命名捕获组,然后尝试从匹配中访问命名组。换句话说,在插入匹配整个模式的字符串之前,您不知道命名捕获组的确切名称。
用于捕获命名捕获组名称的Pattern\(\?<([a-zA-Z][a-zA-Z0-9]*)>(基于Pattern类文档派生)。
(The困难的方法是实现正则表达式的解析器并获取捕获组的名称)。
示例实现:

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模式下使用regex。

1bqhqjot

1bqhqjot2#

这是解决这个问题的第二个简单方法:我们将调用Pattern类中的非公共方法namedGroups()来获得一个Map<String, Integer>,它通过Java Reflection API将组名Map到组号。这种方法的优点是,我们不需要一个包含正则表达式匹配的字符串来找到确切的命名组。
就我个人而言,我认为这不是一个很大的优势,因为在输入字符串中不存在与正则表达式匹配的正则表达式时,知道正则表达式的命名组是无用的。

但是,请注意the drawbacks

  • 如果代码在具有安全限制的系统中运行,拒绝任何试图访问非公共方法(无修饰符,受保护和私有方法)的尝试,则此方法可能不适用。
  • 该代码仅适用于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);
  }
}
quhf5bfb

quhf5bfb3#

你想使用小型name-regexp库。它是一个围绕java.util.regex的瘦 Package 器,支持Java 5或6用户的命名捕获组。

示例用法:

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

Maven:

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

参考文献:

f0ofjuux

f0ofjuux4#

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

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, "");
            }
        });
i2byvkas

i2byvkas5#

没有办法用标准的API做到这一点。您可以使用反射来访问以下内容:

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

如果你不关心索引,使用Map的键集。

dwbf0jvd

dwbf0jvd6#

Java版本20之前的版本无法通过标准API实现这一点。
这是一个长期公认的需求,JDK Bug System issue JDK-7032377 "MatchResult and Pattern should provide a way to query names of named-capturing groups"就是证明。此问题要求通过MatchResultPattern API公开命名捕获组。这个问题是在2011年创建的,该功能最终在2022年为Java 20实现。

dbf7pr2w

dbf7pr2w7#

public Map<String, Integer> namedGroups()可能很快会在Java 20中出现吗?
(我跟踪了前面提到的JDK bug ticket JDK-7032377中的链接。它已经获得了一些牵引力,this commit

mtb9vblg

mtb9vblg8#

从Java 20开始,这可以在MatchResult上使用namedGroups方法(Matcher实现)来实现:

String name = "2023-06-05 johndoe123";
Pattern regex = Pattern.compile("(?<date>[0-9-]+) (?<user>\\w+)");
Matcher matcher = regex.matcher(name);
if (matcher.matches()) {
    MatchResult matchResult = matcher.toMatchResult();
    Map<String, String> groups = matcher.namedGroups().keySet().stream()
            .collect(Collectors.toUnmodifiableMap(
                    Function.identity(), matcher::group));

    System.out.println(groups); // {date=2023-06-05, user=johndoe123}
}

相关问题