提问者:小点点

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


我正在尝试使用新的Java8 StreamsAPI(我是一个完全的新手)来解析CSV文件中的特定行(名称列中有“Neda”的行)。使用以下文章作为动机,我修改并修复了一些错误,以便我可以解析包含3列的文件-“名称”、“年龄”和“高度”。

name,age,height
Marianne,12,61
Julie,13,73
Neda,14,66
Julia,15,62
Maryam,18,70

解析代码如下:

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

有什么方法可以避免在提取标题行之后重新读取文件吗?虽然这是一个非常小的示例文件,但我将把这个逻辑应用于一个大的CSV文件。

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

如何以List的形式只返回一行


共3个答案

匿名用户

显式使用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());
}

File.行(…)也使用BufferedReader.行(…)。唯一的区别是Files.行将配置流,以便关闭流将关闭阅读器,这里我们不需要,因为显式的try(…)语句已经确保关闭BufferedReader

请注意,在line()返回的流被处理后,不能保证阅读器的状态,但我们可以在执行流操作之前安全地读取行。

匿名用户

首先,您担心这段代码会读取文件两次是没有根据的。实际上,Files. line返回的是惰性填充的行的Stream。因此,代码的第一部分只读取第一行,代码的第二部分读取其余部分(即使被忽略,它也会第二次读取第一行)。引用其留档:

将文件中的所有行作为Stream读取。与readAllLines不同,此方法不会将所有行读取到List中,而是在使用流时延迟填充。

关于仅返回一行的第二个问题。在函数式编程中,您尝试做的称为过滤。StreamAPI在Stream. filter的帮助下提供了这样一个方法。此方法采用Predicate作为参数,该函数返回所有应保留的项目的true,否则返回false

在这种情况下,我们需要一个谓词,当名称等于“Neda”时,它将返回true。这可以写成lambda表达式s-

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

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

但是请注意,这并不能确保只有一个名称为“Neda”的项目,它将所有可能的项目收集到一个列表中

仍然请注意,可以通过直接使用BufferedReader来避免调用两次File. line(path),如@Holger的回答。

匿名用户

其他答案很好。但是我建议使用CSV处理库来读取您的输入文件。正如其他人所指出的,CSV格式并不像看起来那么简单。首先,值可能嵌套在引号中,也可能不嵌套在引号中。CSV有许多变体,例如Postgres、MySQL、Mongo、Microsoft Excel等中使用的那些。

Java生态系统提供了几个这样的库。我使用Apache CommonsCSV。

Apache CommonsCSV库不使用流。但是,如果使用库来执行scut工作,则您的工作不需要流。该库可以轻松地从文件中循环行,而无需将大文件加载到内存中。

在提取的列名(在文件的第一次扫描中)与其余行中的值之间创建映射?

Apache CommonsCSV在调用with Header时会自动执行此操作。

以List的形式只返回一行

是的,很容易做到。

根据您的要求,我们可以使用特定行的3个字段值中的每一个填充List。这个List充当一个元组。

List < String > tuple = List.of();  // Our goal is to fill this list of values from a single row. Initialize to an empty nonmodifiable list.

我们指定输入文件的格式:标准CSV(RFC4180),第一行由列名填充。

CSVFormat format =  CSVFormat.RFC4180.withHeader() ;

我们指定在哪里找到我们的输入文件的文件路径。

Path path = Path.of("/Users/basilbourque/people.csv");

我们使用try with资源语法(请参阅教程)来自动关闭我们的解析器。

当我们在每一行中阅读时,我们检查名称是否为Neda。如果找到,我们将使用该行的字段值报告元组List。我们中断循环。我们使用List. of方便地返回一些未知具体类的List对象,这意味着您不能从列表中添加或删除元素。

try (
        CSVParser parser =CSVParser.parse( path , StandardCharsets.UTF_8, format ) ;
)
{
    for ( CSVRecord record : parser )
    {
        if ( record.get( "name" ).equals( "Neda" ) )
        {
            tuple = List.of( record.get( "name" ) , record.get( "age" ) , record.get( "height" ) );
            break ;
        }
    }
}
catch ( FileNotFoundException e )
{
    e.printStackTrace();
}
catch ( IOException e )
{
    e.printStackTrace();
}

如果我们成功了,我们应该在List中看到一些项目。

if ( tuple.isEmpty() )
{
    System.out.println( "Bummer. Failed to report a row for `Neda` name." );
} else
{
    System.out.println( "Success. Found this row for name of `Neda`:" );
    System.out.println( tuple.toString() );
}

运行时。

成功。找到Neda名称的这一行:

[内达,14,66]

与其使用List作为元组,我建议您定义一个Person类来用适当的数据类型表示这些数据。我们这里的代码将返回一个Person实例而不是List