簡體   English   中英

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

[英]Parsing a CSV file for a unique row using the new Java 8 Streams API

我正在嘗試使用新的 Java 8 Streams API(我是一個完整的新手)來解析 CSV 文件中的特定行(名稱列中帶有“Neda”的行)。 使用以下文章作為動機,我修改並修復了一些錯誤,以便我可以解析包含 3 列的文件 - 'name'、'age' 和 'height'。

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<String>的形式返回一行(而不是包含所有行的List<List<String>> )。 我寧願只找到行作為列名與其對應值之間的映射。 (有點像 JDBC 中的結果集)。 我看到一個可能在這里有用的 Collectors.mapMerger 函數,但我不知道如何使用它。

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

Files.lines(…)Files.lines(…) BufferedReader.lines(…) 唯一的區別是Files.lines將配置流,以便關閉流將關閉閱讀器,我們在這里不需要,因為顯式try(…)語句已經確保關閉BufferedReader

請注意,在處理lines()返回的流之后,無法保證讀取器的狀態,但是我們可以執行流操作之前安全地讀取行。

首先,您擔心此代碼讀取文件兩次是不成立的。 實際上, Files.lines返回一個惰性填充的行的流。 因此,代碼的第一部分僅讀取第一行,而代碼的第二部分讀取其余部分(盡管它會再次讀取第一行,即使被忽略)。 引用它的文檔:

從文件中讀取所有行作為Stream readAllLines不同,此方法不會將所有行讀入List ,而是在消耗流時延遲填充。

關於只返回一行的第二個問題。 在函數式編程中,您嘗試執行的操作稱為過濾 Stream API 在Stream.filter的幫助下提供了這樣的方法。 此方法將Predicate作為參數,這是一個函數,對於所有應保留的項目返回true ,否則返回false

在這種情況下,我們想要一個Predicate在名稱等於"Neda"時返回true 這可以寫成 lambda 表達式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());

但是請注意,這並不能確保只有一個名為"Neda" ,它會將所有可能的項目收集到List<List<String>> 根據您的業務需求,您可以添加一些邏輯來查找第一個項目,或者如果沒有找到項目則拋出異常。


仍然請注意,通過直接使用BufferedReader可以避免調用兩次Files.lines(path)的回答。

使用 CSV 處理庫

其他答案都很好。 但我建議使用 CSV 處理庫來讀取您的輸入文件。 正如其他人指出的那樣,CSV 格式並不像看起來那么簡單。 首先,這些值可能嵌套在引號中,也可能不嵌套。 CSV 有很多變體,例如在 Postgres、MySQL、Mongo、Microsoft Excel 等中使用的變體。

Java 生態系統提供了幾個這樣的庫。 我使用Apache Commons CSV

Apache Commons CSV庫不使用流。 但是,如果使用庫來執行 scut 工作,則您的工作不需要流 該庫可以輕松地從文件中循環行,而無需將大文件加載到內存中。

在提取的列名(在文件的第一次掃描中)到剩余行中的值之間創建映射?

當您調用withHeader時, Apache Commons CSV會自動執行此withHeader

以 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 ( RFC 4180 ),第一行由列名填充。

CSVFormat format =  CSVFormat.RFC4180.withHeader() ;

我們指定找到輸入文件的文件路徑。

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

我們使用 try-with-resources 語法(參見教程)來自動關閉我們的解析器。

當我們閱讀每一行時,我們檢查名稱是否為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<String>

我知道我回復得太晚了,但也許將來會對某人有所幫助

我制作了一個 csv 解析器/編寫器,由於其構建器模式而易於使用

對於您的情況:您可以使用過濾器來過濾要解析的行

csvLineFilter(Predicate<String>) 

希望你覺得它很方便,這里是源代碼https://github.com/i7paradise/CsvUtils-Java8/

我加入了一個主類Demo.java來展示它是如何工作的

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM