Apache commons CSV игнорирует поврежденные или недопустимые записи в файле csv и продолжает синтаксический анализ

#java #csv #parsing #apache-commons-csv

#java #csv #синтаксический анализ #apache-commons-csv

Вопрос:

Я пытаюсь проанализировать почти действительный файл CSV, содержащий данные, которые на 99,9% верны и действительны. Однако на полпути есть пара недопустимых записей (слишком много кавычек), например

 a,b,"c",d 
a,b,""c""",d
 

Мой код

     try (Reader reader = new BufferedReader(new FileReader(file), BUFFERED_READER_SIZE);
         CSVParser csvParser = new CSVParser(reader, CSVFormat.EXCEL)
    ) {
        Iterator<CSVRecord> iterator = csvParser.iterator();
        CSVRecord record;
        while (iterator.hasNext()) {
            try {
                record = iterator.next();
            } catch (IllegalStateException e) {
            }
        }
    } catch (IOException e) {
    }
 

Как мне проанализировать CSV, чтобы при обнаружении недопустимой строки / записи он просто пропускал ее и переходил к следующей строке?

Ответ №1:

Я не думаю, что вы можете многое сделать, чтобы обойти это. CSVParser является конечным классом и не позволяет контролировать способ, которым он анализирует базовый поток. Однако это можно обойти, используя пользовательский итератор, который справится с этой задачей.

 public final class Csv {

    private Csv() {
    }

    public interface ICsvParserFactory {

        @Nonnull
        CSVParser createCsvParser(@Nonnull Reader lineReader);

    }

    public static Stream<CSVRecord> tryParseLinesLeniently(final BufferedReader bufferedReader, final ICsvParserFactory csvParserFactory) {
        return bufferedReader.lines()
                .map(line -> {
                    try {
                        return csvParserFactory.createCsvParser(new StringReader(line))
                                .iterator()
                                .next();
                    } catch ( final IllegalStateException ex ) {
                        return null;
                    }
                })
                .filter(Objects::nonNull)
                .onClose(() -> {
                    try {
                        bufferedReader.close();
                    } catch ( final IOException ex ) {
                        throw new RuntimeException(ex);
                    }
                });
    }

}
 

Однако я не думаю, что это хорошая идея в любом случае:

  • Он не может вернуть CSVParser экземпляр.
  • Он может возвращать Iterator<CSVRecord> вместо Stream<CSVRecord> (и сохранять filter операцию), но я просто нахожу потоки более простыми в реализации.
  • Он создает новый анализатор CSV для каждой строки, поэтому этот метод создает много объектов для документа CSV, который содержит «слишком много» строк. Средство чтения строк, вероятно, можно использовать повторно.
  • Вся идея метода заключается в том, что он, не являясь анализатором CSV, предполагает, что каждая строка содержит только одну строку (я действительно не помню, разрешают ли CSV / TSV многострочные записи), поэтому он нарушает правила синтаксического анализа CSV просто по замыслу. Он пока не поддерживает заголовки (но может быть легко улучшен).
 final Csv.ICsvParserFactory csvParserFactory = lineReader -> {
    try {
        return new CSVParser(lineReader, CSVFormat.EXCEL);
    } catch ( final IOException ex ) {
        throw new RuntimeException(ex);
    }
};
try ( final Stream<CSVRecord> csvRecords = Csv.tryParseLinesLeniently(new BufferedReader(reader), csvParserFactory) ) {
    csvRecords.forEachOrdered(System.out::println);
}
 

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


Редактировать 1

В приведенном выше коде есть ошибка реализации: ВСЕ записи, возвращаемые из потока, теперь имеют recordNumber значение 1 .

Теперь я действительно считаю, что запрос не может быть исправлен с помощью синтаксического анализатора Apache Commons CSV, поскольку единственный CSVRecord конструктор также является закрытым для пакета и не может быть создан вне этого пакета, если не использовать отражение или не вторгаться в его объявляющий пакет.

Извините, вам нужно либо исправить свои документы CSV, либо использовать другой анализатор, который может анализировать «более мягко».

Комментарии:

1. CSV (большинство диалектов) допускает поля, содержащие новую строку, если поле заключено в кавычки. Но соблюдение этого сделало бы невозможным выполнение запроса «игнорировать строки со слишком большим количеством кавычек» (поскольку вы не можете точно определить, где находится конец строки), поэтому необходимо предположить, что в этом файле нет многострочных строк.

2. @rici Правильно, фрагментация ввода построчно нарушает грамматику CSV и не учитывает номер записи, который я только что нашел в CSVRecord классе.

3. Вместо того, чтобы передавать ввод по строке за раз, вы можете попытаться вставить кавычку в конце строк с нечетным числом кавычек. Вставка цитаты может быть немного медленной, но она должна быть нечастой.

Ответ №2:

Я использую Apache CSV commons версии 1.9.0, и я могу продолжить извлечение строк после недопустимых строк, просто «поглощая» исключение и просто продолжая. Имейте в виду, что hasNext() метод фактически предварительно извлекает следующую строку, поэтому он может выдавать IllegalStateException как next() метод, так и .

Если вы обработаете исключение, следующим CSVRecord извлеченным будет искаженная версия недопустимой строки, поэтому вы захотите ее пропустить. Я не могу опубликовать свой код, поскольку это IP-адрес моего работодателя, но, надеюсь, это поможет.