Использование CsvBeanReader для чтения файла CSV с переменным количеством столбцов

Итак, я работаю над разбором файла .csv. Я воспользовался советом другой ветки где-то на StackOverflow и скачал SuperCSV. Наконец-то у меня почти все заработало, но теперь я столкнулся с ошибкой, которую трудно исправить.

Проблема возникает из-за того, что последние два столбца данных могут быть заполнены или не заполнены. Вот пример файла .csv, в первой строке которого отсутствует последний столбец, а вторая строка полностью заполнена:

2012:07:25,11:48:20,922,"uLog.exe","",Нажата клавиша,1246,341,-1.00,-1.00,1.00,Shift 2012:07:25,11:48:21,094," uLog.exe","",Нажата клавиша,1246,341,-1.00,-1.00,1.00,b,Shift

Насколько я понимаю Super CSV Javadoc, нет никакого способа заполнить Java Bean CsvBeanReader, если имеется переменное количество столбцов. Это кажется действительно глупым, потому что я чувствую, что эти отсутствующие столбцы должны иметь значение null или какое-либо другое значение по умолчанию при инициализации Bean.

Для справки, вот мой полный код парсера:

public class ULogParser {

String uLogFileLocation;
String screenRecorderFileLocation;

private static final CellProcessor[] cellProcessor = new CellProcessor[] {
    new ParseDate("yyyy:MM:dd"),
    new ParseDate("HH:mm:ss"),
    new ParseDate("SSS"),
    new StrMinMax(0, 100),
    new StrMinMax(0, 100),
    new StrMinMax(0, 100),
    new ParseInt(),
    new ParseInt(),
    new ParseDouble(),
    new ParseDouble(),
    new ParseDouble(),
    new StrMinMax(0, 100),
    new StrMinMax(0, 100),
};

public String[] header = {"Date", "Time", "Msec", "Application", "Window", "Message", "X", "Y", "RelDist", "TotalDist", "Rate", "Extra1", "Extra2"}; 

public ULogParser(String uLogFileLocation, String screenRecorderFileLocation)
{
    this.uLogFileLocation = uLogFileLocation;
    this.screenRecorderFileLocation = screenRecorderFileLocation;
}

public void parse()
{
    try {
        ICsvBeanReader reader = new CsvBeanReader(new BufferedReader(new FileReader(uLogFileLocation)), CsvPreference.STANDARD_PREFERENCE);
        reader.getCSVHeader(false); //parse past the header
        Entry entry;
        entry = reader.read(Entry.class, header, cellProcessor);
        System.out.println(entry.Application);
    } catch (FileNotFoundException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

public void sendToDB()
{
    Query query = new Query();
}
}

И код для класса Entry:

public class Entry
{
private Date Date;
private Date Time;
private Date Msec;
private String Application;
private String Window;
private String Message;
private int X;
private int Y;
private double RelDist;
private double TotalDist;
private double Rate;
private String Extra1;
private String Extra2;

public Date getDate() { return Date; }
public Date getTime() { return Time; }
public Date getMsec() { return Msec; }
public String getApplication() { return Application; }
public String getWindow() { return Window; }
public String getMessage() { return Message; }
public int getX() { return X; }
public int getY() { return Y; }
public double getRelDist() { return RelDist; }
public double getTotalDist() { return TotalDist; }
public double getRate() { return Rate; }
public String getExtra1() { return Extra1; }
public String getExtra2() { return Extra2; }

public void setDate(Date Date) { this.Date = Date; }
public void setTime(Date Time) { this.Time = Time; }
public void setMsec(Date Msec) { this.Msec = Msec; }
public void setApplication(String Application) { this.Application = Application; }
public void setWindow(String Window) { this.Window = Window; }
public void setMessage(String Message) { this.Message = Message; }
public void setX(int X) { this.X = X; }
public void setY(int Y) { this.Y = Y; }
public void setRelDist(double RelDist) { this.RelDist = RelDist; }
public void setTotalDist(double TotalDist) { this.TotalDist = TotalDist; }
public void setRate(double Rate) { this.Rate = Rate; }
public void setExtra1(String Extra1) { this.Extra1 = Extra1; }
public void setExtra2(String Extra2) { this.Extra2 = Extra2; }

public Entry(){}
}

И исключение, которое я получаю (обратите внимание, что это строка, отличная от моего примера выше, в которой отсутствуют оба последних двух столбца):

Exception in thread "main" The value array (size 12)  must match the processors array (size 13): You are probably reading a CSV line with a different number of columns than the number of cellprocessors specified context: Line: 2 Column: 0 Raw line:
[2012:07:25, 11:48:05, 740, uLog.exe,  , Logging started, -1, -1, -1.00, -1.00, -1.00, ]
 offending processor: null
    at org.supercsv.util.Util.processStringList(Unknown Source)
    at org.supercsv.io.CsvBeanReader.read(Unknown Source)
    at processing.ULogParser.parse(ULogParser.java:59)
    at ui.ParseImplicitData.main(ParseImplicitData.java:15)

Да, писать все эти геттеры и сеттеры было головной болью. Кроме того, я прошу прощения, у меня, вероятно, нет идеального соглашения в моем использовании SuperCSV (например, какой CellProcessor использовать, если вы просто хотите немодифицированную строку), но вы поняли идею. Кроме того, этот код явно не является полным. На данный момент я просто пытаюсь успешно получить строку данных.

На данный момент мне интересно, возможно ли использование CsvBeanReader для моих целей. Если нет, то я немного разочарован, так как CsvListReader (я бы разместил гиперссылку, но StackOverflow тоже не позволяет мне, тоже тупой) почти так же просто, как вообще не использовать API, а просто использовать Scanner.next ().

Любая помощь будет оценена по достоинству. Заранее спасибо!


person Bryce Sandlund    schedule 26.07.2012    source источник
comment
К вашему сведению, Super CSV 2.0.0-beta-1 уже вышла. Он включает в себя множество исправлений ошибок и новых функций (включая поддержку Maven и новое расширение Dozer для сопоставления вложенных свойств и массивов/коллекций).   -  person James Bassett    schedule 18.09.2012


Ответы (3)


Изменить: обновление для Super CSV 2.0.0-beta-1

Обратите внимание, что API изменился в Super CSV 2.0.0-beta-1 (пример кода основан на 1.52). Метод getCSVHeader() для всех читателей теперь getHeader() (чтобы соответствовать writeHeader для писателей).

Кроме того, SuperCSVException был переименован в SuperCsvException.


Изменить: обновление для Super CSV 2.1.0

Начиная с версии 2.1.0 можно запускать процессоры ячеек после чтения строки CSV с помощью нового метода executeProcessors(). Для получения дополнительной информации см. этот пример на веб-сайте проекта. Обратите внимание, что это относится только к CsvListReader, так как это единственный ридер, который допускает переменную длину столбца.


Вы правы - CsvBeanReader не поддерживает файлы CSV с переменным количеством столбцов. Согласно большинству спецификаций CSV (включая RFC 4180), количество столбцов должно быть одинаковым на всех ряд.

По этой причине (как разработчик Super CSV) я не хочу добавлять эту функциональность в Super CSV. Если вы можете придумать элегантный способ добавить его, не стесняйтесь вносить предложения на сайт проекта SourceForge. Это, вероятно, означало бы новый ридер, который расширяет CsvBeanReader: ему пришлось бы разделить чтение и сопоставление/обработку на два отдельных метода (вы не можете выполнять какую-либо обработку или сопоставление с полями bean-компонента, если вы не знаете, сколько столбцов есть ).

Простое решение

Простое решение этой проблемы (если у вас есть контроль над CSV-файлом, с которым вы работаете) состоит в том, чтобы просто добавить пустой столбец при записи CSV-файла (первая строка в вашем примере будет иметь запятую в конце, чтобы указать последний столбец пустой). Таким образом, ваш CSV-файл будет действительным (в каждой строке будет одинаковое количество столбцов), и вы сможете использовать CsvBeanReader, как вы уже делаете.

Если это невозможно, то еще не все потеряно!

Необычное решение

Как вы, вероятно, понимаете, CsvBeanReader использует сопоставление имен, чтобы связать каждый столбец в файле CSV с полем в вашем bean-компоненте, и массив CellProcessor для обработки каждого столбца. Другими словами, вы должны знать, сколько есть столбцов (и что они представляют), если хотите их использовать.

CsvListReader, с другой стороны, очень примитивен и может читать строки различной длины (потому что ему не нужно их обрабатывать или отображать).

Таким образом, вы можете комбинировать все функции CsvBeanReader с CsvListReader (как это сделано в следующем примере), читая файл с помощью обоих считывателей параллельно: используя CsvListReader для определения количества столбцов и CsvBeanReader для обработки/отображения.

Обратите внимание, что это предполагает, что может отсутствовать только столбец BirthdayDate (т. е. это не сработает, если вы не можете определить, какой столбец отсутствует).

package example;

import java.io.StringReader;
import java.util.Date;

import org.supercsv.cellprocessor.ParseDate;
import org.supercsv.cellprocessor.ift.CellProcessor;
import org.supercsv.exception.SuperCSVException;
import org.supercsv.io.CsvBeanReader;
import org.supercsv.io.CsvListReader;
import org.supercsv.io.ICsvBeanReader;
import org.supercsv.io.ICsvListReader;
import org.supercsv.prefs.CsvPreference;

public class VariableColumns {

    private static final String INPUT = "name,birthDate,city\n"
        + "John,New York\n" 
        + "Sally,22/03/1974,London\n" 
        + "Jim,Sydney";

    // cell processors
    private static final CellProcessor[] NORMAL_PROCESSORS = 
    new CellProcessor[] {null, new ParseDate("dd/MM/yyyy"), null };
    private static final CellProcessor[] NO_BIRTHDATE_PROCESSORS = 
    new CellProcessor[] {null, null };

    // name mappings
    private static final String[] NORMAL_HEADER = 
    new String[] { "name", "birthDate", "city" };
    private static final String[] NO_BIRTHDATE_HEADER = 
    new String[] { "name", "city" };

    public static void main(String[] args) {

        // using bean reader and list reader together (to read the same file)
        final ICsvBeanReader beanReader = new CsvBeanReader(new StringReader(
                INPUT), CsvPreference.STANDARD_PREFERENCE);
        final ICsvListReader listReader = new CsvListReader(new StringReader(
                INPUT), CsvPreference.STANDARD_PREFERENCE);

        try {
            // skip over header
            beanReader.getCSVHeader(true);
            listReader.getCSVHeader(true);

            while (listReader.read() != null) {

                final String[] nameMapping;
                final CellProcessor[] processors;

                if (listReader.length() == NORMAL_HEADER.length) {
                    // all columns present - use normal header/processors
                    nameMapping = NORMAL_HEADER;
                    processors = NORMAL_PROCESSORS;

                } else if (listReader.length() == NO_BIRTHDATE_HEADER.length) {
                    // one less column - birth date must be missing
                    nameMapping = NO_BIRTHDATE_HEADER;
                    processors = NO_BIRTHDATE_PROCESSORS;

                } else {
                    throw new SuperCSVException(
                            "unexpected number of columns: "
                                    + listReader.length());
                }

                // can now use CsvBeanReader safely 
                // (we know how many columns there are)
                Person person = beanReader.read(Person.class, nameMapping,
                        processors);

                System.out.println(String.format(
                        "Person: name=%s, birthDate=%s, city=%s",
                        person.getName(), person.getBirthDate(),
                        person.getCity()));

            }
        } catch (Exception e) {
            // handle exceptions here
            e.printStackTrace();
        } finally {
            // close readers here
        }
    }

    public static class Person {

        private String name;
        private Date birthDate;
        private String city;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Date getBirthDate() {
            return birthDate;
        }

        public void setBirthDate(Date birthDate) {
            this.birthDate = birthDate;
        }

        public String getCity() {
            return city;
        }

        public void setCity(String city) {
            this.city = city;
        }
    }

}

Надеюсь, это поможет.

О, и есть ли причина, по которой поля в вашем классе Entry не следуют обычным соглашениям об именах (camelCase)? Если вы обновите свой массив header, чтобы использовать верблюжий регистр, то ваши поля также могут быть верблюжьими.

person James Bassett    schedule 28.07.2012
comment
Причина, по которой у них нет обычного верблюжьего регистра, заключается в том, что сначала я получал заголовок динамически, но затем понял, что некоторые заголовки столбцов имеют названия из нескольких слов, которые не преобразуются в код. Я, вероятно, должен изменить все это обратно на camelcase. Я работаю с uLog и не контролирую CSV-файл. - person Bryce Sandlund; 01.08.2012
comment
Извините, случайно разместил неполный комментарий выше, в любом случае, похоже, что ваш код берет только одну строку из файла csv, но я понял идею и могу сделать свою собственную реализацию из того, что у вас есть. Спасибо! - person Bryce Sandlund; 01.08.2012

Что ж, SuperCSV — это программа с открытым исходным кодом. Если вы хотите добавить функциональные возможности, такие как обработка ввода с переменным количеством конечных полей, у вас есть два основных варианта:

  1. Разместите запрос в службу поддержки на сайте SourceForge и надейтесь, что автор согласится и у него будет время это сделать.
  2. Загрузите исходный код, измените его по своему вкусу и внесите изменения в проект.

Вот как работает Open Source.

person Jim Garrison    schedule 26.07.2012
comment
Честно говоря, я не думаю, что это будет так сложно реализовать, так что, возможно, я это сделаю. В то же время, является ли использование CsvListReader лучшей альтернативой? - person Bryce Sandlund; 27.07.2012

Используя uniVocity-parsers, вы можете сопоставлять CSV-файлы с различным количеством столбцов с Java-бинами. Использование аннотаций:

class TestBean {

// if the value parsed in the quantity column is "?" or "-", it will be replaced by null.
@NullString(nulls = { "?", "-" })
// if a value resolves to null, it will be converted to the String "0".
@Parsed(defaultNullRead = "0")
private Integer quantity;   // The attribute type defines which conversion will be executed when processing the value.
// In this case, IntegerConversion will be used.
// The attribute name will be matched against the column header in the file automatically.

@Trim
@LowerCase
// the value for the comments attribute is in the column at index 4 (0 is the first column, so this means fifth column in the file)
@Parsed(index = 4)
private String comments;

// you can also explicitly give the name of a column in the file.
@Parsed(field = "amount")
private BigDecimal amount;

@Trim
@LowerCase
// values "no", "n" and "null" will be converted to false; values "yes" and "y" will be converted to true
@BooleanString(falseStrings = { "no", "n", "null" }, trueStrings = { "yes", "y" })
@Parsed
private Boolean pending;
...
}

Чтобы преобразовать ваш CSV в список TestBean экземпляров:

// BeanListProcessor converts each parsed row to an instance of a given class, then stores each instance into a list.
BeanListProcessor<TestBean> rowProcessor = new BeanListProcessor<TestBean>(TestBean.class);
CsvParserSettings parserSettings = new CsvParserSettings();
parserSettings.setRowProcessor(rowProcessor);
//Uses the first valid row of the CSV to assign names to each column
parserSettings.setHeaderExtractionEnabled(true);

CsvParser parser = new CsvParser(parserSettings);
parser.parse(new FileReader(yourFile));

// The BeanListProcessor provides a list of objects extracted from the input.
List<TestBean> beans = rowProcessor.getBeans();

Раскрытие информации: я являюсь автором этой библиотеки. Это бесплатно и с открытым исходным кодом (лицензия Apache V2.0).

person Jeronimo Backes    schedule 16.11.2014
comment
Добавьте аннотацию Формат над полем. Что-то вроде этого: @Format(formats = {dd-MMM-yyyy, yyyy-MM-dd}) @Parsed private Date date; - person Jeronimo Backes; 27.04.2016
comment
Спасибо, это также сработало для меня @Convert(conversionClass = DateConversion.class, args = {dd/MM/yyyy}) - person Makky; 27.04.2016
comment
Кстати, это очень хороший API. - person Makky; 27.04.2016
comment
@JeronimoBackes: Спасибо за создание этой библиотеки. Я использую эту библиотеку в течение некоторого времени, и она довольно быстрая и удобная. Я думаю, что меня беспокоит то, что когда я анализирую файл с 1 миллионом записей и использую BeanListProcessor‹E› для извлечения созданного компонента для его сохранения в базе данных, объем памяти BeanListProcessor очень высок. Есть ли какая-либо конфигурация в BeanListProcessor для анализа файла в пакетном режиме и возврата bean-компонентов List‹E› в пакетном режиме. Это поможет уменьшить объем памяти и повысить эффективность управления памятью. - person Maverick; 16.08.2016
comment
@maverick вместо этого использует BeanProcessor (без списка в имени). Вам не нужно загружать все в память. Также проверьте класс CsvRoutines и метод для итерации по bean-компонентам, если вам будет проще выполнять итерацию вместо использования обратного вызова. - person Jeronimo Backes; 16.08.2016