使用Java 8 Stream解析.csv文件

11

我有一个包含500多个公司数据的.csv文件。文件中的每一行都是指向特定公司数据集的。我需要解析这个文件并从每个数据集中提取数据以调用4个不同的Web服务。

.csv文件的第一行包含列名。我正在尝试编写一个方法,它接受一个字符串参数,该参数与在.csv文件中找到的列标题相关联。

基于此参数,我希望该方法使用Java 8的Stream功能解析文件,并返回从每行/公司的列标题中获取的数据列表。

我觉得自己把它搞复杂了,但无法想到更有效的方式来实现我的目标。

任何想法或建议将不胜感激。

通过搜索stackoverflow,我发现了以下帖子,它类似但并不完全相同。 Parsing a CSV file for a unique row using the new Java 8 Streams API

    public static List<String> getData(String titleToSearchFor) throws IOException{
    Path path = Paths.get("arbitoryPath");
    int titleIndex;
    String retrievedData = null;
    List<String> listOfData = null;

    if(Files.exists(path)){ 
        try(Stream<String> lines = Files.lines(path)){
            List<String> columns = lines
                    .findFirst()
                    .map((line) -> Arrays.asList(line.split(",")))
                    .get();

            titleIndex = columns.indexOf(titleToSearchFor);

            List<List<String>> values = lines
                    .skip(1)
                    .map(line -> Arrays.asList(line.split(",")))
                    .filter(list -> list.get(titleIndex) != null)
                    .collect(Collectors.toList());

            String[] line = (String[]) values.stream().flatMap(l -> l.stream()).collect(Collectors.collectingAndThen(
                    Collectors.toList(), 
                    list -> list.toArray()));
            String value = line[titleIndex];
            if(value != null && value.trim().length() > 0){
                retrievedData = value;
            }
            listOfData.add(retrievedData);
        }
    }
    return listOfTitles;
}

感谢


2
你的代码有很多问题,你编译过了吗? - Andrew Tobilko
是的,我在Eclipse中编译了它,没有任何编译错误。但是目前我无法访问CSV文件,因此尚未能够进行适当的测试。 - Michael Heneghan
4个回答

20

你不应该重复造轮子,而应该使用常见的CSV解析库。例如,你可以直接使用Apache Commons CSV

它会为你处理许多事情,而且更易于阅读。还有OpenCSV,它更加强大,并带有基于注释的数据类映射。

 try (Reader reader = Files.newBufferedReader(Paths.get("file.csv"));
            CSVParser csvParser = new CSVParser(reader, CSVFormat.DEFAULT
                    .withFirstRecordAsHeader()        
        ) {
            for (CSVRecord csvRecord : csvParser) {
                // Access
                String name = csvRecord.get("MyColumn");
                // (..)
          }

编辑: 不管怎样,如果你真的想自己做,可以看一下这个示例。


时刻牢记:不要重复造轮子!+1 - Jorge Campos
完全同意,我甚至不应该尝试重新发明轮子,因为之前有比我更优秀的人已经做过了。不幸的是,我正在一个项目中,这个项目并不允许我导入外部库,而只能使用预安装的库。我不知道Apache有一个用于csv文件的库,这将在未来非常有用。谢谢你提供的信息 :) - Michael Heneghan
1
如果您需要速度,请查看此CSV解析器比较univocity-parsers比其他库更好地处理边缘情况。 - Jeronimo Backes
它使用流式传输吗?如果我有非常大的文件怎么办? - Md Faraz
@MdFaraz Apache Commons CSV的CSVParser实现了Iterable<CSVRecord>接口,该接口还可以通过流API进一步扩展。进一步研究的关键词:iterable to stream - aff
@MichaelHeneghan,这不是关于“比你更优秀的人”的问题。而是关于“经过尝试和测试”以及时间的问题。即使他们也不得不重新发明轮子。 - TheRealChx101

3

我成功地缩短了您的片段。

如果我理解正确,您需要某个特定列的所有值。该列的名称已给出。

想法是相同的,但我改进了从文件中读取的方式(只读取一次);删除了代码重复(例如 line.split(",")),不必要的包装在 List 中(Collectors.toList())。

// read lines once
List<String[]> lines = lines(path).map(l -> l.split(","))
                                  .collect(toList());

// find the title index
int titleIndex = lines.stream()
                      .findFirst()
                      .map(header -> asList(header).indexOf(titleToSearchFor))
                      .orElse(-1);

// collect needed values
return lines.stream()
            .skip(1)
            .map(row -> row[titleIndex])
            .collect(toList());

我有两个与问题无关的提示:

1. 你已硬编码了一个URI,最好将其值移动到常量或添加方法参数。
2. 如果您检查了相反的条件!Files.exists(path)并引发异常,则可以将主要部分移出if子句。


1

1) 你不能在流上调用多个终端操作。
但是你却调用了两次 : 使用 findFirst() 获取列名,然后使用 collect() 收集行值。在流上调用的第二个终端操作将抛出异常。

