¿Cómo copiar grandes archivos de datos línea por línea?

9

Tengo un CSVarchivo de 35 GB . Quiero leer cada línea y escribir la línea en un nuevo CSV si coincide con una condición.

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

Esto toma aprox. 7 minutos ¿Es posible acelerar ese proceso aún más?

miembros
fuente
1
Sí, podría intentar no hacerlo desde Java, sino hacerlo directamente desde su Linux / Windows / etc. sistema operativo. Java se interpreta y siempre habrá una sobrecarga al usarlo. Además de esto, no, no tengo ninguna forma obvia de acelerarlo, y 7 minutos por 35 GB me parecen razonables.
Tim Biegeleisen
1
¿Quizás eliminarlo lo parallelhace más rápido? ¿Y eso no mezcla las líneas?
Thilo
1
Cree BufferedWriterusted mismo, utilizando el constructor que le permite establecer el tamaño del búfer. Tal vez un tamaño de búfer más grande (o más pequeño) hará la diferencia. Intentaría hacer coincidir el BufferedWritertamaño del búfer con el tamaño del búfer del sistema operativo del host.
Abra
55
@TimBiegeleisen: "Java es interpretado" es engañoso en el mejor de los casos y casi siempre también es incorrecto. Sí, para algunas optimizaciones, es posible que deba abandonar el mundo JVM, pero hacer esto más rápido en Java definitivamente es factible.
Joachim Sauer
1
Debe crear un perfil de la aplicación para ver si hay puntos de acceso sobre los que pueda hacer algo. No podrá hacer mucho acerca de la E / S sin procesar (el búfer de bytes 8192 predeterminado no es tan malo, ya que hay tamaños de sector, etc.), pero pueden estar sucediendo cosas (internamente) que podría ser capaz de trabajar con.
Kayaman

Respuestas:

4

Si es una opción, puede usar GZipInputStream / GZipOutputStream para minimizar la E / S de disco.

Files.newBufferedReader / Writer utiliza un tamaño de búfer predeterminado, 8 KB, creo. Puede probar un búfer más grande.

Al convertir a String, Unicode, se ralentiza (y usa el doble de memoria). El UTF-8 usado no es tan simple como StandardCharsets.ISO_8859_1.

Lo mejor sería si puede trabajar con bytes en su mayor parte y solo para campos CSV específicos convertirlos a String.

Un archivo mapeado en memoria podría ser el más apropiado. Paralelismo puede ser utilizado por rangos de archivos, escupiendo el archivo.

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

Esto se convertirá en un poco de código, haciendo que las líneas funcionen correctamente (byte)'\n', pero no demasiado complejas.

Joop Eggen
fuente
El problema con la lectura de bytes es que en el mundo real tengo que evaluar el comienzo de la línea, la subcadena en un carácter específico y solo escribir la parte restante de la línea en el archivo externo. ¿Entonces probablemente no pueda leer las líneas como bytes solamente?
membersound
Acabo de probar GZipInputStream + GZipOutputStreamcompletamente la memoria en un disco RAM. El rendimiento fue mucho peor ...
membersound
1
En Gzip: entonces no es un disco lento. Sí, los bytes son una opción: las líneas nuevas, las comas, las tabulaciones, el punto y coma, todas pueden manejarse como bytes, y serán considerablemente más rápidas que las cadenas. Bytes como UTF-8 a UTF-16 char a String a UTF-8 a bytes.
Joop Eggen
1
Simplemente asigne diferentes partes del archivo a lo largo del tiempo. Cuando llegue al límite, simplemente cree una nueva MappedByteBufferdesde la última posición buena conocida ( FileChannel.maptoma mucho tiempo).
Joachim Sauer el
1
En 2019, no hay necesidad de usar new RandomAccessFile(…).getChannel(). Solo úsalo FileChannel.open(…).
Holger
0

puedes probar esto:

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

Creo que te ahorrará uno o dos minutos. La prueba se puede hacer en mi máquina en aproximadamente 4 minutos especificando el tamaño del búfer.

¿podría ser más rápido? prueba esto:

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

Esto debería ahorrarte tres o cuatro minutos.

Si eso todavía no es suficiente. (La razón por la que supongo que hace la pregunta probablemente es que necesita ejecutar la tarea repetidamente). si quieres hacerlo en un minuto o incluso un par de segundos. entonces debe procesar los datos y guardarlos en db, luego procesar la tarea por varios servidores.

user_3380739
fuente
Para su último ejemplo: ¿cómo puedo evaluar el cbufcontenido y solo escribir porciones? ¿Y tendría que restablecer el búfer una vez lleno? (¿cómo puedo saber que el búfer está lleno?)
membersound
0

Gracias a todas sus sugerencias, lo más rápido que se me ocurrió fue intercambiar el escritor BufferedOutputStream, lo que dio una mejora aproximada del 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());
                    });
        }
    }

Aún así BufferedReaderfunciona mejor que BufferedInputStreamen mi caso.

miembros
fuente