webentwicklung-frage-antwort-db.com.de

Wie erstelle ich einen Stream von Regex-Übereinstimmungen?

Ich versuche, die Standardeingaben zu analysieren und jede Zeichenfolge zu extrahieren, die einem bestimmten Muster entspricht, die Anzahl der Vorkommen jeder Übereinstimmung zu zählen und die Ergebnisse alphabetisch auszudrucken. Dieses Problem scheint eine gute Entsprechung für die Streams-API zu sein, aber ich kann keine prägnante Methode zum Erstellen eines Match-Streams finden.

Ich konnte dieses Problem umgehen, indem ich einen Iterator für die Übereinstimmungen implementierte und ihn in einen Stream einhüllte, aber das Ergebnis ist nicht sehr lesbar. Wie kann ich einen Stream von Regex-Übereinstimmungen erstellen, ohne zusätzliche Klassen einzuführen?

public class PatternCounter
{
    static private class MatcherIterator implements Iterator<String> {
        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.!#$%&’*+/=?^_`{|}~-][email protected][a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)");

        new TreeMap<String, Long>(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);
        });
    }
}
21
Alfredo Diaz

In Java 8 gibt es Pattern.splitAsStream , das einen durch ein delimiter -Muster getrennten Datenstrom bereitstellt, aber leider keine Unterstützungsmethode für das Abrufen eines Streams von match.

Wenn Sie eine solche Stream implementieren möchten, empfehle ich, Spliterator direkt zu implementieren, anstatt eine Iterator zu implementieren und einzubinden. Sie sind vielleicht mit Iterator besser vertraut, die Implementierung einer einfachen Spliterator ist jedoch unkompliziert:

final class MatchItr extends Spliterators.AbstractSpliterator<String> {
    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 forEachRemaining jedoch mit einer direkten Schleife überschreiben.


Wenn ich Ihren Versuch richtig verstehe, sollte die Lösung folgendermaßen aussehen:

Pattern pattern = Pattern.compile(
                 "[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-][email protected][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<MatchResult> results() direkt auf der Matcher. Um jedoch Übereinstimmungen innerhalb eines Streams zu finden, gibt es eine noch bequemere Methode für Scanner . Damit vereinfacht sich die Implementierung zu

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 Back-Port von Scanner.findAll, der mit Java 8 verwendet werden kann.

24
Holger

Ausgehend von der Lösung von Holger können wir beliebige Matcher-Operationen unterstützen (z. B. das Abrufen der n th-Gruppe), indem der Benutzer eine Function<Matcher, String>-Operation bereitstellt. Wir können die Spliterator auch als Implementierungsdetail ausblenden, so dass Anrufer direkt mit der Stream arbeiten können. Als Faustregel sollte StreamSupport von Bibliothekscode und nicht von Benutzern verwendet werden.

public class MatcherStream {
  private MatcherStream() {}

  public static Stream<String> find(Pattern pattern, CharSequence input) {
    return findMatches(pattern, input).map(MatchResult::group);
  }

  public static Stream<MatchResult> findMatches(
      Pattern pattern, CharSequence input) {
    Matcher matcher = pattern.matcher(input);

    Spliterator<MatchResult> spliterator = new Spliterators.AbstractSpliterator<MatchResult>(
        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 so verwenden:

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

Oder für Ihre spezifische Aufgabe (wieder bei Holger ausleihen):

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));
}
4
dimo414

Wenn Sie eine Scanner zusammen mit regulären Ausdrücken mit der findWithinHorizon-Methode verwenden möchten, können Sie auch einen regulären Ausdruck in einen Stream von Strings konvertieren. Hier verwenden wir einen Stream-Builder, der sehr praktisch in einer while-Schleife ist.

Hier ist ein Beispiel:

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