Java CSV解析器,支持字符串分隔符(多字符)。

20

有没有Java开源库支持CSV使用多个字符(即长度大于1的字符串)作为分隔符?

根据定义,CSV是指逗号分隔的值数据,使用单个字符(',')作为分隔符。然而,许多其他单个字符的替代方案也存在(例如tab键),这使得CSV成为了“字符分隔的值”数据(实质上是DSV: 分隔符分隔的值数据)。

主要的Java开源CSV库(如OpenCSV)支持几乎任何字符作为分隔符,但不支持字符串(多个字符)分隔符。因此,对于使用像“|||”这样的字符串分隔的数据,没有其他选择,只能预处理输入以将字符串转换为单个字符分隔符。从那时起,可以将数据解析为单个字符分隔的值。

因此,如果有一个支持字符串分隔符的库,则无需预处理即可直接使用,这意味着CSV现在代表“CharSequence分隔的值”数据. :-)


4
不太简单,因为CSV可能有带引号的字段,多行记录等。此外,引号、转义字符等有无数种选项。请看http://secretgeek.net/csv_trouble.asp,了解可能遇到的问题的有趣概述。 - PNS
你有没有查看过FlatPack?我问这个是因为根据我的过去研究,这个库似乎比OpenCSV拥有更丰富的API。 - gnat
1
这确实是一个需求,这也是为什么(除了其他很多原因)成熟的库更可取的原因之一,但我玩过的所有库似乎都只支持单个字符分隔符。 - PNS
1
@gnat FlatPack 似乎只支持单字符分隔符。 - PNS
2
正如我在问题中所说,“因此,对于使用字符串分隔的数据,例如“|||”,没有其他选择,只能预处理输入以将字符串转换为单个字符分隔符。” :-) - PNS
显示剩余3条评论
6个回答

5
这是个好问题。在我查看了javadocs并意识到opencsv仅支持字符作为分隔符而非字符串后,这个问题才变得明显起来......以下是一些建议的解决方法(Groovy示例可转换为java)。

忽略隐式中间字段

继续使用OpenCSV,但忽略空字段。显然这是一种欺骗,但对于解析行为良好的数据来说,它可以正常工作。
    CSVParser csv = new CSVParser((char)'|')

    String[] result = csv.parseLine('J||Project report||"F, G, I"||1')

    assert result[0] == "J"
    assert result[2] == "Project report"
    assert result[4] == "F, G, I"
    assert result[6] == "1"

或者

    CSVParser csv = new CSVParser((char)'|')

    String[] result = csv.parseLine('J|||Project report|||"F, G, I"|||1')

    assert result[0] == "J"
    assert result[3] == "Project report"
    assert result[6] == "F, G, I"
    assert result[9] == "1"

自己动手

使用Java String 分词器方法。

    def result = 'J|||Project report|||"F, G, I"|||1'.tokenize('|||')

    assert result[0] == "J"
    assert result[1] == "Project report"
    assert result[2] == "\"F, G, I\""
    assert result[3] == "1"

这种方法的缺点是你失去了忽略引号字符或转义分隔符的能力。

更新

不要预处理数据并修改其内容,为什么不在一个两步的过程中组合上述两种方法:

  1. 使用"自己编写"来先验证数据。拆分每行并证明它包含所需数量的字段。
  2. 使用“忽略字段”方法解析经过验证的数据,并确保已指定正确的字段数。

效率不高,但可能比编写自己的CSV解析器更容易:-)


马克,“忽略字段”的方法很聪明,但对于由多个不同字符组成的字符串无效。我想到的是使用字符串定界符的第一个(或最后一个)字符作为分隔符,然后删除每个字段开头出现的剩余定界符部分。但如果该字符是常见字符,即在多个定界符之外的地方也会遇到,则此方法无效。“自己编写选项”并不像一开始看起来那么容易。请查看secretgeek.net/csv_trouble.asp以了解一些很好的原因。 - PNS
1
我理解这两种解决方案的局限性。正如所述,“忽略字段”方法只适用于解析行为良好的数据。正如您所指出的,如果有人使用不正确数量的分隔符,则会破坏您对数据所做的假设。 “自己编写代码”选项真的只是为了证明它可以完成,除非数据非常规范,否则我不会再费心去尝试。根据我的经验,CSV数据很少是规范的...... - Mark O'Connor
你说得对。我的经验也证实了CSV数据通常不是格式良好的。+1 - PNS
就此而言,这是我的两分意见:创建一个预处理读取器,将任何字符串序列转换为字符,并将此读取器提供给openCSV。 - Luis Muñiz
Apache Commons CSV 似乎也没有这个功能。根据 withRecordSeparator 文档,目前仅支持解析包含 '\n'、'\r' 和 "\r\n" 的输入。 - Mark Teese

