2015-01-26 4 views
19

Ich versuche, die 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.Wie erstelle ich einen Stream von Regex-Treffern?

Ich habe dieses Problem gelöst, 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 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); 
     }); 
    } 
} 
+5

in Java 9, wird es eine Methode Matcher sein .Ergebnisse. siehe http://download.java.net/jdk9/docs/api/java/util/regex/Matcher.html#results-- – user140547

+1

sieht aus wie [Java 9 URI hat sich geändert] (http://download.java .net/java/jdk9/docs/api/java/util/regex/Matcher.html # results--) – Gary

Antwort

20

Nun, in Java 8, gibt es Pattern.splitAsStream, die einen Strom von Gegenständen durch einen Begrenzer Muster aber leider keine Unterstützung Methode für das Erhalten eines Stroms von Matches aufgeteilt bieten.

Wenn Sie eine solche Stream implementieren, empfehle ich, Spliterator direkt implementieren und implementieren und wickeln Sie eine Iterator. Sie können mit Iterator besser vertraut sein, aber einem einfachen Spliterator Implementierung ist straight-forward:

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 prüfen, forEachRemaining mit einer Straight-Forward-Schleife überschreiben, though.


Wenn ich Ihren Versuch verstehen, die Lösung sollte richtig, eher wie:

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 stellt ein Verfahren Stream<MatchResult> results() direkt am Matcher. Aber um Übereinstimmungen in einem Stream zu finden, gibt es an even more convenient method on 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)); 
} 

This answer enthält einen Back-Port von Scanner.findAll, die mit Java verwendet werden kann 8.

+0

Sie können auch die NONNULL-Eigenschaft hinzufügen. Ich bin mir nicht sicher, ob du IMMUTABLE hinzufügen kannst oder nicht; Die Matcher-Dokumentation ist nicht eindeutig, wenn das zugrundeliegende CharSequence-Objekt (das möglicherweise StringBuilder ist) während der Übereinstimmungsergebnisse in einem definierten Verhalten geändert wird. –

+0

@ Jeffrey: in der Tat, 'NONNULL' ist möglich,' IMMUTABLE' könnte angegeben werden, wenn die Quelle eine 'String' * ist und * Sie die volle Kontrolle über den' Matcher' haben, da die 'Matcher's Eigenschaften nicht geändert werden dürfen so (vor allem seine Quelle), aber die Angabe dieser Flags ist nicht so wichtig wie derzeit, niemand nutzt diese Flags ... – Holger

+0

"leider keine Methode zum Abrufen eines Streams von Übereinstimmungen." Ich habe diese Unterlassung nie verstanden. Die Java-Designer müssen etwas dagegen haben, aber wer weiß was es ist. Aufteilen ist nicht dasselbe, da leere Strings am Anfang des Match-Arrays üblich sind. Seufzer. –

3

abgehend von Holger-Lösung, können wir beliebige Matcher Operationen unterstützen (wie immer die n Gruppe), indem der Benutzer eine Function<Matcher, String> Operation zur Verfügung stellt. Wir können auch die Spliterator als ein Implementierungsdetail verstecken, so dass Anrufer einfach mit der Stream direkt arbeiten können. Als Faustregel sollte StreamSupport vom 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 wie so verwenden:

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

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

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)); 
} 
+1

besser, um es einfach zu einem 'Stream ' Ich denke. Sie wollen keine Funktionen zulassen, die das 'Match' mutieren, und dann können Sie es einem 'Stream ' zuordnen, um OP zu erfüllen, indem Sie 'Stream :: map' verwenden. –

+1

find() sollte nicht nur CharSequence-Parameter akzeptieren String –

+0

@PatrickParker gute Vorschläge, hatte ich 'MatchResult' nicht bemerkt. – dimo414

Verwandte Themen