Как копировать большие файлы данных построчно?


9

У меня есть CSVфайл 35 ГБ . Я хочу прочитать каждую строку и записать строку в новый CSV, если он соответствует условию.

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

Это занимает ок. 7 минут Можно ли еще быстрее ускорить этот процесс?


1
Да, вы можете попытаться сделать это не из Java, а сделать это прямо из Linux / Windows / и т. Д. Операционная система. Java интерпретируется, и при ее использовании всегда будут накладные расходы. Кроме этого, нет, у меня нет никакого очевидного способа ускорить его, и 7 минут для 35 ГБ кажутся мне разумными.
Тим Бигелейзен

1
Может быть, удаление parallelделает это быстрее? И разве это не перемешивает линии?
Тило

1
Создайте BufferedWriterсебя, используя конструктор, который позволяет вам установить размер буфера. Возможно, больший (или меньший) размер буфера будет иметь значение. Я хотел бы попытаться сопоставить BufferedWriterразмер буфера с размером буфера операционной системы хоста.
Авра

5
@TimBiegeleisen: «Java интерпретируется» в лучшем случае вводит в заблуждение, а также почти всегда неверно. Да, для некоторых оптимизаций вам может потребоваться покинуть мир JVM, но сделать это быстрее в Java определенно выполнимо.
Иоахим Зауэр

1
Вы должны профилировать приложение, чтобы увидеть, есть ли какие-либо горячие точки, с которыми вы можете что-то сделать. Вы не сможете ничего сделать с необработанным вводом-выводом (8192-байтовый буфер по умолчанию не так уж и плох, так как в нем задействованы размеры секторов и т. Д.), Но могут быть вещи (внутренние), которые вы могли бы сделать работать с.
Каяман

Ответы:


4

Если это опция, вы можете использовать GZipInputStream / GZipOutputStream для минимизации дискового ввода-вывода.

Files.newBufferedReader / Writer использует размер буфера по умолчанию, я полагаю, 8 КБ. Вы можете попробовать больший буфер.

Преобразование в строку, Unicode, замедляется (и использует в два раза больше памяти). Используемый UTF-8 не так прост, как StandardCharsets.ISO_8859_1.

Лучше всего, если вы можете работать с байтами по большей части и только для определенных CSV-полей конвертировать их в String.

Файл сопоставления памяти может быть наиболее подходящим. Параллелизм может использоваться диапазонами файлов, выплевывая файл.

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

Это станет немного больше кода, получая строки прямо (byte)'\n', но не слишком сложным.


Проблема с чтением байтов состоит в том, что в реальном мире я должен оценить начало строки, подстроку определенного символа и записать только оставшуюся часть строки в выходной файл. Так что я, вероятно, не могу прочитать строки только как байты?
membersound

Я только что проверил GZipInputStream + GZipOutputStreamполностью память на виртуальном диске. Производительность была намного хуже ...
membersound

1
На Gzip: тогда это не медленный диск. Да, байты - это опция: символы новой строки, запятая, табуляция, точка с запятой могут обрабатываться как байты и будут значительно быстрее, чем String. Байт от UTF-8 до UTF-16, символ от String до UTF-8, до байтов.
Joop Eggen

1
Просто сопоставьте различные части файла с течением времени. Когда вы достигнете предела, просто создайте новую MappedByteBufferиз последней заведомо хорошей позиции ( FileChannel.mapзанимает много времени).
Иоахим Зауэр

1
В 2019 году нет необходимости использовать new RandomAccessFile(…).getChannel(). Просто используйте FileChannel.open(…).
Хольгер

0

Вы можете попробовать это:

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

Я думаю, что это сэкономит вам одну или две минуты. Тест можно выполнить на моей машине примерно за 4 минуты, указав размер буфера.

это может быть быстрее? попробуй это:

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

Это должно сэкономить вам три или четыре минуты.

Если этого еще недостаточно. (Вероятно, причина, по которой вы задаете вопрос, заключается в том, что вам нужно выполнять задачу повторно). если вы хотите сделать это за одну минуту или даже пару секунд. Затем вы должны обработать данные и сохранить их в БД, а затем обработать задачу несколькими серверами.


К вашему последнему примеру: как я могу оценить cbufсодержимое и выписать только порции? И должен ли я сбросить буфер после заполнения? (как я могу знать , что буфер заполнен?)
membersound

0

Благодаря всем вашим предложениям быстрее всего я обменивался писателем BufferedOutputStream, что дало улучшение примерно на 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());
                    });
        }
    }

Тем не менее, BufferedReaderработает лучше, чем BufferedInputStreamв моем случае.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.