Быстрый синтаксический анализ 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, содержащих запятые
Ответ 2
opencsv
Посмотрите opencsv.
Это сообщение в блоге opencsv - простой анализатор 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. Ваш выбор будет зависеть от точного варианта использования (т.е. необработанные данные против привязки данных и т.д.).
Ответ 7
Я реализовал довольно быстрый парсер CSV, взгляните на https://github.com/titorenko/quick-csv-streamer
Ответ 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 );
}
}