【问题标题】:How to copy large data files line by line?如何逐行复制大数据文件?
【发布时间】:2020-02-18 10:34:18
【问题描述】:

我有一个 35GB CSV 文件。我想读取每一行,如果符合条件,则将其写入新的 CSV。

try (BufferedWriter writer = Files.newBufferedWriter(Paths.get("source.csv"))) {
    try (BufferedReader br = Files.newBufferedReader(Paths.get("target.csv"))) {
        br.lines().parallel()
            .filter(line -> StringUtils.isNotBlank(line)) //bit more complex in real world
            .forEach(line -> {
                writer.write(line + "\n");
        });
    }
}

这大约需要。 7 分钟。是否有可能进一步加快这一进程?

【问题讨论】:

  • 是的,您可以尝试不在 Java 中执行此操作,而是直接从您的 Linux/Windows/etc 中执行此操作。操作系统。 Java 是解释型的,使用它总会有开销。除此之外,不,我没有任何明显的方法来加速它,35GB 的 7 分钟对我来说似乎是合理的。
  • 也许删除parallel 会更快?这不是打乱线吗?
  • 自己创建BufferedWriter,使用constructor,让您设置缓冲区大小。也许更大(或更小)的缓冲区大小会有所作为。我会尝试将BufferedWriter 缓冲区大小与主机操作系统缓冲区大小相匹配。
  • @TimBiegeleisen:“Java 被解释”充其量只是误导,而且几乎总是错误的。是的,对于某些优化,您可能需要离开 JVM 世界,但是在 Java 中更快地做到这一点绝对是可行的。
  • 您应该对应用程序进行概要分析,以查看是否有任何热点可以解决。您将无法对原始 IO 做太多事情(默认的 8192 字节缓冲区并没有那么糟糕,因为涉及到扇区大小等),但可能会发生一些事情(内部),您可能能够做到一起工作。

标签: java java-stream java-io


【解决方案1】:

如果是一个选项,您可以使用 GZipInputStream/GZipOutputStream 来最小化磁盘 I/O。

Files.newBufferedReader/Writer 使用默认缓冲区大小,我相信是 8 KB。您可以尝试更大的缓冲区。

转换为字符串,Unicode,减慢到(并使用两倍的内存)。使用的 UTF-8 并不像 StandardCharsets.ISO_8859_1 那样简单。

如果您可以在大多数情况下使用 bytes 并且仅针对特定的 CSV 字段将它们转换为字符串,则最好。

内存映射文件可能是最合适的。文件范围可能会使用并行性,从而将文件吐出。

try (FileChannel sourceChannel = new RandomAccessFile("source.csv","r").getChannel(); ...
MappedByteBuffer buf = sourceChannel.map(...);

这将变成有点多的代码,在(byte)'\n' 上得到正确的行,但不会过于复杂。

【讨论】:

  • 字节读取的问题是,在现实世界中,我必须评估行的开头,特定字符的子字符串,并且只将行的剩余部分写入输出文件。所以我可能无法仅将这些行读取为字节?
  • 我刚刚在 ramdisk 上测试了 GZipInputStream + GZipOutputStream 完全内存。性能差很多...
  • 在 Gzip 上:那么它不是一个慢速磁盘。是的,字节是一个选项:换行符、逗号、制表符、分号都可以作为字节处理,并且比字符串要快得多。字节为 UTF-8 到 UTF-16 字符到字符串到 UTF-8 到字节。
  • 只是随着时间的推移映射文件的不同部分。当您达到限制时,只需从最后一个已知良好位置创建一个新的MappedByteBufferFileChannel.map 需要多头)。
  • 2019年不用new RandomAccessFile(…).getChannel()。只需使用FileChannel.open(…)
【解决方案2】:

你可以试试这个:

try (BufferedWriter writer = new BufferedWriter(new FileWriter(targetFile), 1024 * 1024 * 64)) {
  try (BufferedReader br = new BufferedReader(new FileReader(sourceFile), 1024 * 1024 * 64)) {

我认为它会为您节省一两分钟。通过指定缓冲区大小,测试可以在大约 4 分钟内在我的机器上完成。

会不会更快?试试这个:

final char[] cbuf = new char[1024 * 1024 * 128];

try (Writer writer = new FileWriter(targetFile)) {
  try (Reader br = new FileReader(sourceFile)) {
    int cnt = 0;
    while ((cnt = br.read(cbuf)) > 0) {
      // add your code to process/split the buffer into lines.
      writer.write(cbuf, 0, cnt);
    }
  }
}

这应该可以为您节省三到四分钟。

如果这还不够。 (我猜你问这个问题的原因可能是你需要重复执行任务)。如果你想在一分钟甚至几秒钟内完成它。那么你应该处理数据并将其保存到db中,然后由多个服务器处理任务。

【讨论】:

  • 到你的最后一个例子:我怎样才能评估cbuf的内容,只写出部分内容?我是否必须在缓冲区满后重置? (我怎么知道缓冲区已满?)
【解决方案3】:

感谢您的所有建议,我想到的最快的方法是与BufferedOutputStream 交换作者,这带来了大约 25% 的改进:

   try (BufferedReader reader = Files.newBufferedReader(Paths.get("sample.csv"))) {
        try (BufferedOutputStream writer = new BufferedOutputStream(Files.newOutputStream(Paths.get("target.csv")), 1024 * 16)) {
            reader.lines().parallel()
                    .filter(line -> StringUtils.isNotBlank(line)) //bit more complex in real world
                    .forEach(line -> {
                        writer.write((line + "\n").getBytes());
                    });
        }
    }

在我的情况下,BufferedReader 的性能仍然优于 BufferedInputStream

【讨论】:

    猜你喜欢
    • 2011-04-06
    • 2014-04-30
    • 1970-01-01
    • 2022-11-24
    • 2011-12-22
    • 1970-01-01
    相关资源
    最近更新 更多