在不将整个文件加载到内存中的情况下合并大型文件?

7

我希望将包含字符串的大文件合并为一个文件,并尝试使用nio2。我不想将整个文件加载到内存中,所以我尝试使用BufferedReader:

public void mergeFiles(filesToBeMerged) throws IOException{

Path mergedFile = Paths.get("mergedFile");
Files.createFile(mergedFile);

List<Path> _filesToBeMerged = filesToBeMerged;

try (BufferedWriter writer = Files.newBufferedWriter(mergedFile,StandardOpenOption.APPEND)) {
        for (Path file : _filesToBeMerged) {
// this does not work as write()-method does not accept a BufferedReader
            writer.append(Files.newBufferedReader(file));
        }
    } catch (IOException e) {
        System.err.println(e);
    }

}

我用这个尝试了一下,它可以工作,但是字符串的格式(例如换行符等)没有复制到合并后的文件中:

...
try (BufferedWriter writer = Files.newBufferedWriter(mergedFile,StandardOpenOption.APPEND)) {
        for (Path file : _filesToBeMerged) {
//              writer.write(Files.newBufferedReader(file));
            String line = null;


BufferedReader reader = Files.newBufferedReader(file);
            while ((line = reader.readLine()) != null) {
                    writer.append(line);
                    writer.append(System.lineSeparator());
             }
reader.close();
        }
    } catch (IOException e) {
        System.err.println(e);
    }
...

如何在不将整个文件加载到内存中的情况下,使用NIO2合并大型文件?
4个回答

22

如果您希望高效地合并两个或多个文件,您应该问问自己:为什么要使用基于charReaderWriter来执行这项任务。

使用这些类会将文件的字节从系统默认编码转换为Unicode字符,再从Unicode字符转换回系统默认编码。这意味着程序必须对整个文件执行两次数据转换。

顺便说一下,BufferedReaderBufferedWriter绝不是NIO2的产物。这些类存在于Java的第一个版本中。

当您使用基于字节的NIO函数进行复制时,文件可以在不受Java应用程序影响的情况下传输,最好的情况是在文件系统缓冲区中直接执行传输:

import static java.nio.file.StandardOpenOption.*;

import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;

public class MergeFiles
{
  public static void main(String[] arg) throws IOException {
    if(arg.length<2) {
      System.err.println("Syntax: infiles... outfile");
      System.exit(1);
    }
    Path outFile=Paths.get(arg[arg.length-1]);
    System.out.println("TO "+outFile);
    try(FileChannel out=FileChannel.open(outFile, CREATE, WRITE)) {
      for(int ix=0, n=arg.length-1; ix<n; ix++) {
        Path inFile=Paths.get(arg[ix]);
        System.out.println(inFile+"...");
        try(FileChannel in=FileChannel.open(inFile, READ)) {
          for(long p=0, l=in.size(); p<l; )
            p+=in.transferTo(p, l-p, out);
        }
      }
    }
    System.out.println("DONE.");
  }
}

1
哇,这个解决方案真的很棒 - 而且源代码非常简短。谢谢!你知道一个基于nio2的解决方案来将一个大文件分割成一组小文件吗?实际上,我正在使用类似于http://todayguesswhat.blogspot.de/2014/05/java-split-large-file-sample-code-high.html的东西。 - nimo23
1
@nimo23:我认为,当你尝试理解我的答案中的代码,特别是FileChannel.transferTo的作用时,你会意识到如何实现分割的解决方案(即:非常相似)。如果你在实现过程中遇到困难,可以开一个新问题。 - Holger
1
好的,我会自己尝试并在这里提供解决方案! - nimo23
1
好的,我已经发布了一个解决方案:https://dev59.com/I4Lba4cB1Zd3GeqPk-hR。我找不到一个使用nio2的解决方案,因为使用nio2时,拆分文件的大小只能通过文件大小来减小。然而,我想按行数拆分文本文件。你能找到(更好的)使用nio2的splitTextFiles()方法的解决方案吗? - nimo23

3

随着

Files.newBufferedReader(file).readLine()

每次都创建新的缓冲区,而且它总是在第一行被重置。
替换为:
BufferedReader reader = Files.newBufferedReader(file);
while ((line = reader.readLine()) != null) {
  writer.write(line);
}

完成后,记得使用.close()关闭阅读器。


谢谢,我已经在源代码中进行了更改。你知道怎么样可以保留合并文件的格式到“mergedFile”文件中吗?例如,合并文件可能有回车符或空行。使用上述方法时,所有这些都没有复制到“mergedFile”中。 - nimo23
不确定您的意思,但是您可以使用writer.write(System.lineSeparator());手动添加换行符。 - Marco Acierno
我在想哪个更高效。上面的解决方案还是http://www.programcreek.com/2012/09/merge-files-in-java/中的解决方案更好?你知道哪一个更高效吗? - nimo23
@nimo23 为此编写一个测试。由于您有一个大文件,因此执行多次复制并检查每种方法所需的时间。 - Michal Gruca

1

readLine() 方法不包含行尾标记 ("\n" 或 "\r\n")。这就是错误的原因。

while ((line = reader.readLine()) != null) {
    writer.write(line);
    writer.write("\r\n"); // Windows
}

您也可以忽略这种(可能不同的)行尾过滤,并使用

try (OutputStream out = new FileOutputStream(file);
    for (Path source : filesToBeMerged) {
        Files.copy(path, out);
        out.write("\r\n".getBytes(StandardCharsets.US_ASCII));
    }
}

这将明确地写入一个换行符,以防止最后一行没有结束符的情况。
可能仍然存在一个问题,即文件开头的可选且不美观的Unicode BOM字符标记文本为UTF-8 / UTF-16LE / UTF-16BE。

0
我试着用三种方法将文件合并成一个文件。 我测试了这些方法,但我不知道哪个是最好的...还是不知道。 我以为FileChannel比其他方法更快,但对我来说并不是这样。 如果你们有任何问题,请告诉我。
    private static void mergeFiles(List<Path> sources, Path destination) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(destination.toFile(), true))) {
            for (Path path : sources) {
                try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(path.toFile())))) {
                    String line;
                    while ((line = reader.readLine()) != null) {
                        writer.write(line);
                        writer.newLine();
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

2. InputStream & Files.copy
2. 输入流和文件复制
    private static void mergeFiles2(List<Path> sources, Path destination) {
        try {
            BinaryOperator<InputStream> sequenceInputStream = SequenceInputStream::new;
            List<InputStream> inputStreams = new ArrayList<>();

            for (Path path : sources) {
                InputStream is = Files.newInputStream(path, StandardOpenOption.READ);
                inputStreams.add(is);
            }

            InputStream streams = inputStreams.parallelStream().reduce(sequenceInputStream).orElseThrow(() -> new IllegalStateException("inputStreams reduce exception"));
            Files.copy(streams, destination, StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
  1. File Channel
    private static void mergeFiles3(List<Path> sources, Path destination) {
        try (FileChannel desChannel = FileChannel.open(destination, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
            for (Path path : sources) {
                try (FileChannel srcChannel = FileChannel.open(path, StandardOpenOption.READ)) {
                    for (long position = 0, size = srcChannel.size(); position < size; ) {
                        position += srcChannel.transferTo(position, size - position, desChannel);
                    }
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接