2015-06-30 54 views
2

我想使用Java 8流獲取字符串流(例如從純文本文件讀取)並生成一串句子。我假設句子可以跨越界限。Java 8句子流

因此,舉例來說,我想去來自:

"This is the", "first sentence. This is the", "second sentence." 

到:

"This is the first sentence.", "This is the second sentence." 

我可以看到,它可能得到句子的一部分流如下:

Pattern p = Pattern.compile("\\."); 
Stream<String> lines 
    = Stream.of("This is the", "first sentence. This is the", "second sentence."); 

Stream<String> result = lines.flatMap(s -> p.splitAsStream(s)); 

但後來我不知道如何產生一個流將片段加入句子。我想以懶惰的方式做到這一點,以便只讀取原始流所需的內容。有任何想法嗎?

+2

是否有必要使用正則表達式?你有沒有嘗試使用StringBuilder和String.split? – 10101010

回答

4

將文本分解爲句子並不像尋找點那麼簡單。例如,你不想在「史密斯先生」之間劃分...

謝天謝地,已經有一個JRE類來照顧那個,BreakIterator。它什麼都沒有,就是Stream支持,因此爲了與流使用它,它周圍的一些支持代碼是必需的:

public class SentenceStream extends Spliterators.AbstractSpliterator<String> 
implements Consumer<CharSequence> { 

    public static Stream<String> sentences(Stream<? extends CharSequence> s) { 
     return StreamSupport.stream(new SentenceStream(s.spliterator()), false); 
    } 
    Spliterator<? extends CharSequence> source; 
    CharBuffer buffer; 
    BreakIterator iterator; 

    public SentenceStream(Spliterator<? extends CharSequence> source) { 
     super(Long.MAX_VALUE, ORDERED|NONNULL); 
     this.source = source; 
     iterator=BreakIterator.getSentenceInstance(Locale.ENGLISH); 
     buffer=CharBuffer.allocate(100); 
     buffer.flip(); 
    } 

    @Override 
    public boolean tryAdvance(Consumer<? super String> action) { 
     for(;;) { 
      int next=iterator.next(); 
      if(next!=BreakIterator.DONE && next!=buffer.limit()) { 
       action.accept(buffer.subSequence(0, next-buffer.position()).toString()); 
       buffer.position(next); 
       return true; 
      } 
      if(!source.tryAdvance(this)) { 
       if(buffer.hasRemaining()) { 
        action.accept(buffer.toString()); 
        buffer.position(0).limit(0); 
        return true; 
       } 
       return false; 
      } 
      iterator.setText(buffer.toString()); 
     } 
    } 

    @Override 
    public void accept(CharSequence t) { 
     buffer.compact(); 
     if(buffer.remaining()<t.length()) { 
      CharBuffer bigger=CharBuffer.allocate(
       Math.max(buffer.capacity()*2, buffer.position()+t.length())); 
      buffer.flip(); 
      bigger.put(buffer); 
      buffer=bigger; 
     } 
     buffer.append(t).flip(); 
    } 
} 

與支持類,你可以簡單地說,如:

Stream<String> lines = Stream.of(
    "This is the ", "first sentence. This is the ", "second sentence."); 
sentences(lines).forEachOrdered(System.out::println); 
+0

這是一個有趣的解決方案。感謝那! 我猜測可以通過一些調整來處理第一行缺少最終空格的問題(「這是」而不是「這就是」),但是您的建議看起來很有幫助。 – Adam

+0

爲每個字符串添加隱含的空間很容易。這只是解決方案的語義應該如何的問題。我把它寫成「像一個大字符串或像'flatMap(String :: chars)'」那樣處理一串字符串,即沒有將其他字符添加到字符串中。唯一需要改變的是if(buffer.remaining()<= t.length())'(注意'<=')和'buffer.append(t).append('').flip(); '... – Holger

3

這是一個連續的,有狀態的問題,Stream的設計師不太喜歡。

從更一般的意義上說,您正在實現一個詞法分析器,它將令牌序列轉換爲另一種令牌序列。雖然你可能使用Stream來解決它與技巧和黑客,真的沒有理由。僅僅因爲Stream在那裏並不意味着我們必須將它用於一切。

這就是說,你的問題的答案是使用flatMap()與一個有狀態的函數,它保存中間數據,並在遇到點時發出整個句子。還有EOF的問題 - 您需要源流中EOF的定位值,以便該功能可以對其作出反應。

2

我的StreamEx庫有一個collapse方法,旨在解決這樣的任務。首先,讓我們改變你的正則表達式來查找後面一個,留下的結束點,所以我們以後可以使用它們:

StreamEx.of(input).flatMap(Pattern.compile("(?<=\\.)")::splitAsStream) 

這裏input是數組,列表,JDK流或只是逗號分隔的字符串。

接下來我們摺疊兩個字符串,如果第一個不以點結束。合併功能應加入這兩個部分爲單一字符串添加它們之間的空間:

.collapse((a, b) -> !a.endsWith("."), (a, b) -> a + ' ' + b) 

最後,我們應該修剪開頭和結尾的空格如果有的話:

.map(String::trim); 

整個代碼是在這裏:

List<String> lines = Arrays.asList("This is the", "first sentence. This is the", 
    "second sentence. Third sentence. Fourth", "sentence. Fifth sentence.", "The last"); 
Stream<String> stream = StreamEx.of(lines) 
     .flatMap(Pattern.compile("(?<=\\.)")::splitAsStream) 
     .collapse((a, b) -> !a.endsWith("."), (a, b) -> a + ' ' + b) 
     .map(String::trim); 
stream.forEach(System.out::println); 

的輸出是下面的:

This is the first sentence. 
This is the second sentence. 
Third sentence. 
Fourth sentence. 
Fifth sentence. 
The last 

更新:自StreamEx 0.3.4版本以來,您可以安全地使用並行流執行相同操作。

+0

謝謝。你的「崩潰」想法聽起來很有用。 – Adam