Быстрый синтаксический анализ CSV

У меня есть приложение Java-сервера, которое загружает CSV файл и анализирует его. Анализ может занимать от 5 до 45 минут и происходит каждый час. Этот метод является узким местом приложения, поэтому он не является преждевременной оптимизацией. Код до сих пор:

        client.executeMethod(method);
        InputStream in = method.getResponseBodyAsStream(); // this is http stream

        String line;
        String[] record;

        reader = new BufferedReader(new InputStreamReader(in), 65536);

        try {
            // read the header line
            line = reader.readLine();
            // some code
            while ((line = reader.readLine()) != null) {
                 // more code

                 line = line.replaceAll("\"\"", "\"NULL\"");

                 // Now remove all of the quotes
                 line = line.replaceAll("\"", "");     


                 if (!line.startsWith("ERROR"){
                   //bla bla 
                    continue;
                 }

                 record = line.split(",");
                 //more error handling
                 // build the object and put it in HashMap
         }
         //exceptions handling, closing connection and reader

Есть ли какая-нибудь существующая библиотека, которая поможет мне ускорить работу? Могу ли я улучшить существующий код?

Ответы

Ответ 1

Apache Commons CSV

Вы видели Apache Commons CSV?

Будьте осторожны при использовании split

Имейте в виду, что split возвращает только представление данных, что означает, что исходный объект line не подходит для сборки мусора, в то время как имеется ссылка на любое из его представлений. Возможно, создание защитной копии поможет? (Отчет об ошибках Java)

Это также не надежно в группировке экранированных столбцов CSV, содержащих запятые

Ответ 3

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

Ниже приведен краткий анализ и предлагаемое решение

  • Из кода кажется, что вы читаете данные по сети (скорее всего, apache-common-httpclient lib).
  • Вам нужно убедиться, что узкое место, о котором вы говорите, не связано с передачей данных по сети.
  • Один из способов увидеть - просто выгрузить данные в каком-либо файле (без разбора) и посмотреть, сколько это нужно. Это даст вам представление о том, сколько времени фактически потрачено на синтаксический анализ (по сравнению с текущим наблюдением).
  • Теперь посмотрим, как используется пакет java.util.concurrent. Некоторая ссылка, которую вы можете использовать, (1, 2)
  • Что вы делаете, так это задачи, которые вы делаете в цикле for, могут выполняться в потоке.
  • Использование threadpool и concurrency значительно улучшит вашу производительность.

Хотя решение требует определенных усилий, но в конце это поможет вам.

Ответ 4

Проблема вашего кода заключается в том, что он использует replaceAll и split, которые являются очень дорогостоящими. Вы должны обязательно рассмотреть использование синтаксического анализатора csv/reader, который будет обрабатывать один проход.

Существует метка github

https://github.com/uniVocity/csv-parsers-comparison

к сожалению, выполняется под java 6. Число немного отличается от java 7 и 8. Я пытаюсь получить более подробные данные для разных размеров файла, но он работает в процессе

см. https://github.com/arnaudroger/csv-parsers-comparison

Ответ 5

opencsv

Вы должны посмотреть OpenCSV. Я бы ожидал, что у них будут оптимизация производительности.

Ответ 6

Немного поздно здесь, теперь есть несколько проектов бенчмаркинга для парсеров CSV. Ваш выбор будет зависеть от точного варианта использования (т.е. необработанные данные против привязки данных и т.д.).

Ответ 8

Для скорости вы не хотите использовать replaceAll, и вы также не хотите использовать регулярные выражения. То, что вы в основном всегда хотите делать в критических случаях, таких как создание символа конечного автомата за анализатором символов. Я сделал это, свернув все это в функцию Iterable. Он также принимает поток и анализирует его, не сохраняя и не кэшируя. Так что, если вы можете прервать лечение рано, то, скорее всего, все будет хорошо. Он также должен быть достаточно коротким и хорошо закодированным, чтобы было понятно, как он работает.

public static Iterable<String[]> parseCSV(final InputStream stream) throws IOException {
    return new Iterable<String[]>() {
        @Override
        public Iterator<String[]> iterator() {
            return new Iterator<String[]>() {
                static final int UNCALCULATED = 0;
                static final int READY = 1;
                static final int FINISHED = 2;
                int state = UNCALCULATED;
                ArrayList<String> value_list = new ArrayList<>();
                StringBuilder sb = new StringBuilder();
                String[] return_value;

                public void end() {
                    end_part();
                    return_value = new String[value_list.size()];
                    value_list.toArray(return_value);
                    value_list.clear();
                }

                public void end_part() {
                    value_list.add(sb.toString());
                    sb.setLength(0);
                }

                public void append(int ch) {
                    sb.append((char) ch);
                }

                public void calculate() throws IOException {
                    boolean inquote = false;
                    while (true) {
                        int ch = stream.read();
                        switch (ch) {
                            default: //regular character.
                                append(ch);
                                break;
                            case -1: //read has reached the end.
                                if ((sb.length() == 0) && (value_list.isEmpty())) {
                                    state = FINISHED;
                                } else {
                                    end();
                                    state = READY;
                                }
                                return;
                            case '\r':
                            case '\n': //end of line.
                                if (inquote) {
                                    append(ch);
                                } else {
                                    end();
                                    state = READY;
                                    return;
                                }
                                break;
                            case ',': //comma
                                if (inquote) {
                                    append(ch);
                                } else {
                                    end_part();
                                    break;
                                }
                                break;
                            case '"': //quote.
                                inquote = !inquote;
                                break;
                        }
                    }
                }

                @Override
                public boolean hasNext() {
                    if (state == UNCALCULATED) {
                        try {
                            calculate();
                        } catch (IOException ex) {
                        }
                    }
                    return state == READY;
                }

                @Override
                public String[] next() {
                    if (state == UNCALCULATED) {
                        try {
                            calculate();
                        } catch (IOException ex) {
                        }
                    }
                    state = UNCALCULATED;
                    return return_value;
                }
            };
        }
    };
}

Вы обычно обрабатываете это довольно услужливо, как:

for (String[] csv : parseCSV(stream)) {
    //<deal with parsed csv data>
}

Вся прелесть этого API стоит в довольно загадочной функции.

Ответ 9

Apache Commons CSV ➙ 12 секунд для миллионов строк

Есть ли какая-нибудь существующая библиотека, которая поможет мне ускорить процесс?

Да, проект Apache Commons CSV работает очень хорошо по моему опыту.

Вот пример приложения, которое использует библиотеку Apache Commons CSV для записи и чтения строк из 24 столбцов: целочисленное последовательное число, Instant и остальные являются случайными объектами UUID.

На 10000 строк запись и чтение занимают примерно полсекунды. Чтение включает в себя восстановление объектов Integer, Instant и UUID.

Мой пример кода позволяет включить или выключить восстановление объектов. Я бегал с миллионами строк. Это создает файл 850 мегабайт. Я использую Java 12 на MacBook Pro (Retina, 15-дюймовый, конец 2013 г.), 2,3 ГГц Intel Core i7, 16 ГБ 1600 МГц DDR3, встроенный твердотельный накопитель Apple.

Для миллиона строк десять секунд на чтение плюс две секунды на разбор:

  • Запись: PT25.994816S
  • Только чтение: PT10.353912S
  • Чтение и анализ: PT12.219364S

Исходный код представляет собой один файл .java. Имеет метод записи и метод read. Оба метода вызываются из main метода.

Я открыл BufferedReader, вызвав Files.newBufferedReader.

package work.basil.example;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVPrinter;
import org.apache.commons.csv.CSVRecord;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.Instant;
import java.util.UUID;

public class CsvReadingWritingDemo
{
    public static void main ( String[] args )
    {
        CsvReadingWritingDemo app = new CsvReadingWritingDemo();
        app.write();
        app.read();
    }

    private void write ()
    {
        Instant start = Instant.now();
        int limit = 1_000_000; // 10_000  100_000  1_000_000
        Path path = Paths.get( "/Users/basilbourque/IdeaProjects/Demo/csv.txt" );
        try (
                Writer writer = Files.newBufferedWriter( path, StandardCharsets.UTF_8 );
                CSVPrinter printer = new CSVPrinter( writer , CSVFormat.RFC4180 );
        )
        {
            printer.printRecord( "id" , "instant" , "uuid_01" , "uuid_02" , "uuid_03" , "uuid_04" , "uuid_05" , "uuid_06" , "uuid_07" , "uuid_08" , "uuid_09" , "uuid_10" , "uuid_11" , "uuid_12" , "uuid_13" , "uuid_14" , "uuid_15" , "uuid_16" , "uuid_17" , "uuid_18" , "uuid_19" , "uuid_20" , "uuid_21" , "uuid_22" );
            for ( int i = 1 ; i <= limit ; i++ )
            {
                printer.printRecord( i , Instant.now() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() , UUID.randomUUID() );
            }
        } catch ( IOException ex )
        {
            ex.printStackTrace();
        }
        Instant stop = Instant.now();
        Duration d = Duration.between( start , stop );
        System.out.println( "Wrote CSV for limit: " + limit );
        System.out.println( "Elapsed: " + d );
    }

    private void read ()
    {
        Instant start = Instant.now();

        int count = 0;
        Path path = Paths.get( "/Users/basilbourque/IdeaProjects/Demo/csv.txt" );
        try (
                Reader reader = Files.newBufferedReader( path , StandardCharsets.UTF_8) ;
        )
        {
            CSVFormat format = CSVFormat.RFC4180.withFirstRecordAsHeader();
            CSVParser parser = CSVParser.parse( reader , format );
            for ( CSVRecord csvRecord : parser )
            {
                if ( true ) // Toggle parsing of the string data into objects. Turn off ('false') to see strictly the time taken by Apache Commons CSV to read & parse the lines. Turn on ('true') to get a feel for real-world load.
                {
                    Integer id = Integer.valueOf( csvRecord.get( 0 ) ); // Annoying zero-based index counting.
                    Instant instant = Instant.parse( csvRecord.get( 1 ) );
                    for ( int i = 3 - 1 ; i <= 22 - 1 ; i++ ) // Subtract one for annoying zero-based index counting.
                    {
                        UUID uuid = UUID.fromString( csvRecord.get( i ) );
                    }
                }
                count++;
                if ( count % 1_000 == 0 )  // Every so often, report progress.
                {
                    //System.out.println( "# " + count );
                }
            }
        } catch ( IOException e )
        {
            e.printStackTrace();
        }

        Instant stop = Instant.now();
        Duration d = Duration.between( start , stop );
        System.out.println( "Read CSV for count: " + count );
        System.out.println( "Elapsed: " + d );
    }
}