使用CsvBeanReader读取具有可变列数的CSV文件

10

我正在处理一个 .csv 文件的解析工作。我按照StackOverflow上另一个线程的建议下载了SuperCSV。现在,基本上所有事情都运行良好,但是现在我遇到了一个似乎很难解决的错误。

问题出在最后两列数据可能有值,也可能没有值。以下是一个示例 .csv 文件,第一行缺少最后一列,而第二行完全有值:

2012:07:25,11:48:20,922,"uLog.exe","",Key pressed,1246,341,-1.00,-1.00,1.00,Shift 2012:07:25,11:48:21,094,"uLog.exe","",Key pressed,1246,341,-1.00,-1.00,1.00,b,Shift

根据我对Super CSV Javadoc的理解,如果Java Bean中的列数不确定,则无法使用CsvBeanReader填充 Java Bean 。这看起来非常愚蠢,因为当初始化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)

是的,编写所有那些getter和setter真是烦死了。另外,我很抱歉,我可能在使用SuperCSV时没有完全遵守规范(例如,如果您只想要未修改的字符串应该使用哪个CellProcessor),但您能理解我的意思。此外,这段代码显然不完整。目前,我只是尝试成功地检索出一行数据。

此时,我想知道是否可以将CsvBeanReader用于我的目的。如果不行,我会有点失望,因为CsvListReader(我本来想发链接的,但StackOverflow不允许我这么做,太蠢了)几乎跟不使用API一样简单,只需要使用Scanner.next()。

如果有任何帮助,将不胜感激。提前感谢!


FYI,Super CSV 2.0.0-beta-1现已发布。它包括许多错误修复和新功能(包括Maven支持以及用于映射嵌套属性和数组/集合的新Dozer扩展)。 - James Bassett
3个回答

4

编辑:更新至Super CSV 2.0.0-beta-1

请注意,Super CSV 2.0.0-beta-1中的API已更改(代码示例基于1.52)。所有读取器上的getCSVHeader()方法现在是getHeader()(以与写入器上的writeHeader相一致)。

此外,SuperCSVException已更名为SuperCsvException


编辑:更新至Super CSV 2.1.0

自版本2.1.0起,可以使用新的executeProcessors()方法在读取CSV行后执行单元格处理器。有关更多信息,请参见项目网站上的此示例。请注意,这仅适用于CsvListReader,因为它是唯一允许变量列长度的读取器。


你是正确的 - CsvBeanReader不支持具有可变列数的CSV文件。根据大多数CSV规范(包括RFC 4180),每行的列数必须相同。

因此(作为Super CSV开发人员),我不愿将此功能添加到Super CSV中。如果您能想到一种优雅的方法来添加它,可以在项目的SourceForge网站上提出建议。这可能意味着一个扩展CsvBeanReader的新读取器:它必须将读取和映射/处理拆分为两个单独的方法(除非您知道有多少列,否则无法对bean的字段进行任何处理或映射)。

简单解决方案

如果您控制正在使用的CSV文件,则此问题的简单解决方案是在编写CSV文件时添加空白列(您的示例中的第一行末尾会有逗号 - 表示最后一列为空)。这样,您的CSV文件将有效(每行都具有相同数量的列),并且您可以像以前一样使用CsvBeanReader

如果不可能,请不要灰心!

高级解决方案

如您所知,CsvBeanReader使用名称映射将CSV文件中的每个列与bean中的字段关联,并使用CellProcessor数组处理每个列。换句话说,如果要使用它,则必须知道有多少列(以及它们代表什么)。

CsvListReader非常原始,可以读取长度不同的行(因为它不需要处理或映射它们)。另一方面,您可以通过同时使用两个阅读器来结合CsvBeanReaderCsvListReader的所有功能(如下例所示):使用CsvListReader来确定有多少列,然后使用CsvBeanReader进行处理/映射。请注意,这假定只有birthDate列可能不存在(即,如果您无法确定哪个列缺失,则无法工作)。
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类中的字段为什么不遵循常规命名约定(驼峰式)?如果您更新header数组以使用驼峰式,则您的字段也可以使用驼峰式。


他们没有普通的驼峰命名法的原因是,起初我是动态获取标题,但后来意识到一些列标题有多个单词的标题,这些标题无法转换为代码。我应该把它全部改回驼峰命名法。我正在使用uLog,并且无法控制CSV文件。 - Bryce Sandlund
抱歉,上面的评论不完整。无论如何,看起来你的代码只是从CSV文件中取出了一行,但我明白了你的意思,并且可以根据你的代码自己实现。谢谢! - Bryce Sandlund

1
使用uniVocity-parsers,您可以使用注释将列数不定的CSV文件映射到Java Bean。
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许可证)。


在字段上方添加格式注释,类似于以下内容: @Format(formats = {"dd-MMM-yyyy", "yyyy-MM-dd"}) @Parsed private Date date; - Jeronimo Backes
谢谢,这对我也起作用了 @Convert(conversionClass = DateConversion.class, args = { "dd/MM/yyyy" }) - Makky
顺便说一下,这是非常好的 API。 - Makky
@JeronimoBackes:感谢您创建这个库。我已经使用了一段时间,它非常快速和方便。我担心的是当我解析一个有100万条记录的文件并且使用BeanListProcessor<E>获取创建的bean以将其持久化到数据库时,BeanListProcessor的内存占用非常高。在BeanListProcessor中是否有任何配置可以批处理解析文件,并批量返回List<E> bean。这将有助于减少内存占用和有效的内存管理。 - Maverick
1
@maverick 使用 BeanProcessor 替代(名称中不带“list”)。您无需将所有内容加载到内存中。如果您发现迭代比使用回调更容易,请还要检查 CsvRoutines 类和迭代 bean 的方法。 - Jeronimo Backes

1

好的,SuperCSV是开源的。如果您想添加功能,例如处理具有可变数量尾随字段的输入,您基本上有两个选择:

  1. 在SourceForge网站上发布支持请求,并希望作者同意并有时间完成它
  2. 下载源代码,按照您的喜好进行更改,并将更改贡献给项目。

这就是开源的工作方式。


我真的不认为实现这个会很难,所以也许我会这么做。同时,使用CsvListReader是最好的选择吗? - Bryce Sandlund

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接