使用新的Java 8 Streams API解析唯一行的CSV文件

joh*_*co3 7 java csv java-8 java-stream

我想使用新的Java 8流API(对此我一个完整的新手)解析为一个特定的行中的CSV文件(其中以"内达"在名称列).使用以下文章获取动机,我修改并修复了一些错误,以便我可以解析包含3列的文件 - 'name','age'和'height'.

name,age,height
Marianne,12,61
Julie,13,73
Neda,14,66
Julia,15,62
Maryam,18,70
Run Code Online (Sandbox Code Playgroud)

解析代码如下:

@Override
public void init() throws Exception {
    Map<String, String> params = getParameters().getNamed();
    if (params.containsKey("csvfile")) {
        Path path = Paths.get(params.get("csvfile"));
        if (Files.exists(path)){
            // use the new java 8 streams api to read the CSV column headings
            Stream<String> lines = Files.lines(path);
            List<String> columns = lines
                .findFirst()
                .map((line) -> Arrays.asList(line.split(",")))
                .get();
            columns.forEach((l)->System.out.println(l));
            // find the relevant sections from the CSV file
            // we are only interested in the row with Neda's name
            int nameIndex = columns.indexOf("name");
            int ageIndex columns.indexOf("age");
            int heightIndex = columns.indexOf("height");
            // we need to know the index positions of the 
            // have to re-read the csv file to extract the values
            lines = Files.lines(path);
            List<List<String>> values = lines
                .skip(1)
                .map((line) -> Arrays.asList(line.split(",")))
                .collect(Collectors.toList());
            values.forEach((l)->System.out.println(l));
        }
    }        
}
Run Code Online (Sandbox Code Playgroud)

有没有办法避免在提取标题行后重新读取文件?虽然这是一个非常小的示例文件,但我将此逻辑应用于大型CSV文件.

是否有技术使用流API在提取的列名称(在文件的第一次扫描中)与剩余行中的值之间创建映射?

如何以形式返回一行List<String>(而不是List<List<String>>包含所有行).我更愿意只将行作为列名与其对应值之间的映射.(有点像JDBC中的结果集).我在这里看到了一个可能有用的Collectors.mapMerger函数,但我不知道如何使用它.

Hol*_*ger 11

BufferedReader明确使用:

List<String> columns;
List<List<String>> values;
try(BufferedReader br=Files.newBufferedReader(path)) {
    String firstLine=br.readLine();
    if(firstLine==null) throw new IOException("empty file");
    columns=Arrays.asList(firstLine.split(","));
    values = br.lines()
        .map(line -> Arrays.asList(line.split(",")))
        .collect(Collectors.toList());
}
Run Code Online (Sandbox Code Playgroud)

Files.lines(…)也适应BufferedReader.lines(…).唯一的区别是,Files.lines将配置流,以便关闭流将关闭读取器,我们在这里不需要,因为显式try(…)语句已经确保关闭BufferedReader.

请注意,在处理返回的流之后无法保证读取器的状态lines(),但我们可以执行流操作之前安全地读取行.


Tun*_*aki 6

首先,您对此代码正在读取文件两次的担忧尚未建立.实际上,Files.lines返回一个懒惰填充的行的Stream.因此,代码的第一部分只读取第一行,代码的第二部分读取其余部分(它确实第二次读取第一行,即使被忽略).引用其文档:

从文件中读取所有行作为Stream.与readAllLines此不同,此方法不会将所有行读入a List,而是在流消耗时延迟填充.

关于返回一行的第二个问题.在函数式编程中,您要做的是称为过滤.Stream API借助于提供这样的方法Stream.filter.此方法采用Predicateas参数,该函数返回true应保留的所有项,false否则返回.

在这种情况下,我们希望在名称等于时Predicate返回.这可以写成lambda表达式.true"Neda"s -> s.equals("Neda")

因此,在代码的第二部分中,您可以:

lines = Files.lines(path);
List<List<String>> values = lines
            .skip(1)
            .map(line -> Arrays.asList(line.split(",")))
            .filter(list -> list.get(0).equals("Neda")) // keep only items where the name is "Neda"
            .collect(Collectors.toList());
Run Code Online (Sandbox Code Playgroud)

但请注意,这并不能确保名称中只有一个项目"Neda",它会将所有可能的项目收集到一个项目中List<List<String>>.您可以添加一些逻辑来查找第一个项目,或者如果找不到任何项目则抛出异常,具体取决于您的业务需求.


请注意,在@ Holger的答案中Files.lines(path)直接使用BufferedReaderas 可以避免调用两次.

  • @ johnco3我同意Holger的意见,你应该开一个新问题.但是,为了给你一个提示,你可以从你得到的列表中创建地图(要创建地图,你需要遍历列表的元素,不要迭代文件的行;)). (2认同)