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?


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.


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.


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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.