Jak skopiować duże pliki danych linia po linii?

9

Mam CSVplik 35 GB . Chcę przeczytać każdą linię i napisać linię do nowego pliku CSV, jeśli pasuje do określonego warunku.

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

To zajmuje około 7 minut. Czy można jeszcze bardziej przyspieszyć ten proces?

Membersound
źródło
1
Tak, możesz spróbować tego nie robić w Javie, ale raczej bezpośrednio z poziomu Linux / Windows / etc. system operacyjny. Java jest interpretowana i korzystanie z niej zawsze wiąże się z dodatkowymi kosztami. Poza tym nie, nie mam żadnego oczywistego sposobu na przyspieszenie, a 7 minut na 35 GB wydaje mi się rozsądne.
Tim Biegeleisen,
1
Może usunięcie parallelgo przyspieszy? I czy to nie przesuwa linii wokół?
Thilo,
1
Utwórz BufferedWritersiebie, używając konstruktora, który pozwala ustawić rozmiar bufora. Być może większy (lub mniejszy) rozmiar bufora zrobi różnicę. Spróbowałbym dopasować BufferedWriterrozmiar bufora do rozmiaru bufora systemu operacyjnego hosta.
Abra,
5
@TimBiegeleisen: „Java jest interpretowana” w najlepszym przypadku wprowadza w błąd i prawie zawsze również źle. Tak, w przypadku niektórych optymalizacji może być konieczne opuszczenie świata JVM, ale szybsze wykonanie tego w Javie jest zdecydowanie wykonalne.
Joachim Sauer
1
Powinieneś profilować aplikację, aby sprawdzić, czy są jakieś punkty aktywne, w których możesz coś zrobić. Nie będziesz w stanie wiele zrobić z surowym We / Wy (domyślny bufor 8192 bajtów nie jest taki zły, ponieważ dotyczą wielkości sektorów itp.), Ale mogą się zdarzyć rzeczy (wewnętrznie), które możesz pracować z.
Kayaman

Odpowiedzi:

4

Jeśli jest to opcja, można użyć GZipInputStream / GZipOutputStream, aby zminimalizować dyskowe operacje we / wy.

Pliki.newBufferedReader / Writer używają domyślnego rozmiaru bufora, jak sądzę. Możesz wypróbować większy bufor.

Konwersja na ciąg, Unicode, spowalnia do (i zużywa dwukrotnie pamięć). Zastosowany UTF-8 nie jest tak prosty jak StandardCharsets.ISO_8859_1.

Najlepiej byłoby, gdybyś przeważnie pracował z bajtami i tylko dla określonych pól CSV przekonwertował je na String.

Plik odwzorowany w pamięci może być najbardziej odpowiedni. Zakresy plików mogą być używane równolegle, plując plik.

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

Stanie się to trochę dużym kodem, wprowadzanie wierszy od razu (byte)'\n', ale nie będzie zbyt skomplikowane.

Joop Eggen
źródło
Problem z czytaniem bajtów polega na tym, że w prawdziwym świecie muszę oceniać początek linii, podciągając na konkretny znak i zapisując tylko pozostałą część linii do pliku wyjściowego. Więc prawdopodobnie nie mogę odczytać wierszy jako bajtów?
Membersound
Właśnie przetestowałem GZipInputStream + GZipOutputStreampełną pamięć na ramdysku. Wydajność była znacznie gorsza ...
członkowie
1
W Gzip: to nie jest wolny dysk. Tak, bajty są opcją: znaki nowej linii, przecinek, tabulator, średnik wszystkie mogą być traktowane jako bajty i będą znacznie szybsze niż jako ciąg. Bajty jako UTF-8 na UTF-16 char na String na UTF-8 na bajty.
Joop Eggen
1
Po prostu zmapuj różne części pliku w czasie. Kiedy osiągniesz limit, po prostu stwórz nowy MappedByteBufferz ostatniej znanej dobrej pozycji ( FileChannel.maptrwa długo).
Joachim Sauer
1
W 2019 roku nie ma potrzeby korzystania new RandomAccessFile(…).getChannel(). Po prostu użyj FileChannel.open(…).
Holger
0

możesz spróbować:

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

Myślę, że pozwoli ci to zaoszczędzić jedną lub dwie minuty. test można wykonać na moim komputerze w ciągu około 4 minut, określając rozmiar bufora.

czy może być szybciej? Spróbuj tego:

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

Powinno to zaoszczędzić trzy lub cztery minuty.

Jeśli to wciąż za mało. (Myślę, że prawdopodobnie zadajesz pytanie, dlatego musisz wielokrotnie wykonywać to zadanie). jeśli chcesz to zrobić w ciągu jednej minuty lub nawet kilku sekund. następnie powinieneś przetworzyć dane i zapisać je w db, a następnie przetworzyć zadanie przez wiele serwerów.

użytkownik_3380739
źródło
Do twojego ostatniego przykładu: jak mogę następnie ocenić cbufzawartość i wypisać tylko fragmenty? I czy musiałbym zresetować bufor po zapełnieniu? (skąd mam wiedzieć, że bufor jest pełny?)
Membersound
0

Dzięki wszystkim twoim sugestiom najszybciej wymyśliłem wymianę pisarza BufferedOutputStream, co dało poprawę o około 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());
                    });
        }
    }

Nadal BufferedReaderdziała lepiej niż BufferedInputStreamw moim przypadku.

Membersound
źródło