2

在2022年,openCSV版本5.7.1仍不支持多字符分隔符。

解决方案 - 使用appache commons-csv,版本1.9.0支持多字符分隔符!

CSVFormat.Builder.create().setDelimiter(separator);


这应该是最简单的解决方案,对我来说很有效。 - undefined
如果输入流是gzip流,commons-csv 1.10.0将会错误地解析由多个字符分隔的列,请谨慎使用。 - undefined

1

对我来说,这些解决方案都不起作用,因为它们都假设您可以将整个CSV文件存储在内存中,从而允许轻松的replaceAll类型操作。

我知道这很慢,但我选择了Scanner。它具有惊人的功能,并且使您可以使用任何字符串作为记录分隔符来制作简单的CSV阅读器。它还允许您解析非常大的CSV文件(我曾经处理过10GB的单个文件),因为您可以逐个读取记录。

Scanner s = new Scanner(inputStream, "UTF-8").useDelimiter(">|\n");

我希望有更快的解决方案,但我找到的所有库都不支持它。自2017年初以来,FasterXML一直有一个开放的票据来添加这个功能:https://github.com/FasterXML/jackson-dataformats-text/issues/14

1

使用分隔符 || 的解决方法:在所需列之间添加虚拟字段。

public class ClassName {
    @CsvBindByPosition(position = 0)
    private String column1;
    @CsvBindByPosition(position = 1)
    private String dummy1;
    @CsvBindByPosition(position = 2)
    private String column2;
    @CsvBindByPosition(position = 3)
    private String dummy2;
    @CsvBindByPosition(position = 4)
    private String column3;
    @CsvBindByPosition(position = 5)
    private String dummy5;
    @CsvBindByPosition(position = 6)
    private String column4;
}
And then parse them using 
List<ClassName> responses = new CsvToBeanBuilder<ClassName>(new FileReader("test.csv"))
                .withType(ClassName.class)
                .withSkipLines(1) // to skip header
                .withSeparator('|')
                // to parse || , we use |
                .build()
                .parse();

1
尝试使用univocity-parsers,它支持多字符分隔符,并且具有最佳性能。
至于commons-csv:如果输入流是gzip流,commons-csv 1.10.0会错误地解析由多个字符分隔的列,所以请谨慎使用。

-2

尝试使用opencsv

它可以完成你所需的一切,特别是处理带引号值中嵌入的分隔符(例如"a,b","c"解析为["a,b","c"])。

我成功地使用过它,而且我喜欢它。

编辑:

由于opencsv仅处理单个字符分隔符,因此您可以通过以下方式解决此问题:

String input;
char someCharNotInInput = '|';
String delimiter = "abc"; // or whatever
input.replaceAll(delimiter, someCharNotInInput);
new CSVReader(input, someCharNotInInput); // etc
// Put it back into each value read
value.replaceAll(someCharNotInInput, delimiter); // in case it's inside delimiters

3
OpenCSV是一个优秀的库,但它只支持单字符分隔符,不支持多字符分隔符。 - PNS
问题不在于处理任何形式的单字符分隔符(包括嵌入式分隔符),而在于处理多字符分隔符。 :-) - PNS
是的,那就是我在问题中提到的“预处理”步骤,谢谢。 - PNS
但是这样的替换不会区分引号内或引号外的定界符。 - Bart Kiers
不过,恢复原始值不会有问题。总的来说,预处理是可行但并不理想,这就是我最初发布问题的原因。 - PNS

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