繁体   English   中英

使用Java Streams从文本文件一次读取X行?

[英]Read X lines at a time from a text file using Java Streams?

我有一个“普通的旧文本文件”,其中行以新行字符结尾。 出于任意原因,我需要一次读取和解析此文本文件4(X为通用)行。

我想将Java流用于此任务,我知道我可以将文件转换为如下所示的流:

try (Stream<String> stream = Files.lines(Paths.get("file.txt""))) {
    stream.forEach(System.out::println);
} catch (IOException e) {
    e.printStackTrace();
}

但是,我如何使用Java的Stream API将文件“捆绑”成4个连续的行?

这是java.util.Scanner的工作。 在Java 9中,您可以简单地使用

try(Scanner s = new Scanner(PATH)) {
    s.findAll("(.*\\R){1,4}")
     .map(mr -> Arrays.asList(mr.group().split("\\R")))
     .forEach(System.out::println);
}

对于Java 8,您可以使用此答案findAll的后端口。 为该方法添加import static后,您可以像使用它一样使用它

try(Scanner s = new Scanner(PATH)) {
    findAll(s, Pattern.compile("(.*\\R){1,4}"))
        .map(mr -> Arrays.asList(mr.group().split("\\R")))
        .forEach(System.out::println);
}

请注意,匹配操作的结果是包含最多四行的单个字符串(最后一行较少)。 如果这适合您的后续操作,您可以跳过将该字符串拆分为单独的行。

您甚至可以使用MatchResult的属性来更复杂地处理块,例如

try(Scanner s = new Scanner(PATH)) {
    findAll(s, Pattern.compile("(.*)\\R(?:(.*)\\R)?(?:(.*)\\R)?(?:(.*)\\R)?"))
        .flatMap(mr -> IntStream.rangeClosed(1, 4)
                           .mapToObj(ix -> mr.group(ix)==null? null: ix+": "+mr.group(ix)))
        .filter(Objects::nonNull)
        .forEach(System.out::println);
}

有一种方法可以使用标准Java 8 Stream API将文件内容分区并处理为n -size块。 您可以使用Collectors.groupingBy()将文件内容分区为块 - 您可以将它们收集为Collection<List<String>> ,也可以在收集所有行时应用一些处理(例如,您可以将它们连接到单个String) 。

看一下下面的例子:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

public class ReadFileWithStream {

    public static void main(String[] args) throws IOException {
        // Path to a file to read
        final Path path = Paths.get(ReadFileWithStream.class.getResource("/input.txt")‌​.toURI());
        final AtomicInteger counter = new AtomicInteger(0);
        // Size of a chunk
        final int size = 4;

        final Collection<List<String>> partitioned = Files.lines(path)
                .collect(Collectors.groupingBy(it -> counter.getAndIncrement() / size))
                .values();

        partitioned.forEach(System.out::println);
    }
}

我的输入文件包含一些数字(一行中有一个数字) ,当我运行以下代码时,我会得到类似的结果:

[0, 0, 0, 2]
[0, -3, 2, 0]
[1, -3, -8, 0]
[2, -12, -11, -11]
[-8, -1, -8, 0]
[2, -1, 2, -1]
... and so on

Collectors.groupingBy()允许我使用不同的下游收集器。 默认使用Collectors.toList() ,因此我的结果被累积到List<String> ,我得到Collection<List<String>>作为最终结果。

假设我想要读取4个大小的块,我想将所有数字加在一个块中。 在这种情况下,我将使用Collectors.summingInt()作为我的下游函数,返回的结果是Collection<Integer>

final Collection<Integer> partitioned = Files.lines(path)
        .collect(Collectors.groupingBy(it -> counter.getAndIncrement() / size, Collectors.summingInt(Integer::valueOf)))
        .values();

输出:

2
-1
-10
-32
-17
2
-11
-49
... and so on

最后但并非最不重要。 Collectors.groupingBy()返回一个映射,其中值按特定键分组。 这就是为什么最后我们调用Map.values()来获取此映射中包含的值的集合。

希望能帮助到你。

这是使用Guava的Iterators.partition方法的简单方法:

try (Stream<String> stream = Files.lines(Paths.get("file.txt""))) {

    Iterator<List<String>> iterator = Iterators.partition(stream.iterator(), 4);

    // iterator.next() returns each chunk as a List<String>

} catch (IOException e) {
    // handle exception properly
}

这仅适用于顺序处理,但如果您从磁盘读取文件,我很难想象并行处理会带来什么好处......


编辑:如果你想,而不是使用迭代器,你可以再次将其转换为流:

Stream<List<String>> targetStream = StreamSupport.stream(
      Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED),
      false);

如果你想坚持使用流,我看到的唯一解决方案是编写自己的自定义收集器。 它并非用于此目的,但您可以使用它。

private static final class CustomCollector {

    private List<String> list = new ArrayList<>();

    private List<String> acumulateList = new ArrayList<>();

    public void accept(String str) {
        acumulateList.add(str);
        if (acumulateList.size() == 4) { // acumulate 4 strings
            String collect = String.join("", acumulateList);
            // I just joined them in on string, you can do whatever you want
            list.add(collect);
            acumulateList = new ArrayList<>();
        }
    }

    public CustomCollector combine(CustomCollector other) {
        throw new UnsupportedOperationException("Parallel Stream not supported");
    }

    public List<String> finish() {
        if(!acumulateList.isEmpty()) {
            list.add(String.join("", acumulateList));
        }
        return list;
    }

    public static Collector<String, ?, List<String>> collector() {
        return Collector.of(CustomCollector::new, CustomCollector::accept, CustomCollector::combine, CustomCollector::finish);
    }
}

并像这样使用它:

stream.collect(CustomCollector.collector());

如果您愿意使用RxJava ,则可以使用其buffer功能:

Stream<String> stream = Files.lines(Paths.get("file.txt"))

Observable.fromIterable(stream::iterator)
          .buffer(4)                      // Observable<List<String>>
          .map(x -> String.join(", ", x)) // Observable<String>
          .forEach(System.out::println);

buffer创建一个Observable ,用于收集特定大小的列表中的元素。 在上面的例子中,我通过map添加了另一个转换,使列表更加友好,但您可以根据需要转换Observable 例如,如果您有一个方法processChunk ,它将List<String>作为参数并返回一个String ,您可以执行以下操作:

Observable<String> fileObs =
    Observable.fromIterable(stream::iterator)
              .buffer(4)
              .map(x -> processChunk(x));

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM