【问题标题】:How to determine the delimiter in CSV file如何确定 CSV 文件中的分隔符
【发布时间】:2018-08-20 12:15:09
【问题描述】:

我有一个场景,我必须解析来自不同来源的 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,是的,逗号,冒号,分号
  • 你需要用逗号处理小数分隔符吗? (通常为什么他们在字段 sep.. 中使用逗号以外的其他内容。)
  • 所以,有时您的文件应该由, 分割,有时由( 分割? AND 每个文件都应该允许出现非分隔符字符吗?我不知道这样的东西是否可以在不链接 file-separator 的情况下进行编码。
  • @agentp,不会是小数点

标签: java csv


【解决方案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. read only first n lines from the file 的head函数的高效实现
  1. 随着您增加测试样本 n 的数量,您得到错误答案的可能性会大大降低。我经常发现 n=2 就足够了

【讨论】:

    【解决方案2】:

    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常用的csv库中没有这个检测格式的功能吗?
    • @Pavan 据我所知,Commons CSV 没有。 Commons CSV 确实有 header autodetection,但我在文档中没有看到分隔符或格式检测。
    • @vlz univocity 有这个功能,而且功能看起来很不错。也看看那个。
    【解决方案3】:

    是的,但前提是不允许分隔符作为常规文本存在

    最简单的答案是列出一个包含所有可用分隔符的列表,并尝试识别正在使用的字符。尽管如此,您必须对文件或创建文件的人设置一些限制。看看以下两种情况:

    案例 1 - file.csv 的内容

    test,test2,test3
    

    案例 2 - 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 循环中,您可以使用正则表达式。

    【讨论】:

      【解决方案4】:

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

      one,two:three
      

      这可以是逗号分隔或冒号分隔的文件。你无法分辨它是哪种类型。

      如果你能保证第一行的所有列都用引号括起来,例如,如果它总是这种格式:

      "one","two","three"
      

      那么你也许可以使用这个逻辑(虽然它不是 100% 防弹的):

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

      如果你不能保证这样的受限格式,那么最好将分隔符作为参数传递。

      然后您可以使用广为人知的开源 CSV 解析器读取文件,例如 Apache Commons CSV

      【讨论】:

        【解决方案5】:

        这取决于....

        如果您的数据集总是相同的长度和/或分隔符永远不会出现在您的数据列中,您可以只读取文件的第一行,查看它是否有渴望的分隔符,设置它,然后读取其余部分使用该分隔符的文件。

        类似

        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();
        }
        

        格雷茨凯

        【讨论】:

        • 我不能保证“从不发生”部分
        • @MeladEzzat 如果你不能保证数据列从不包含分隔符,那么你不应该使用String.split()
        • @MeladEzzat 在这种情况下 - 在任何情况下,您将如何确定如何拆分数据?! ;-) 我会尝试简化/统一要导入的格式 - 据我所知,这将是最简单的方法......
        • 投反对票的人是否愿意让我知道他/她投反对票的原因?!
        【解决方案6】:

        添加这样的条件,

        String [] country;
        if(line.contains(",")
            country = line.split(",");
        else if(line.contains(";"))
            country=line.split(";");
        

        【讨论】:

        • 如果数据包含字符之一,或者 ;
        • 如果为这些额外的字符添加条件,我想还有更多。
        猜你喜欢
        • 1970-01-01
        • 2013-02-03
        • 1970-01-01
        • 2015-11-01
        • 2021-12-17
        • 2020-11-25
        • 1970-01-01
        • 1970-01-01
        • 2014-07-08
        相关资源
        最近更新 更多