2) 而不是使用 Stream<String> lines = Files.lines(path)) 读取所有行到一个流中,你应该使用 Files.readAllLines() 分成两步,它返回一个字符串列表。
使用第一个元素来检索列名称,使用整个列表来检索与条件匹配的每行的值。

3) 你将检索分割为多个小步骤,在单个流处理中可以缩短这些步骤,它将迭代所有行,仅保留符合条件的行并将其收集。

这将得到类似下面的结果:

public static List<String> getData(String titleToSearchFor) throws IOException {
    Path path = Paths.get("arbitoryPath");

    if (Files.exists(path)) {
        List<String> lines = Files.readAllLines(path);

        List<String> columns = Arrays.asList(lines.get(0)
                                                  .split(","));

        int titleIndex = columns.indexOf(titleToSearchFor);

        List<String> values = lines.stream()
                                   .skip(1)
                                   .map(line -> Arrays.asList(line.split(",")))
                                   .map(list -> list.get(titleIndex))
                                   .filter(Objects::nonNull)
                                   .filter(s -> s.trim()
                                                 .length() > 0)
                                   .collect(Collectors.toList());

        return values;
    }

    return new ArrayList<>();

}

1 - 当然,我这么笨犯了这种错误。干杯! 2 - 我曾经考虑过这个,但这个函数将被重复使用于可能包含数千条目的其他CSV文件,所以我担心会出现OutOfMemoryError。 3 - 如果使用readAllLines,则这是另一个很好的实现选项。谢谢! - Michael Heneghan

1
作为惯例,您应该使用Jackson!查看文档 如果您希望Jackson将第一行用作标题信息:
public class CsvExample {
    public static void main(String[] args) throws IOException {
        String csv = "name,age\nIBM,140\nBurger King,76";
        CsvSchema bootstrapSchema = CsvSchema.emptySchema().withHeader();
        ObjectMapper mapper = new CsvMapper();
        MappingIterator<Map<String, String>> it = mapper.readerFor(Map.class).with(bootstrapSchema).readValues(csv);
        List<Map<String, String>> maps = it.readAll();
    }
}

或者您可以将架构定义为Java对象:

public class CsvExample {
    private static class Pojo {
        private final String name;
        private final int age;

        @JsonCreator
        public Pojo(@JsonProperty("name") String name, @JsonProperty("age") int age) {
            this.name = name;
            this.age = age;
        }

        @JsonProperty("name")
        public String getName() {
            return name;
        }

        @JsonProperty("age")
        public int getAge() {
            return age;
        }
    }

    public static void main(String[] args) throws IOException {
        String csv = "name,age\nIBM,140\nBurger King,76";
        CsvSchema bootstrapSchema = CsvSchema.emptySchema().withHeader();
        ObjectMapper mapper = new CsvMapper();
        MappingIterator<Pojo> it = mapper.readerFor(Pojo.class).with(bootstrapSchema).readValues(csv);
        List<Pojo> pojos = it.readAll();
    }
}

很遗憾,我现在的项目只允许我导入有限的库,所以希望只使用Java JDK来完成它,但是对于其他项目也要注意这一点。谢谢。 - Michael Heneghan

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