Bagaimana cara menyalin file data besar baris demi baris?

9

Saya memiliki CSVfile 35GB . Saya ingin membaca setiap baris, dan menulis baris ke CSV baru jika cocok dengan suatu syarat.

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

Ini membutuhkan sekitar. 7 menit. Apakah mungkin untuk mempercepat proses itu lebih banyak lagi?

anggota
sumber
1
Ya, Anda bisa mencoba tidak melakukan ini dari Jawa tetapi melakukannya langsung dari Linux / Windows / etc. sistem operasi. Java diinterpretasikan, dan akan selalu ada overhead dalam menggunakannya. Selain itu, tidak, saya tidak punya cara yang jelas untuk mempercepatnya, dan 7 menit untuk 35GB tampaknya masuk akal bagi saya.
Tim Biegeleisen
1
Mungkin menghapus parallelmembuatnya lebih cepat? Dan bukankah itu mengocok garis sekitar?
Thilo
1
Buat BufferedWriterdiri Anda sendiri, menggunakan konstruktor yang memungkinkan Anda mengatur ukuran buffer. Mungkin ukuran buffer yang lebih besar (atau lebih kecil) akan membuat perbedaan. Saya akan mencoba mencocokkan BufferedWriterukuran buffer dengan ukuran buffer sistem operasi host.
Abra
5
@TimBiegeleisen: "Java ditafsirkan" paling menyesatkan dan hampir selalu salah juga. Ya, untuk beberapa optimasi Anda mungkin harus meninggalkan dunia JVM, tetapi melakukan ini lebih cepat di Jawa pasti bisa dilakukan.
Joachim Sauer
1
Anda harus membuat profil aplikasi untuk melihat apakah ada hotspot yang dapat Anda lakukan. Anda tidak akan dapat berbuat banyak tentang IO mentah (buffer 8192 byte default tidak terlalu buruk, karena ada ukuran sektor dll.), Tetapi mungkin ada hal-hal terjadi (secara internal) yang mungkin dapat Anda lakukan bekerja dengan.
Kayaman

Jawaban:

4

Jika ini merupakan opsi, Anda dapat menggunakan GZipInputStream / GZipOutputStream untuk meminimalkan I / O disk.

Files.newBufferedReader / Writer menggunakan ukuran buffer default, 8 KB saya percaya. Anda dapat mencoba buffer yang lebih besar.

Konversi ke String, Unicode, melambat ke (dan menggunakan memori dua kali). UTF-8 yang digunakan tidak sesederhana StandardCharsets.ISO_8859_1.

Yang terbaik adalah jika Anda dapat bekerja dengan byte untuk sebagian besar dan hanya untuk bidang CSV spesifik mengubahnya menjadi String.

File yang dipetakan memori mungkin yang paling tepat. Paralelisme dapat digunakan oleh rentang file, meludah file.

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

Ini akan menjadi kode yang sedikit banyak, mendapatkan garis yang benar (byte)'\n', tetapi tidak terlalu rumit.

Joop Eggen
sumber
Masalah dengan membaca byte adalah bahwa di dunia nyata saya harus mengevaluasi awal baris, substring pada karakter tertentu dan hanya menulis bagian yang tersisa dari baris ke dalam pengabaian. Jadi saya mungkin tidak bisa membaca baris sebagai byte saja?
membersound
Saya baru saja menguji GZipInputStream + GZipOutputStreamsepenuhnya memori pada ramdisk. Kinerja jauh lebih buruk ...
membersound
1
Pada Gzip: maka itu bukan disk yang lambat. Ya, byte adalah opsi: baris baru, koma, tab, titik koma semua dapat ditangani sebagai byte, dan akan jauh lebih cepat daripada sebagai String. Bytes sebagai UTF-8 ke UTF-16 char to String to UTF-8 to bytes.
Joop Eggen
1
Hanya memetakan berbagai bagian file dari waktu ke waktu. Ketika Anda mencapai batas, buat saja yang baru MappedByteBufferdari posisi terakhir yang diketahui baik ( FileChannel.mapbutuh waktu lama).
Joachim Sauer
1
Pada 2019, tidak perlu digunakan new RandomAccessFile(…).getChannel(). Gunakan saja FileChannel.open(…).
Holger
0

Anda dapat mencoba ini:

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

Saya pikir itu akan menghemat satu atau dua menit. tes dapat dilakukan pada mesin saya dalam waktu sekitar 4 menit dengan menentukan ukuran buffer.

mungkinkah lebih cepat? coba ini:

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

Ini akan menghemat waktu Anda tiga atau empat menit.

Jika itu masih belum cukup. (Alasan saya kira Anda mengajukan pertanyaan mungkin adalah Anda perlu menjalankan tugas berulang kali). jika Anda ingin menyelesaikannya dalam satu menit atau bahkan beberapa detik. maka Anda harus memproses data dan menyimpannya ke db, lalu memproses tugas dengan beberapa server.

user_3380739
sumber
Untuk contoh terakhir Anda: bagaimana saya bisa mengevaluasi cbufkonten, dan hanya menulis bagian? Dan apakah saya harus mengatur ulang buffer setelah penuh? (bagaimana saya bisa tahu buffer sudah penuh?)
memberound
0

Terima kasih atas semua saran Anda, yang tercepat saya dapatkan adalah menukar penulis BufferedOutputStream, yang memberikan peningkatan sekitar 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());
                    });
        }
    }

Tetap BufferedReaderberkinerja lebih baik daripada BufferedInputStreamdalam kasus saya.

anggota
sumber