如何确定CSV文件中的分隔符

16

我有一个情景,需要解析来自不同来源的CSV文件,解析代码非常简单和直观。

        String csvFile = "/Users/csv/country.csv";
        String line = "";
        String cvsSplitBy = ",";
        try (BufferedReader br = new BufferedReader(new FileReader(csvFile))) {
            while ((line = br.readLine()) != null) {
                // use comma as separator
                String[] country = line.split(cvsSplitBy);
                System.out.println("Country [code= " + country[4] + " , name=" + country[5] + "]");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

我的问题来自于CSV分隔符字符,我有许多不同的格式,有时是,,有时是;

是否有任何方法在解析文件之前确定分隔符字符


我有许多不同的格式。允许使用哪些分隔符集?逗号、冒号、分号...? - zlakad
@zlakad,是的,逗号、冒号、分号。 - Melad Basilius
你需要处理逗号作为小数分隔符的数字吗?(通常这就是为什么他们在字段分隔符中使用其他字符的原因。) - agentp
有时候你的文件应该按,分割,有时候应该按(分割?并且每个文件都应该允许非分隔符字符的出现?我不知道是否可以在不链接文件分隔符的情况下编写这样的代码。 - zlakad
@agentp,不,它将是小数点。 - Melad Basilius
这个问题太依赖于你将拥有的文件及其格式。您需要发布更多信息,以使此问题具有意义。 - phflack
6个回答

18

univocity-parsers支持自动检测分隔符(还有行尾和引号)。只需要使用它,而不是与您的代码抗争:

CsvParserSettings settings = new CsvParserSettings();
settings.detectFormatAutomatically();

CsvParser parser = new CsvParser(settings);
List<String[]> rows = parser.parseAll(new File("/path/to/your.csv"));

// if you want to see what it detected
CsvFormat format = parser.getDetectedFormat();

免责声明:我是这个库的作者,确保了所有可能出现的边角情况都得到了考虑。它是开源且免费的(Apache 2.0许可证)

希望这可以帮助到您。


我们在Apache Common CSV库中没有这个检测格式的功能吗? - Pavan
据我所知,Commons CSV 没有这个功能。Commons CSV 有标题自动检测,但是我在文档中没有看到分隔符或格式检测的内容。 - vlz
@vlz,Univocity拥有这个功能,而且功能看起来非常不错。你也可以看一下它。 - Pavan
如果您提供了一组分隔符,那么这将很好地工作。但是,如果您设置了自动检测并且没有提供允许的分隔符集合,则可能会出现错误。例如:像这样的简单CSV(以制表符分隔),它无法识别制表符作为分隔符,例如O__\tB__。 - csf
1
@csf 这个程序旨在从包含多行的大型输入中检测分隔符。单独一行是不足以可靠地检测任何内容的。 - Jeronimo Backes
显示剩余2条评论

4

是的,但只有当分隔符字符不允许存在于常规文本中时才可以

最简单的答案是创建一个包含所有可用分隔符字符的列表,并尝试识别正在使用哪个字符。即使如此,您还需要对文件或创建它们的人/人员进行一些限制。看以下两种情况:

案例1-文件.csv的内容

test,test2,test3

案例二 - file.csv 文件的内容

test1|test2,3|test4

如果您已经知道分隔符的字符,那么您可以使用逗号,拆分第一个字符串和竖线|拆分第二个字符串,得到相同的结果。但是,如果尝试通过解析文件来确定分隔符,则两个字符串都可以使用逗号,字符进行拆分,最终结果如下所示: 情况1-使用逗号,拆分的结果
test1
test2
test3

案例2 - 使用,拆分的结果

test1|test2
3|test4

如果你不知道要使用哪个分隔符,就无法创建一个“神奇”的算法来解析每种文本组合;即使是正则表达式或计算字符出现次数也无法帮助你。

最坏情况

test1,2|test3,4|test5

通过查看文本,可以使用|作为分隔符对其进行标记化。但是,出现频率相同的,|。因此,从算法的角度来看,两种结果都是准确的:

正确结果

test1,2
test3,4
test5

错误的结果

test1
2|test3
4|test5

如果你有一套指南或者可以控制CSV文件的生成方式,那么你可以尝试使用String.contains()方法查找所使用的分隔符,并使用上述字符列表。例如:

public class MyClass {

    private List<String> delimiterList = new ArrayList<>(){{
        add(",");
        add(";");
        add("\t");
        // etc...
    }};

    private static String determineDelimiter(String text) {
        for (String delimiter : delimiterList) {
            if(text.contains(delimiter)) {
                return delimiter;
            }
        }
        return "";
    }

    public static void main(String[] args) {
        String csvFile = "/Users/csv/country.csv";
        String line = "";
        String cvsSplitBy = ",";
        String delimiter = "";
        boolean firstLine = true;
        try (BufferedReader br = new BufferedReader(new FileReader(csvFile)))  {
            while ((line = br.readLine()) != null) {
                if(firstLine) {
                    delimiter = determineDelimiter(line);
                    if(delimiter.equalsIgnoreCase("")) {
                        System.out.println("Unsupported delimiter found: " + delimiter);
                        return;
                    }
                    firstLine = false;
                }
                // use comma as separator
                String[] country = line.split(delimiter);
                System.out.println("Country [code= " + country[4] + " , name=" + country[5] + "]");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

更新

为了更加优化,determineDelimiter() 方法中可以使用正则表达式来替代 for-each 循环。


1

虽然我同意Lefteris008的观点,即无法拥有能够正确确定所有情况的函数,但我们可以拥有一种既高效又在实践中给出大部分正确结果的函数。

def head(filename: str, n: int):
    try:
        with open(filename) as f:
            head_lines = [next(f).rstrip() for x in range(n)]
    except StopIteration:
        with open(filename) as f:
            head_lines = f.read().splitlines()
    return head_lines


def detect_delimiter(filename: str, n=2):
    sample_lines = head(filename, n)
    common_delimiters= [',',';','\t',' ','|',':']
    for d in common_delimiters:
        ref = sample_lines[0].count(d)
        if ref > 0:
            if all([ ref == sample_lines[i].count(d) for i in range(1,n)]):
                return d
    return ','

我的高效实现基于

  1. 先验知识,例如您经常使用的常见分隔符列表 ',;\t |:',甚至是分隔符可能使用的可能性,以便我通常将正则表达式','放在列表的顶部
  2. 每行文本文件中分隔符出现的频率相等。这是为了解决如果我们读取单个行并且看到频率相等(误检测为Lefteris008)或者正确的分隔符在第一行中出现的频率比错误的分隔符更少的问题
  3. 一个高效的头函数的实现仅从文件中读取前n行
  1. 随着测试样本数量n的增加,获得错误答案的可能性大大降低。 我通常发现n = 2足够

1
如果分隔符可以出现在数据列中,那么您所要求的就是不可能的。例如,考虑CSV文件的第一行:
one,two:three

这可能是一个逗号分隔或冒号分隔的文件,你无法确定它的类型。
如果你能保证第一行所有列都被引号包围,例如如果它总是以下格式:
"one","two","three"

如果您想使用此逻辑(尽管它不是100%防弹),则可以这样做:

if (line.contains("\",\""))
    delimiter = ',';
else if (line.contains("\";\""))
    delimiter = ';';

如果无法保证格式的限制,那么最好将分隔符字符作为参数传递。然后,您可以使用广泛知名的开源CSV解析器(例如Apache Commons CSV)来读取文件。

-1

这要看情况……

如果你的数据集总是相同长度和/或分隔符从不出现在你的数据列中,你可以只读取文件的第一行,查找所需的分隔符,设置它,然后使用该分隔符读取文件的其余部分。

类似于以下代码:

String csvFile = "/Users/csv/country.csv";
String line = "";
String cvsSplitBy = ",";
try (BufferedReader br = new BufferedReader(new FileReader(csvFile))) {
    while ((line = br.readLine()) != null) {
        // use comma as separator
        if (line.contains(",")) {
            cvsSplitBy = ",";
        } else if (line.contains(";")) {
           cvsSplitBy = ";";
        } else {
            System.out.println("Wrong separator!");
        }
        String[] country = line.split(cvsSplitBy);
        System.out.println("Country [code= " + country[4] + " , name=" + country[5] + "]");
    }
} catch (IOException e) {
    e.printStackTrace();
}

问候 Kai


我无法保证“永远不会发生”的部分。 - Melad Basilius
1
@MeladEzzat 如果您无法保证数据列绝不包含分隔符,则不应使用String.split()。请注意。 - DodgyCodeException
在这种情况下,你会如何确定如何分割你的数据呢?!;-) 我会尝试简化/统一导入格式 - 就我所看到的而言,那将是最简单的方法... - Kai Adelmann
下投票者是否愿意告诉我为什么要给我点踩?! - Kai Adelmann

-1
添加一个类似这样的条件:
String [] country;
if(line.contains(",")
    country = line.split(",");
else if(line.contains(";"))
    country=line.split(";");

如果数据包含逗号或分号中的任何一个字符,会怎样呢? - Melad Basilius
我认为应该添加更多的else if条件来处理那些额外的字符。 - Balayesu Chilakalapudi

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