Como copiar grandes arquivos de dados linha por linha?

9

Eu tenho um CSVarquivo de 35GB . Quero ler cada linha e gravar a linha em um novo CSV, se corresponder a uma condição.

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");
        });
    }
}

Isso leva aprox. 7 minutos. É possível acelerar ainda mais esse processo?

som do membro
fonte
11
Sim, você pode tentar não fazer isso do Java, mas fazê-lo diretamente do seu Linux / Windows / etc. sistema operacional. Java é interpretado, e sempre haverá uma sobrecarga em usá-lo. Além disso, não, não tenho nenhuma maneira óbvia de acelerar, e 7 minutos para 35 GB me parecem razoáveis.
Tim Biegeleisen 22/10/19
11
Talvez a remoção do paralleltorna mais rápido? E isso não embaralha as linhas?
Thilo
11
Crie BufferedWritervocê mesmo, usando o construtor que permite definir o tamanho do buffer. Talvez um tamanho de buffer maior (ou menor) faça a diferença. Eu tentaria corresponder o BufferedWritertamanho do buffer ao tamanho do buffer do sistema operacional do host.
Abra
5
@ TimBiegeleisen: "Java é interpretado" é enganoso na melhor das hipóteses e quase sempre errado também. Sim, para algumas otimizações, pode ser necessário deixar o mundo da JVM, mas fazer isso mais rapidamente em Java é definitivamente possível.
Joachim Sauer
11
Você deve criar um perfil do aplicativo para ver se há pontos de acesso sobre os quais possa fazer algo. Você não poderá fazer muito sobre a IO bruta (o buffer de 8192 bytes padrão não é tão ruim, pois há tamanhos de setor etc. envolvidos), mas pode haver coisas acontecendo (internamente) que você pode conseguir trabalhar com.
Kayaman # 22/19

Respostas:

4

Se for uma opção, você poderá usar GZipInputStream / GZipOutputStream para minimizar a E / S do disco.

O Files.newBufferedReader / Writer usa um tamanho de buffer padrão, 8 KB, acredito. Você pode tentar um buffer maior.

A conversão para String, Unicode, diminui para (e usa o dobro da memória). O UTF-8 usado não é tão simples quanto StandardCharsets.ISO_8859_1.

O melhor seria se você pudesse trabalhar com bytes na maior parte e apenas para campos CSV específicos convertê-los em String.

Um arquivo mapeado de memória pode ser o mais apropriado. O paralelismo pode ser usado por intervalos de arquivos, cuspindo o arquivo.

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

Isso se tornará um pouco demais de código, colocando as linhas corretamente (byte)'\n', mas não excessivamente complexo.

Joop Eggen
fonte
O problema com a leitura de bytes é que, no mundo real, tenho que avaliar o início da linha, usando substring em um caractere específico e apenas escrevendo a parte restante da linha no arquivo externo. Então provavelmente não consigo ler as linhas apenas como bytes?
membersound
Acabei de testar GZipInputStream + GZipOutputStreamtotalmente a memória em um ramdisk. O desempenho foi muito pior ...
membersound
11
No Gzip: então não é um disco lento. Sim, bytes é uma opção: novas linhas, vírgula, tabulação e ponto-e-vírgula podem ser tratadas como bytes e serão consideravelmente mais rápidas que como String. Bytes como UTF-8 a UTF-16 char para String para UTF-8 em bytes.
Joop Eggen
11
Basta mapear diferentes partes do arquivo ao longo do tempo. Quando você atingir o limite, basta criar um novo a MappedByteBufferpartir da última posição FileChannel.mapválida ( leva muito tempo).
Joachim Sauer
11
Em 2019, não há necessidade de usar new RandomAccessFile(…).getChannel(). Apenas use FileChannel.open(…).
Holger
0

você pode tentar isso:

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

Eu acho que você vai economizar um ou dois minutos. o teste pode ser feito na minha máquina em cerca de 4 minutos, especificando o tamanho do buffer.

poderia ser mais rápido? tente isto:

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);
    }
  }
}

Isso deve economizar três ou quatro minutos.

Se isso ainda não é suficiente. (A razão pela qual acho que você faz a pergunta provavelmente é que você precisa executar a tarefa repetidamente). se você quiser fazê-lo em um minuto ou até alguns segundos. você deve processar os dados e salvá-los no banco de dados, depois processar a tarefa por vários servidores.

user_3380739
fonte
Para o seu último exemplo: como posso avaliar o cbufconteúdo e escrever apenas partes? E eu teria que redefinir o buffer uma vez cheio? (como posso saber se o buffer está cheio?) #
24519
0

Graças a todas as suas sugestões, o mais rápido que eu vim foi trocar o escritor, o que resultou em uma BufferedOutputStreammelhoria de aproximadamente 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());
                    });
        }
    }

Ainda assim, o BufferedReaderdesempenho é melhor do que BufferedInputStreamno meu caso.

som do membro
fonte