簡體   English   中英

Java通過謂詞將流分割為流的流

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM