高效的CSV文本处理利器——univocity-parsers

汪臻
2023-12-01

univocity-parsers简介

工作中经常会遇到需要导出或者解析csv的需求,Java中处理csv的开源库也有很多,本文主要介绍通过univocity-parsers来解析和生成csv,univocity-parsers源码存放于github,在写这篇文章的时候univocity-parsers 最新版为2.8.4

使用详解

在详解介绍之前,我们先通过一个简单的例子来看看如何使用univocity-parsers

 

@Slf4j
public class HowToUse {

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Student {

        @Parsed(field = "userNumber")
        private String userNumber;

        @Parsed(field = "userName")
        private String userName;

        @Parsed(field = "age")
        private Integer age;

    }

    public static final String[] HEADERS = new String[]{"userNumber", "userName", "age"};

    @Test
    public void howToUse() throws IOException {
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {

            // 生成CSV内容
            Student student = new Student("1111111111111111111111", "testUser", 20);
            final CsvWriterSettings csvWriterSettings = new CsvWriterSettings();
            csvWriterSettings.setHeaderWritingEnabled(Boolean.TRUE);
            csvWriterSettings.setHeaders(HEADERS);
            csvWriterSettings.setRowWriterProcessor(new BeanWriterProcessor<>(Student.class));
            CsvWriter writer = new CsvWriter(outputStream, csvWriterSettings);
            writer.processRecord(student);
            writer.close();

            final byte[] out = outputStream.toByteArray();
            log.info("output: {}", new String(out));


            // 解析CSV内容
            CsvParserSettings csvParserSettings = new CsvParserSettings();
            final BeanListProcessor beanListProcessor = new BeanListProcessor(Student.class);
            csvParserSettings.setProcessor(beanListProcessor);
            CsvParser csvParser = new CsvParser(csvParserSettings);
            csvParser.parse(new ByteArrayInputStream(out));

            final List<Student> students = beanListProcessor.getBeans();
            final String[] headers = beanListProcessor.getHeaders();
            log.info("headers: {}", String.join(",", headers));
            log.info("students: {}", students.toString());

        }
    }
}

 

 - output: userNumber,userName,age
1111111111111111111111,testUser,20

- headers: userNumber,userName,age
- students: [HowToUse.Student(userNumber=1111111111111111111111, userName=testUser, age=20)]

这里可以看到,基于注解能够很快的生成和解析CSV内容。Parsed可以标记属性和header之间的对应关系,而Processor负责处理这两者之间的映射。

生成csv文本

setting介绍

从上面的例子可以看出,CsvWriterSettings用来进行输出的一些配置。

 

    // Format接口,这里使用的CsvFormat,下面对CsvFormat详细介绍
    private F format;

    // 默认的nullValue,输出的属性的如果是null,则使用这个值进行输出
    private String nullValue = null;

    // 一个列最大字符长度
    private int maxCharsPerColumn = 4096;

    // 最多列数
    private int maxColumns = 512;

    // 是否跳过空行,例如输出的时候如果对应的object是null,如果是true,则跳过
    private boolean skipEmptyLines = true;

    // 是否跳过尾部的空格
    private boolean ignoreTrailingWhitespaces = true;

    // 是否跳过首部的空格
    private boolean ignoreLeadingWhitespaces = true;

    /** 
    可以配置一些对属性的筛选
    ExcludeFieldNameSelector(excludeFields): 通过属性的名字来忽略一些属性的输出
    FieldNameSelector(selectFields): 通过属性的名字来选择只输出一些属性
    这里其他对FieldSelector的实现
    **/
    private FieldSelector fieldSelector = null;

    //
    private boolean autoConfigurationEnabled = true;

    // 异常处理
    private ProcessorErrorHandler<? extends Context> errorHandler;

    // 配置出现异常的时候error meesage写入到内容的长度
    private int errorContentLength = -1;

    // 是否跳过bits当做空格
    private boolean skipBitsAsWhitespace = true;

    /**
    这个是关键部分,例如我们刚才使用的BeanWriterProcessor,是通过Bean的方式输入
    也可以自己实现这个借口
    **/
    private RowWriterProcessor<?> rowWriterProcessor;

    // 如果设置成true,在写入第一行的数据的时候,如果headers设置了则会自动先写入header
    private Boolean headerWritingEnabled = null;

    // 如果写入了一个empty的string可以用这个值代替
    private String emptyValue = "";

    private boolean expandIncompleteRows = false;

    private boolean columnReorderingEnabled = false;

    // headers的配置,可以调用writer的writeHeaders方法进行写入header的操作
    private String[] headers;

    //
    private boolean escapeUnquotedValues = false;

    // 是否通过fortmat配置的quote符号,所有的是否加上quote符号,如果设置成true,默认配置符号是", 测原来列内容为xxx,变成"xxx"
    private boolean quoteAllFields = false;

    // 
    private boolean isInputEscaped = false;

    private boolean normalizeLineEndingsWithinQuotes = true;
    private char[] quotationTriggers = new char[0];

    // 如果设置成true, 如果内容 My "precious",则变成 "My ""precious"""
    private boolean quoteEscapingEnabled = false;
</code></pre>

<h3>format介绍</h3>

