2015-10-04 10 views
7

Ho un file di testo che contiene molte linee di stringa. Se voglio trovare linee prima e dopo un corrispondente in grep, farò così:Come ottenere linee prima e dopo la corrispondenza da java 8 stream come grep?

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

Come posso implementa l'equivalente in Java 8 usando flusso?

+0

che non è, purtroppo, supportati dalle API flusso fuori dalla scatola, ma ciò che si desidera è chiamato un "finestra scorrevole". –

risposta

2

Tale scenario non è ben supportato da Stream API poiché i metodi esistenti non forniscono un accesso ai vicini elemento nello stream. La soluzione più vicina che posso pensare senza creare iteratori custom/spliterators e chiamate di libreria di terze parti è quello di leggere il file di input in List e quindi utilizzare gli indici 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); 

L'output grep comprende anche delimitatore speciale come "--" per designare le linee omesse. Se si vuole andare oltre e imitare tali comportamenti così, posso suggerire di provare la mia libera StreamEx biblioteca come ha intervalMap metodo che è utile in questo caso:

// 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

Come osservato Tagir Valeev, questo tipo di il problema non è ben supportato dall'API dei flussi. Se si desidera leggere in modo incrementale le righe dall'input e stampare le linee corrispondenti con il contesto, è necessario introdurre uno stage stateful pipeline (o un collector o uno splitterator personalizzato) che aggiunge un po 'di complessità.

Se si desidera leggere tutte le righe in memoria, risulta che BitSet è una rappresentazione utile per la manipolazione di gruppi di corrispondenze. Questo ha qualche somiglianza con la soluzione di Tagir, ma invece di usare intervalli di numeri interi per rappresentare linee da stampare, usa 1 bit in uno BitSet. Alcuni vantaggi di BitSet sono che ha un numero di operazioni di massa incorporate e ha una rappresentazione interna compatta. Può anche produrre un flusso di indici di 1-bit, che è abbastanza utile per questo problema.

In primo luogo, cominciamo con la creazione di un BitSet che ha un 1-bit per ogni riga che corrisponde al predicato:

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); 

Ora che abbiamo impostato il bit di linee di corrispondenza, Trasmettiamo gli indici di ogni 1 bit. Quindi impostiamo i bit nel bitset che rappresenta il contesto precedente e successivo. Questo ci dà un singolo BitSet i cui 1 bit rappresentano tutte le linee da stampare, comprese le linee di contesto.

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

Se vogliamo solo per stampare tutte le linee, tra cui contesto, siamo in grado di fare questo:

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

L'attuale grep -A a -B b comando stampa un separatore tra ogni gruppo di righe di contesto. Per capire quando stampare un separatore, guardiamo ogni 1 bit nel bit di contesto impostato. Se c'è un 0 bit che lo precede, o se è all'inizio, abbiamo impostato un po 'nel risultato. Questo ci dà un 1-bit all'inizio di ogni gruppo di righe di contesto:

Non vogliamo stampare il separatore prima di ogni gruppo di righe di contesto; vogliamo stamparlo tra ogni gruppo.Ciò significa che dobbiamo cancellare il primo 1 bit (se presente):

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

Ora, possiamo stampare le righe dei risultati. Ma prima di stampare ogni linea, controlliamo per vedere se dobbiamo stampare un separatore prima:

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

Approccio interessante, potenziato. Un'altra alternativa è quella di unire i primi due passi insieme prendendo 'IntStream.range (..). Filter (..). FlatMap (..). Filter (..)' passi dalla mia soluzione, quindi '.collect (BitSet :: nuovo, BitSet :: set, BitSet :: or) 'invece di' .distinct(). sorted() '. Ciò preserverebbe l'efficienza della memoria, mentre potrebbe sembrare più "streamy". Btw 'i> 0 &&! Context.get (i-1) || i == 0' potrebbe essere abbreviato in 'i == 0 || ! Context.get (i-1) '. –

+2

Ho semplificato il tuo passaggio intermedio. Spero non ti dispiaccia che l'ho modificato direttamente; sembrava troppo complicato per un commento per me, anche se facile da capire nel suo contesto. – Holger

+0

@TagirValeev Buon suggerimento nel tuo "btw". Avevo aggiunto il caso "i == 0" dopo aver raccolto quel caso limite e non avevo notato la semplificazione che poteva essere fatta. Modificato. –

4

Se siete disposti a utilizzare una libreria di terze parti e non hanno bisogno di parallelismo, poi jOOλ offre finestra di stile SQL funziona come segue

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

snervamento

-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:  }); 

la funzione lead() accede al valore successivo ordine di attraversamento dalla finestra, il 0 La funzioneaccede alla riga precedente.

Disclaimer: io lavoro per l'azienda dietro jOOλ

Problemi correlati