[英]Java 8 Streams - Apply fixed amount of predicate filters to single Stream
[英]Java split stream by predicate into stream of streams
我有幾百個大的(6GB)gziped日志文件,我正在使用我想要解析的GZIPInputStream
來閱讀。 假設每個人都有以下格式:
Start of log entry 1
...some log details
...some log details
...some log details
Start of log entry 2
...some log details
...some log details
...some log details
Start of log entry 3
...some log details
...some log details
...some log details
我通過BufferedReader.lines()
逐行傳輸gziped文件內容。 流看起來像:
[
"Start of log entry 1",
" ...some log details",
" ...some log details",
" ...some log details",
"Start of log entry 2",
" ...some log details",
" ...some log details",
" ...some log details",
"Start of log entry 2",
" ...some log details",
" ...some log details",
" ...some log details",
]
每個日志條目的開頭都可以通過謂詞標識: line -> line.startsWith("Start of log entry")
。 我想根據這個謂詞將此Stream<String>
轉換為Stream<Stream<String>>
。 每個“子流”應該在謂詞為真時開始,並在謂詞為假時收集行,直到下一次謂詞為真,表示該子流的結束和下一個的開始。 結果如下:
[
[
"Start of log entry 1",
" ...some log details",
" ...some log details",
" ...some log details",
],
[
"Start of log entry 2",
" ...some log details",
" ...some log details",
" ...some log details",
],
[
"Start of log entry 3",
" ...some log details",
" ...some log details",
" ...some log details",
],
]
從那里,我可以獲取每個子流並通過new LogEntry(Stream<String> logLines)
映射它,以便將相關的日志行聚合到LogEntry
對象中。
這里有一個粗略的概念:
import java.io.*;
import java.nio.charset.*;
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import static java.lang.System.out;
class Untitled {
static final String input =
"Start of log entry 1\n" +
" ...some log details\n" +
" ...some log details\n" +
" ...some log details\n" +
"Start of log entry 2\n" +
" ...some log details\n" +
" ...some log details\n" +
" ...some log details\n" +
"Start of log entry 3\n" +
" ...some log details\n" +
" ...some log details\n" +
" ...some log details";
static final Predicate<String> isLogEntryStart = line -> line.startsWith("Start of log entry");
public static void main(String[] args) throws Exception {
try (ByteArrayInputStream gzipInputStream
= new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)); // mock for fileInputStream based gzipInputStream
InputStreamReader inputStreamReader = new InputStreamReader( gzipInputStream );
BufferedReader reader = new BufferedReader( inputStreamReader )) {
reader.lines()
.splitByPredicate(isLogEntryStart) // <--- What witchcraft should go here?
.map(LogEntry::new)
.forEach(out::println);
}
}
}
約束:我有數百個這樣的大文件要並行處理(但每個文件只有一個連續的流),這使得將它們完全加載到內存中(例如將它們存儲為List<String> lines
)是不可行的。
任何幫助贊賞!
我認為主要的問題是你是逐行閱讀並嘗試從行中創建一個LogEntry
實例,而不是逐塊讀取(可能會覆蓋很多行)。
為此,您可以使用正確的正則表達式使用Scanner.findAll
(自Java 9以來可用):
String input =
"Start of log entry 1\n" +
" ...some log details 1.1\n" +
" ...some log details 1.2\n" +
" ...some log details 1.3\n" +
"Start of log entry 2\n" +
" ...some log details 2.1\n" +
" ...some log details 2.2\n" +
" ...some log details 2.3\n" +
"Start of log entry 3\n" +
" ...some log details 3.1\n" +
" ...some log details 3.2\n" +
" ...some log details 3.3";
try (ByteArrayInputStream gzip =
new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8));
InputStreamReader reader = new InputStreamReader(gzip);
Scanner scanner = new Scanner(reader)) {
String START = "Start of log entry \\d+";
Pattern pattern = Pattern.compile(
START + "(?<=" + START + ").*?(?=" + START + "|$)",
Pattern.DOTALL);
scanner.findAll(pattern)
.map(MatchResult::group)
.map(s -> s.split("\\R"))
.map(LogEntry::new)
.forEach(System.out::println);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
因此,這可以通過懶惰地在Scanner
實例中查找匹配來實現。 Scanner.findAll
返回Stream<MatchResult>
, MatchResult.group()
返回匹配的String
。 然后我們用換行符( \\\\R
)拆分這個字符串。 這將返回一個String[]
其中每個數組元素都是每一行。 然后,假設LogEntry
具有接受String[]
參數的構造函數,我們將這些數組中的每一個轉換為LogEntry
實例。 最后,假設LogEntry
有一個覆蓋toString()
方法,我們將每個LogEntry
實例打印到輸出。
值得一提的是,當在流上調用forEach
時, Scanner
開始工作。
另外一個注釋是我們用於匹配輸入中的日志條目的正則表達式。 我不是正則表達式世界的專家,所以我幾乎可以肯定這里有很大的改進空間。 首先,我們使用Pattern.DOTALL
這樣的.
不僅匹配常見字符,還匹配換行符。 然后,有真正的正則表達式。 這個想法是匹配並消耗Start of log entry \\\\d+
,然后它使用后面 Start of log entry \\\\d+
,然后它以非貪婪的方式消耗來自輸入的字符(這是.*?
部分),最后它看起來反超 ,以檢查是否有另一個occurence Start of log entry \\\\d+
,或者如果輸入的結尾已經達到。 如果你想深入研究這個主題,請參考這篇關於正則表達式的精彩文章 。
如果您不使用Java 9+,我不知道任何類似的替代方案。 但是,您可以創建一個自定義Spliterator
,它包裝由BufferedReader.lines()
返回的流返回的Spliterator
,並Spliterator
添加所需的解析行為。 然后,您需要從此Spliterator
創建一個新的Stream
。 根本不是一項微不足道的任務......
弗雷德里科的答案可能是解決這一特殊問題的最好方法。 在他最后一次考慮自定義Spliterator
,我將為類似的問題添加一個改編版本的答案,我建議使用自定義迭代器來創建一個分塊流。 此方法也適用於非輸入讀取器創建的其他流。
public class StreamSplitter<T>
implements Iterator<Stream<T>>
{
private Iterator<T> incoming;
private Predicate<T> startOfNewEntry;
private T nextLine;
public static <T> Stream<Stream<T>> streamOf(Stream<T> incoming, Predicate<T> startOfNewEntry)
{
Iterable<Stream<T>> iterable = () -> new StreamSplitter<>(incoming, startOfNewEntry);
return StreamSupport.stream(iterable.spliterator(), false);
}
private StreamSplitter(Stream<T> stream, Predicate<T> startOfNewEntry)
{
this.incoming = stream.iterator();
this.startOfNewEntry = startOfNewEntry;
if (incoming.hasNext())
nextLine = incoming.next();
}
@Override
public boolean hasNext()
{
return nextLine != null;
}
@Override
public Stream<T> next()
{
List<T> nextEntrysLines = new ArrayList<>();
do
{
nextEntrysLines.add(nextLine);
} while (incoming.hasNext()
&& !startOfNewEntry.test((nextLine = incoming.next())));
if (!startOfNewEntry.test(nextLine)) // incoming does not have next
nextLine = null;
return nextEntrysLines.stream();
}
}
例
public static void main(String[] args)
{
Stream<String> flat = Stream.of("Start of log entry 1",
" ...some log details",
" ...some log details",
"Start of log entry 2",
" ...some log details",
" ...some log details",
"Start of log entry 3",
" ...some log details",
" ...some log details");
StreamSplitter.streamOf(flat, line -> line.matches("Start of log entry.*"))
.forEach(logEntry -> {
System.out.println("------------------");
logEntry.forEach(System.out::println);
});
}
// Output
// ------------------
// Start of log entry 1
// ...some log details
// ...some log details
// ------------------
// Start of log entry 2
// ...some log details
// ...some log details
// ------------------
// Start of log entry 3
// ...some log details
// ...some log details
迭代器總是向前看一行。 只要該行是新條目的開頭,它將包裹流中的前一個條目並將其作為next
返回。 工廠方法streamOf
將此迭代器轉換為要在上面給出的示例中使用的流。
我將拆分條件從正則表達式更改為Predicate
,因此您可以借助多個正則表達式,if-conditions等指定更復雜的條件。
請注意,我只使用上面的示例數據對其進行了測試,因此我不知道它將如何處理更復雜,錯誤或空的輸入。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.