2015-10-04 6 views
7

Ich habe eine Textdatei, die viele Zeichenketten enthält. Wenn ich Zeilen finden wollen vor und nach einem passenden in grep, werde ich tun, wie folgt:Wie bekomme ich Zeilen vor und nach dem Abgleich von Java 8 Stream wie Grep?

grep -A 10 -B 10 "ABC" myfile.txt 

Wie kann ich implementiert das Äquivalent in Java 8 Strom mit?

+0

, die leider ist nicht von dem Stream-API aus der Box unterstützt, aber was Sie wollen, ist ein „Schiebefenster“ genannt. –

Antwort

2

Ein solches Szenario wird von der Stream-API nicht gut unterstützt, da vorhandene Methoden keinen Zugriff auf die Elementnachbarn im Stream bieten. Die nächste Lösung, die ich ausdenken kann, ohne individuelle Iteratoren Erstellung/spliterators und Drittanbieter-Bibliothek Anrufe ist die Eingabedatei in List zu lesen und dann verwenden Indizes Stream:

List<String> input = Files.readAllLines(Paths.get(fileName)); 
Predicate<String> pred = str -> str.contains("ABC"); 
int contextLength = 10; 

IntStream.range(0, input.size()) // line numbers 
    // filter them leaving only numbers of lines satisfying the predicate 
    .filter(idx -> pred.test(input.get(idx))) 
    // add nearby numbers 
    .flatMap(idx -> IntStream.rangeClosed(idx-contextLength, idx+contextLength)) 
    // remove numbers which are out of the input range 
    .filter(idx -> idx >= 0 && idx < input.size()) 
    // sort numbers and remove duplicates 
    .distinct().sorted() 
    // map to the lines themselves 
    .mapToObj(input::get) 
    // output 
    .forEachOrdered(System.out::println); 

Die grep Ausgabe enthält auch spezielle Trennzeichen wie "--" um die weggelassenen Linien zu bezeichnen. Wenn Sie weiter gehen wollen und ein solches Verhalten zu imitieren als auch, kann ich vorschlagen, dass Sie meine freie StreamEx Bibliothek, um zu versuchen, wie es intervalMap Methode hat, die in diesem Fall hilfreich ist:

// Same as IntStream.range(...).filter(...) steps above 
IntStreamEx.ofIndices(input, pred) 
    // same as above 
    .flatMap(idx -> IntStream.rangeClosed(idx-contextLength, idx+contextLength)) 
    // remove numbers which are out of the input range 
    .atLeast(0).less(input.size()) 
    // sort numbers and remove duplicates 
    .distinct().sorted() 
    .boxed() 
    // merge adjacent numbers into single interval and map them to subList 
    .intervalMap((i, j) -> (j - i) == 1, (i, j) -> input.subList(i, j + 1)) 
    // flatten all subLists prepending them with "--" 
    .flatMap(list -> StreamEx.of(list).prepend("--")) 
    // skipping first "--" 
    .skip(1) 
    .forEachOrdered(System.out::println); 
1

Wie Tagir Valeev erwähnt, diese Art von Problem wird von der Stream-API nicht gut unterstützt. Wenn Sie inkrementell Zeilen aus der Eingabe lesen und übereinstimmende Zeilen mit Kontext drucken möchten, müssen Sie eine Stateful-Pipelinestufe (oder einen benutzerdefinierten Collector oder Spliterator) einführen, was ein wenig Komplexität mit sich bringt.

Wenn Sie bereit sind, alle Zeilen in den Speicher zu lesen, stellt sich heraus, dass BitSet eine nützliche Darstellung für das Manipulieren von Übereinstimmungsgruppen ist. Dies hat eine gewisse Ähnlichkeit mit Tagirs Lösung, aber anstatt Ganzzahlbereiche zu verwenden, um zu druckende Zeilen darzustellen, verwendet es 1-Bits in einer BitSet. Einige Vorteile von BitSet sind, dass es eine Reihe von integrierten Massenoperationen hat, und es hat eine kompakte interne Darstellung. Es kann auch einen Strom von Indizes der 1-Bits erzeugen, was für dieses Problem ziemlich nützlich ist.

Zuerst lassen Sie uns beginnen, indem sie einen BitSet schaffen, die ein 1-Bit für jede Zeile hat, die das Prädikat übereinstimmt:

void contextMatch(Predicate<String> pred, int before, int after, List<String> input) { 
    int len = input.size(); 
    BitSet matches = IntStream.range(0, len) 
           .filter(i -> pred.test(input.get(i))) 
           .collect(BitSet::new, BitSet::set, BitSet::or); 

Nun, da wir das Bit Reihe von passenden Linien haben wir die Indizes ausströmen von jedem 1-Bit. Dann setzen wir die Bits im Bitset, die den Vorher-Nachher-Kontext darstellen. Dies gibt uns eine einzige BitSet, deren 1-Bit alle Zeilen einschließlich der Kontextlinien darstellen.

BitSet context = matches.stream() 
     .collect(BitSet::new, 
       (bs,i) -> bs.set(Math.max(0, i - before), Math.min(i + after + 1, len)), 
       BitSet::or); 

Wenn wir nur die Linien alle drucken möchten, einschließlich Kontext, können wir dies tun:

context.stream() 
      .forEachOrdered(i -> System.out.println(input.get(i))); 

Die tatsächlichen grep -A a -B b Befehl druckt eine Trenneinrichtung zwischen jeder Gruppe von Kontextzeilen. Um herauszufinden, wann ein Trennzeichen gedruckt werden soll, betrachten wir jedes 1-Bit im Kontext-Bit-Set. Wenn ein 0-Bit davor steht, oder wenn es ganz am Anfang steht, setzen wir ein bisschen in das Ergebnis. Dies gibt uns einen 1-Bit am Anfang jeder Gruppe von Kontextzeilen:

Wir wollen nicht vor jeder Gruppe von Kontextzeilen des Separator drucken; wir wollen es zwischen jeder Gruppe drucken.Das heißt, wir müssen das erste 1-Bit löschen (falls vorhanden):

// clear the first bit 
    int first = separators.nextSetBit(0); 
    if (first >= 0) { 
     separators.clear(first); 
    } 

Jetzt können wir die Ergebniszeilen ausdrucken. Aber vor jeder Zeile gedruckt wird, überprüfen wir, ob wir einen Separator zuerst gedruckt werden soll:

context.stream() 
      .forEachOrdered(i -> { 
       if (separators.get(i)) { 
        System.out.println("--"); 
       } 
       System.out.println(input.get(i)); 
      }); 
} 
+0

Interessanter Ansatz, upvoted. Eine andere Alternative besteht darin, die ersten beiden Schritte zusammen zu führen, indem man 'IntStream.range (..). Filter (..). FlatMap (..). Filter (..)' Schritte von meiner Lösung und dann '.collect (BitSet :: new, BitSet :: set, BitSet :: or) 'anstelle von' .distinct(). sorted() '. Dies würde die Speichereffizienz erhalten, während es "stromreicher" aussehen könnte. Btw 'i> 0 &&! Kontext.get (i-1) || i == 0 'könnte zu' i == 0 || verkürzt werden ! context.get (i-1) '. –

+2

Ich habe Ihren Zwischenschritt vereinfacht. Ich hoffe, es macht dir nichts aus, dass ich es direkt bearbeitet habe; Es sah für einen Kommentar zu kompliziert aus, während es in seinem Kontext einfach zu verstehen war. – Holger

+0

@TagirValeev Guter Vorschlag in Ihrem "BTW". Ich hatte nachher den 'i == 0'-Fall hinzugefügt, um diesen Randfall aufzuheben, und ich bemerkte nicht die Vereinfachung, die gemacht werden konnte. Bearbeitet. –

4

Wenn Sie bereit sind, eine dritte Partei-Bibliothek zu verwenden und keine Parallelität benötigen, dann bietet jOOλ SQL-Stil Fenster Funktionen wie

Seq.seq(Files.readAllLines(Paths.get(new File("/path/to/Example.java").toURI()))) 
    .window(-1, 1) 
    .filter(w -> w.value().contains("ABC")) 
    .forEach(w -> { 
     System.out.println("-1:" + w.lag().orElse("")); 
     System.out.println(" 0:" + w.value()); 
     System.out.println("+1:" + w.lead().orElse("")); 
     // ABC: Just checking 
    }); 

Nachgeben

-1:  .window(-1, 1) 
0:  .filter(w -> w.value().contains("ABC")) 
+1:  .forEach(w -> { 
-1:   System.out.println("+1:" + w.lead().orElse("")); 
0:   // ABC: Just checking 
+1:  }); 

lead() die Funktion greift auf den nächsten Wert in Durchlauf-Reihenfolge von dem Fenster folgt, das 0 Die Funktiongreift auf die vorherige Zeile zu.

Haftungsausschluss: Ich arbeite für die Firma, die hinter jOOλ