Wie erstelle ich einen Stream von Regex-Treffern?

Ich versuche, Standardeingabe zu analysieren und jede Zeichenfolge zu extrahieren, die mit einem bestimmten Muster übereinstimmt, zähle die Anzahl der Vorkommen jeder Übereinstimmung und drucke die Ergebnisse alphabetisch. Dieses Problem scheint eine gute Übereinstimmung mit der Streams-API zu sein, aber ich kann keinen übersichtlichen Weg finden, um einen Match-Stream von einem Matcher zu erstellen.

Ich habe dieses Problem getriggers, indem ich einen Iterator über die Übereinstimmungen implementierte und ihn in einen Stream verpackte, aber das Ergebnis ist nicht gut lesbar. Wie kann ich einen Stream von Regex-Matches erstellen, ohne zusätzliche classn einzuführen?

public class PatternCounter { static private class MatcherIterator implements Iterator { private final Matcher matcher; public MatcherIterator(Matcher matcher) { this.matcher = matcher; } public boolean hasNext() { return matcher.find(); } public String next() { return matcher.group(0); } } static public void main(String[] args) throws Throwable { Pattern pattern = Pattern.compile("[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)"); new TreeMap(new BufferedReader(new InputStreamReader(System.in)) .lines().map(line -> { Matcher matcher = pattern.matcher(line); return StreamSupport.stream( Spliterators.spliteratorUnknownSize(new MatcherIterator(matcher), Spliterator.ORDERED), false); }).reduce(Stream.empty(), Stream::concat).collect(groupingBy(o -> o, counting())) ).forEach((k, v) -> { System.out.printf("%s\t%s\n",k,v); }); } } 

Nun, in Java 8 gibt es Pattern.splitAsStream das einen Strom von Elementen bereitstellt, die durch ein Delimiter- Muster getrennt sind, aber leider keine Methode, um einen Stream von Übereinstimmungen zu erhalten .

Wenn Sie einen solchen Stream implementieren, empfehle ich, Spliterator direkt zu implementieren, anstatt einen Iterator implementieren und zu verpacken. Sie sind vielleicht mit Iterator besser vertraut, aber die Implementierung eines einfachen Spliterator ist einfach:

 final class MatchItr extends Spliterators.AbstractSpliterator { private final Matcher matcher; MatchItr(Matcher m) { super(m.regionEnd()-m.regionStart(), ORDERED|NONNULL); matcher=m; } public boolean tryAdvance(Consumer< ? super String> action) { if(!matcher.find()) return false; action.accept(matcher.group()); return true; } } 

Sie können jedoch forEachRemaining mit einer geradlinigen Schleife überschreiben.


Wenn ich Ihren Versuch richtig verstehe, sollte die Lösung eher wie folgt aussehen:

 Pattern pattern = Pattern.compile( "[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)"); try(BufferedReader br=new BufferedReader(System.console().reader())) { br.lines() .flatMap(line -> StreamSupport.stream(new MatchItr(pattern.matcher(line)), false)) .collect(Collectors.groupingBy(o->o, TreeMap::new, Collectors.counting())) .forEach((k, v) -> System.out.printf("%s\t%s\n",k,v)); } 

Java 9 bietet eine Methode Stream results() direkt auf dem Matcher . Aber um Übereinstimmungen in einem Stream zu finden, gibt es eine noch bequemere Methode für den Scanner . Damit vereinfacht sich die Implementierung

 try(Scanner s = new Scanner(System.console().reader())) { s.findAll(pattern) .collect(Collectors.groupingBy(MatchResult::group,TreeMap::new,Collectors.counting())) .forEach((k, v) -> System.out.printf("%s\t%s\n",k,v)); } 

Diese Antwort enthält einen Zurück-Port von Scanner.findAll , der mit Java 8 verwendet werden kann.

Ausgehend von Holgers Lösung können wir beliebige Matcher Operationen unterstützen (wie zum Beispiel die n- te Gruppe), indem der Benutzer eine Function . Wir können den Spliterator als Implementierungsdetail ausblenden, damit Anrufer einfach direkt mit dem Stream können. Als Faustregel sollte StreamSupport vom Bibliothekscode und nicht von Benutzern verwendet werden.

 public class MatcherStream { private MatcherStream() {} public static Stream find(Pattern pattern, CharSequence input) { return findMatches(pattern, input).map(MatchResult::group); } public static Stream findMatches( Pattern pattern, CharSequence input) { Matcher matcher = pattern.matcher(input); Spliterator spliterator = new Spliterators.AbstractSpliterator( Long.MAX_VALUE, Spliterator.ORDERED|Spliterator.NONNULL) { @Override public boolean tryAdvance(Consumer< ? super MatchResult> action) { if(!matcher.find()) return false; action.accept(matcher.toMatchResult()); return true; }}; return StreamSupport.stream(spliterator, false); } } 

Sie können es dann wie folgt verwenden:

 MatcherStream.find(Pattern.compile("\\w+"), "foo bar baz").forEach(System.out::println); 

Oder für Ihre spezielle Aufgabe (erneut von Holger leihen):

 try(BufferedReader br = new BufferedReader(System.console().reader())) { br.lines() .flatMap(line -> MatcherStream.find(pattern, line)) .collect(Collectors.groupingBy(o->o, TreeMap::new, Collectors.counting())) .forEach((k, v) -> System.out.printf("%s\t%s\n", k, v)); } 

Wenn Sie einen Scanner zusammen mit regulären Ausdrücken mit der Methode findWithinHorizon verwenden findWithinHorizon , können Sie auch einen regulären Ausdruck in einen String-String konvertieren. Hier verwenden wir einen Stream-Builder, der während einer herkömmlichen while Schleife sehr praktisch ist.

Hier ist ein Beispiel:

 private Stream extractRulesFrom(String text, Pattern pattern, int group) { Stream.Builder builder = Stream.builder(); try(Scanner scanner = new Scanner(text)) { while (scanner.findWithinHorizon(pattern, 0) != null) { builder.accept(scanner.match().group(group)); } } return builder.build(); }