<pre><code class="language-java ">    // 换行符,默认为 \n
    private static final String systemLineSeparatorString;
    private static final char[] systemLineSeparator;

    // 引用符号
    private char quote = '"';
    // 转义符号
    private char quoteEscape = '"';
    // 分割符,默认为,
    private char delimiter = ',';

    private Character charToEscapeQuoteEscaping = null;

通过一个简单的例子来看看改变fortmat的结果

 

@Test
    public void excludeFields() throws IOException {
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            Student student = new Student("1111111111111111111111", "@testUser", 20);

            CsvFormat csvFormat = new CsvFormat();
            csvFormat.setQuote('@');
            csvFormat.setQuoteEscape('*');
            csvFormat.setDelimiter('|');

            final CsvWriterSettings csvWriterSettings = new CsvWriterSettings();
            csvWriterSettings.setHeaderWritingEnabled(Boolean.TRUE);
            csvWriterSettings.setQuoteAllFields(true);
            csvWriterSettings.setFormat(csvFormat);
            csvWriterSettings.setQuoteEscapingEnabled(true);
            csvWriterSettings.setHeaders(HEADERS);
            csvWriterSettings.setRowWriterProcessor(new BeanWriterProcessor<>(Student.class));
            CsvWriter writer = new CsvWriter(outputStream, csvWriterSettings);
            writer.processRecord(student);
            writer.close();

            final byte[] out = outputStream.toByteArray();
            log.info("output: {}", new String(out));
        }
    }

 

- output: @userNumber@|@userName@|@age@
@1111111111111111111111@|@*@testUser@|@20@

@1111111111111111111111@这一部分因为setQuoteAllFields设置为true,则前后加上了@
|设置成了分割符, 替换了原来的,
@*@testUser@因为里面有@,则使用QuoteEscape来进行转义,经常遇到需要用\进行转义

注解的使用

有时候需要对输出的文本进行一些处理,例如有时候如果字段对应的数字太长,用excel打开csv文件的时候,会被转成科学计数法,这个时候可能需要对输出的字段进行一些处理

 

@Slf4j
public class AnnotationTest {

    @AllArgsConstructor
    @NoArgsConstructor
    public static class Student {

        @Parsed(field = "userNumber")
        @Convert(conversionClass = HumanReadableStringOutputConvert.class)
        private String userNumber;

        @Parsed(field = "userName")
        private String userName;

        @Parsed(field = "age")
        private Integer age;
    }

    public static class HumanReadableStringOutputConvert implements Conversion<String, String> {

        private String prefix;

        private String suffix;

        public HumanReadableStringOutputConvert(String... args) {
            String defaultPrefix = "=\"";
            String defaultSuffix = "\"";
            final int length = args.length;
            if (length >= 1) {
                defaultPrefix = args[0];
            }

            if (length >= 2) {
                defaultSuffix = args[1];
            }

            this.prefix = defaultPrefix;
            this.suffix = defaultSuffix;

        }

        @Override
        public String execute(String input) {
            return null;
        }

        @Override
        public String revert(String input) {
            if (input == null) {
                return input;
            }
            return prefix + input + suffix;
        }
    }

    @Test
    public void name() throws IOException {
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {

            // 生成CSV内容
            Student student = new Student("1111111111111111111111", "testUser", 20);
            final CsvWriterSettings csvWriterSettings = new CsvWriterSettings();
            csvWriterSettings.setHeaderWritingEnabled(Boolean.TRUE);
            csvWriterSettings.setHeaders(HEADERS);
            csvWriterSettings.setRowWriterProcessor(new BeanWriterProcessor<>(Student.class));
            CsvWriter writer = new CsvWriter(outputStream, csvWriterSettings);
            writer.processRecord(student);
            writer.close();

            final byte[] out = outputStream.toByteArray();
            log.info("output: {}", new String(out));
        }
    }
}

 

21:44:03.707 [main] INFO space.chaoluo.univocity.generate.AnnotationTest - output: userNumber,userName,age
="1111111111111111111111",testUser,20

通过Convert的注解使用,自定义一个convert,重写revert方法,可以对输出的内容进行一些处理
通过上面自定义的处理之后,用excel打开文本,userNumber字段不会转成科学计数法

注: execute对应的方法是解析的时候。

解析csv文本

通过上面对生成的介绍,在解析时候很多的配置也是同样如此,只不过是通过CsvParserSettingsCsvParser去实现。

性能与扩展性

如下为我们对比 uniVocity-parsers 和 JavaCSV 的测试对比表:

文件大小JavaCSV解析耗时uniVocity-parsers解析耗时
10MB, 145453 行1138ms836ms
100MB, 809008 行23s6s
434MB, 4499959 行91s28s
1GB, 23803502 行245s70s

这里可以查看几乎所有CSV解析库的性能对比分析表,从表中可以发现,uniVocity-parsers以绝对优势领先其他库。

uniVocity-parsers在性能和灵活性方面的优势得益于如下设计和机制:

  • 以单独线程读取数据(通过调用CsvParserSettings.setReadInputOnSeparateThread() 进行设置)
  • 并行的行数据处理器 (参考 RowProcessor 的实现类 ConcurrentRowProcessor)
  • 通过继承 ColumnProcessor 类来根据业务需求处理列数据
  • 通过继承 RowProcessor 类来根据业务需求处理行数据
 类似资料: