2015-05-13 12 views
13

Ho un file di testo che contiene URL ed e-mail. Ho bisogno di estrarli tutti dal file. Ogni URL ed email possono essere trovati più di una volta, ma il risultato non dovrebbe contenere duplicati. posso estrarre tutti gli URL utilizzando il seguente codice:Split java.util.stream.Stream

Files.lines(filePath). 
    .map(urlPattern::matcher) 
    .filter(Matcher::find) 
    .map(Matcher::group) 
    .distinct(); 

posso estrarre tutti i messaggi di posta elettronica utilizzando il seguente codice:

Files.lines(filePath). 
    .map(emailPattern::matcher) 
    .filter(Matcher::find) 
    .map(Matcher::group) 
    .distinct(); 

Posso estrarre tutti gli URL e email la lettura del flusso restituito da Files.lines(filePath) un solo tempo? Qualcosa come suddividere il flusso di righe per lo streaming di URL e il flusso di e-mail.

+0

'flusso FILESTREAM = Files.lines (Paths.get ("test")); \t \t fileStream.//email partita \t \t fileStream.//url match' L'unica soluzione che mi viene in mente, quando il problema è che non si vuole creare 2 flussi – Loki

+2

immagino, memorizzare le linee in 'List' e attraversandolo due volte non è qualificato come soluzione, corretto? –

+6

Loki, non è possibile attraversare lo stesso flusso due volte. –

risposta

10

È possibile utilizzare il collettore partitioningBy, sebbene non sia ancora una soluzione molto elegante.

Map<Boolean, List<String>> map = Files.lines(filePath) 
     .filter(str -> urlPattern.matcher(str).matches() || 
         emailPattern.matcher(str).matches()) 
     .distinct() 
     .collect(Collectors.partitioningBy(str -> urlPattern.matcher(str).matches())); 
List<String> urls = map.get(true); 
List<String> emails = map.get(false); 

Se non si desidera applicare regexp due volte, si può fare utilizzando l'oggetto coppia intermedia (per esempio, SimpleEntry):

public static String classify(String str) { 
    return urlPattern.matcher(str).matches() ? "url" : 
     emailPattern.matcher(str).matches() ? "email" : null; 
} 

Map<String, Set<String>> map = Files.lines(filePath) 
     .map(str -> new AbstractMap.SimpleEntry<>(classify(str), str)) 
     .filter(e -> e.getKey() != null) 
     .collect(Collectors.groupingBy(e -> e.getKey(), 
      Collectors.mapping(e -> e.getValue(), Collectors.toSet()))); 

Usando il mio libero StreamEx libreria l'ultimo passo sarebbe essere più breve:

Map<String, Set<String>> map = StreamEx.of(Files.lines(filePath)) 
     .mapToEntry(str -> classify(str), Function.identity()) 
     .nonNullKeys() 
     .grouping(Collectors.toSet()); 
+0

Ho modificato la mia risposta, aggiungendo chiamata 'matcher.group (1)' per estrarre URL o e-mail dalla stringa. Sarebbe fantastico, se lo aggiungi al tuo codice, diventa corretto. –

+1

La domanda usa '.distinct()' dopo il filtraggio che suggerisce che la raccolta in 'Set's piuttosto che in' List's è più appropriata. Generalmente, il metodo 'classify' è una buona idea, rendendo più semplice l'uso di' Collector's piuttosto che implementare un 'Collector 'personalizzato (come ho fatto io) – Holger

+1

@ york.beta: non ha senso usare' group (1) "a patto che si utilizzi' matches' in quanto ciò implica che l'intero 'String' corrisponde. Sarebbe diverso se tu usassi 'find', ma sarebbe una domanda completamente diversa in quanto implica la possibilità che entrambi i pattern trovino una corrispondenza all'interno della stessa linea ... – Holger

1

Dato che non è possibile riutilizzare un flusso, l'unica opzione sarebbe "farlo manualmente", penso.

File.lines(filePath).forEach(s -> /** match and sort into two lists */); 

Se c'è un'altra soluzione per questo, sarei felice di informarlo!

+0

Sì, ci ho pensato, sono curioso che ci sia un'altra soluzione, quindi ho risposto a questa domanda. –

0

La domanda complessiva dovrebbe essere: perché si vuole eseguire lo streaming solo una volta?

L'estrazione degli URL e l'estrazione delle e-mail sono operazioni diverse e pertanto devono essere gestite nelle proprie operazioni di streaming. Anche se la sorgente del flusso sottostante contiene centinaia di migliaia di record, il tempo per l'iterazione può essere trascurato rispetto alle operazioni di mappatura e filtraggio.

L'unica cosa da considerare come possibile problema di prestazioni è l'operazione IO. La soluzione più pulita è quindi quello di leggere il file una sola volta e poi lo streaming su una collezione risultante due volte:

List<String> allLines = Files.readAllLines(filePath); 
allLines.stream() ... // here do the URLs 
allLines.stream() ... // here do the emails 

Naturalmente questo richiede un po 'di memoria.

+0

A volte potrebbe essere ragionevole farlo in un solo passaggio. Ad esempio, il file di input contiene milioni di righe di cui solo un piccolo bit soddisfa le espressioni regolari. –

+0

L'estrazione di URL ed e-mail è solo un esempio, mi piacerebbe estrarre un altro dato da alcuni file enormi. Quindi leggerli a memoria o leggerli alcune volte non sono soluzioni. –

4

È possibile eseguire l'abbinamento all'interno di un Collector:

Map<String,Set<String>> map=Files.lines(filePath) 
    .collect(HashMap::new, 
     (hm,line)-> { 
      Matcher m=emailPattern.matcher(line); 
      if(m.matches()) 
       hm.computeIfAbsent("mail", x->new HashSet<>()).add(line); 
      else if(m.usePattern(urlPattern).matches()) 
       hm.computeIfAbsent("url", x->new HashSet<>()).add(line); 
     }, 
     (m1,m2)-> m2.forEach((k,v)->m1.merge(k, v, 
            (s1,s2)->{s1.addAll(s2); return s1;})) 
    ); 
Set<String> mail=map.get("mail"), url=map.get("url"); 

Nota che questo può essere facilmente adattato per trovare più corrispondenze all'interno di una riga:

Map<String,Set<String>> map=Files.lines(filePath) 
    .collect(HashMap::new, 
     (hm,line)-> { 
      Matcher m=emailPattern.matcher(line); 
      while(m.find()) 
       hm.computeIfAbsent("mail", x->new HashSet<>()).add(m.group()); 
      m.usePattern(urlPattern).reset(); 
      while(m.find()) 
       hm.computeIfAbsent("url", x->new HashSet<>()).add(m.group()); 
     }, 
     (m1,m2)-> m2.forEach((k,v)->m1.merge(k, v, 
            (s1,s2)->{s1.addAll(s2); return s1;})) 
    );