Быстрый анализ 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

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

7 ответов

Решение

Apache Commons CSV

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

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

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

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

opencsv

Посмотрите на opencsv.

Этот пост в блоге, opencsv - простой анализатор CSV, имеет пример использования.

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

Есть тест на github

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

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

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

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

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

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

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

opencsv

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

Quirk-CSV


Новый ребенок в квартале. Он использует аннотации java и построен на apache-csv, одной из самых быстрых библиотек для синтаксического анализа csv.

Эта библиотека также является потокобезопасной, если вы хотите повторно использовать CSVProcessor, вы можете и должны.

Пример:

Pojo

@CSVReadComponent(type = CSVType.NAMED)
@CSVWriteComponent(type = CSVType.ORDER)
public class Pojo {
    @CSVWriteBinding(order = 0)
    private String name;

    @CSVWriteBinding(order = 1)
    @CSVReadBinding(header = "age")
    private Integer age;

    @CSVWriteBinding(order = 2)
    @CSVReadBinding(header = "money")
    private Double money;

    @CSVReadBinding(header = "name")
    public void setA(String name) {
        this.name = name;
    }

    @Override
    public String toString() {

    return "Name: " + name + System.lineSeparator() + "\tAge: " + age + System.lineSeparator() + "\tMoney: "
            + money;
}}

Главный

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.*;


public class SimpleMain {
public static void main(String[] args) {
    String csv = "name,age,money" + System.lineSeparator() + "Michael Williams,34,39332.15";

    CSVProcessor processor = new CSVProcessor(Pojo.class);
    List<Pojo> list = new ArrayList<>();
    try {
        list.addAll(processor.parse(new StringReader(csv)));
        list.forEach(System.out::println);

        System.out.println();

        StringWriter sw = new StringWriter();
        processor.write(list, sw);
        System.out.println(sw.toString());
    } catch (IOException e) {
    }


}}

Поскольку он построен на основе apache-csv, вы можете использовать мощный инструмент CSVFormat. Допустим, разделителем для csv являются трубы (|) вместо запятых (,), которые вы могли бы, например:

CSVFormat csvFormat = CSVFormat.DEFAULT.withDelimiter('|');
List<Pojo> list = processor.parse(new StringReader(csv), csvFormat);

Еще одним преимуществом является наследование.

Для других примеров обработки чтения / записи непримитивных данных

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

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

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

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

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

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

В моем примере кода вы можете включать и выключать воссоздание объектов. Я запустил оба с миллионом строк. Это создает файл размером 850 мегабайт. Я использую Java12 на MacBook Pro (Retina, 15 дюймов, конец 2013 г.), Intel Core i7 2,3 ГГц, 16 ГБ DDR3 1600 МГц, встроенный твердотельный накопитель 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 );
    }
}

Для скорости вы не хотите использовать 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 стоит в довольно загадочной функции.

Другие вопросы по тегам