使用 Java 流从 csv 文件中过滤

Filtering from csv files using Java stream

我有一个包含 SW 字符的 csv 文件,我想使用 java 流找到最重的字符。这是文件示例:

name;height;mass;hair_color;skin_color;eye_color;birth_year;gender
Luke Skywalker;172;77;blond;fair;blue;19BBY;male
C-3PO;167;75;n/a;gold;yellow;112BBY;n/a
R2-D2;96;32;n/a;white, blue;red;33BBY;n/a
Darth Vader;202;136;none;white;yellow;41.9BBY;male
Leia Organa;150;49;brown;light;brown;19BBY;female
Owen Lars;178;120;brown, grey;light;blue;52BBY;male
Beru Whitesun lars;165;75;brown;light;blue;47BBY;female
Grievous;216;159;none;brown, white;green, yellow;unknown;male
Finn;unknown;unknown;black;dark;dark;unknown;male
Rey;unknown;unknown;brown;light;hazel;unknown;female
Poe Dameron;unknown;unknown;brown;light;brown;unknown;male

预期输出是字符串“Grievous”。

最初我想创建一个字符 class,我可以在其中存储数据并在拆分行后使用对象而不是字符串数组。但是,每个值都可能是未知的或 n/a,因此不太确定如何解决它。有没有办法只使用流来实现这一点?

这是我的初步尝试,将每一行映射到具有字段 nameheight 的新 Person 对象,但是这种方法不能正确处理未知输入。

public static String getHeaviestCharacter(String file) throws IOException {
    return Files.lines(Paths.get(file))
            .map(line -> line.split(";"))
            .map(part -> new Person(part[0], part[2]))
            .max((p1, p2) -> Integer.compare(p1.getWeight(), p2.getWeight()))
            .map(p1.getName());
}

我不建议使用 Streams 执行此操作,而是使用一些 CSV 库,因为它更安全。


public static void main(String[] args) {
    try {
        BufferedReader reader = new BufferedReader(new FileReader(new File("characters.csv")));

        // Skip first line
        reader.readLine();

        Optional<String> optionalHeaviestCharacter = getHeaviestCharactersName(reader.lines());

        System.out.println(optionalHeaviestCharacter);

    } catch (IOException e) {
        e.printStackTrace();
    }
}

public static Optional<String> getHeaviestCharactersName(Stream<String> lineStream) {
    return lineStream
            .map(lineString -> lineString.split(";")) // map every line string to an array with all values
            .filter(values -> values[2].matches("[0-9]+")) // filter out characters with a non-number value as a mass
            .max((values1, values2) -> Integer.compare(Integer.parseInt(values1[2]), Integer.parseInt(values2[2]))) // get element with maximum mass
            .map(heaviestValues -> heaviestValues[0]); // map values array of heaviest character to its name
}

首先我们读取文件,我将其命名为 characters.csv。您可能需要编辑文件路径以指向您的文件。

BufferedReader reader = new BufferedReader(new FileReader(new File("characters.csv")));

然后我们通过调用reader.lines()方法

从文件中读取所有行,每一行作为Stream<String>中的一个字符串

函数 getHeaviestCharactersName 将 return 和 Optional<String>。 Optional 将为空,例如当所有字符都具有 unknown/invalid 质量或根本没有字符时。

如果您认为总会有至少一个字符具有有效质量,那么您只会得到具有 optionalHeaviestCharacter.get() 的最重字符的名称。 否则你必须先检查 Optional 是否为空:

if (optionalHeaviestCharacter.isEmpty()) {
    System.out.println("Could not find a character with the heaviest mass");
} else {
    System.out.println("Heaviest character is " + optionalHeaviestCharacter.get());
}

您可以通过电话获取姓名

正如其他人指出的那样,我怀疑流是解决您的特定问题的最佳方法。但既然你问了,只是为了好玩,我试了一下。经过web-searching和trial-and-error之后,我似乎找到了使用流的解决方案。

我们使用NIO.2 classes Path & Files打开数据文件

我们通过调用Files.lines来定义一个流。

我们通过调用 Stream#skip.

省略了 header 行

您的某些输入行在我们的目标第三字段中具有 non-numeric 值“未知”。所以我们调用 Stream#filter 来忽略这些行。我们通过使用 String#split 提取第三个字段,同时传递烦人的 zero-based 索引号 2.

要获得第三列中的最高数字,我们需要进行排序。为了排序,我们提取了 Comparator created via Comparator.comparingInt. To get the needed int value, we parse the text of the third field using Integer.parseInt.

中的第三个字段

排序后,我们需要访问流中的最后一个元素,因为它应该具有最大权重的角色。这对我来说似乎很笨拙,但显然获取流的最后一个元素的方法是 .reduce( ( first , second ) -> second ).orElse( null )。我真希望我们有一个 Stream#last 方法!

最后一个元素是 String object,输入文件中的一行文本。所以我们需要再次拆分字符串。但是这次我们拆分时,我们采用 first 元素而不是第三个元素,因为我们的目标是报告角色的名字。第一个元素由烦人的 zero-based 索引号 0.

标识

瞧,我们得到 Grievous 作为我们的最终结果。

Path path = Paths.get( "/Users/basil_dot_work/inputs.csv" );
if ( Files.notExists( path ) ) { throw new IllegalStateException( "Failed to find file at path: " + path ); }

Stream < String > lines;
try { lines = Files.lines( path , StandardCharsets.UTF_8 ); } catch ( IOException e ) { throw new RuntimeException( e ); }
String result =
        lines
                .skip( 1L )  // Skip the header row, with column names.
                .filter(  // Filter out lines whose targeted value is "unknown". We need text made up only of digits.
                        line -> ! line.split( ";" )[ 2 ].equalsIgnoreCase( "unknown" )
                )
                .sorted(  // Sort by extracting third field’s text, then parse to get an `int` value.
                        Comparator.comparingInt( ( String line ) -> Integer.parseInt( line.split( ";" )[ 2 ] ) )
                )
                .reduce( ( first , second ) -> second ).orElse( null ) // Get last element.
                .split( ";" )[ 0 ]; // Extract name of character from first field of our one and only line of input left remaining after processing.

System.out.println( "result = " + result );

result = Grievous

请务必将我的方法与其他人的方法进行比较 Answer, by Florian Hartung。另一个可能更好;还没仔细研究

没有流

为了进行比较,这里是更传统的代码,很少或根本没有使用流。

我们以与上面相同的方式从文件中读取行。

我们需要跳过第一行,即列标题的 header 行。但是 Files.lines 编辑的 List object return 是不可修改的。所以我们不能简单地删除该列表的第一个元素。所以我们通过调用 lines.subList( 1 , lines.size() ) 有效地跳过了第一行。 subList 命令 return 是一个作为视图映射回原始列表的列表,而不是实际创建一个新的单独列表。这很有效,适合我们在这里使用。

我们将 class 定义为 record 来保存每个人的详细信息。我们使用 Integer 而不是 int 以便我们可以为带有 unknown 文本而不是数字的行保留 null

对于每一行,我们直接将文本项传输到 String 个成员字段。但是对于高度和质量,我们使用三元运算符来 return null 或实例化 Integer object.

我们通过添加到列表来收集我们的 Person object。

为了得到最大的Person object,其中mass最大,我们需要忽略那些null。所以我们在这里使用一个简单的流来制作 Person object 的新列表,质量为 non-null。此流可以替换为常规循环,但会更冗长。

通过过滤列表,我们调用 Collections.max while passing a Comparator object 来比较 mass 成员字段。

我们最终得到一个 Person object。所以我们查询它的 name 成员字段。

瞧,我们得到 Grievous 作为我们的最终结果。

Path path = Paths.get( "/Users/basil_dot_work/inputs.csv" );
if ( Files.notExists( path ) ) { throw new IllegalStateException( "Failed to find file at path: " + path ); }

List < String > lines;
try { lines = Files.lines( path , StandardCharsets.UTF_8 ).toList(); } catch ( IOException e ) { throw new RuntimeException( e ); }
lines = lines.subList( 1 , lines.size() ); // Skip over first line.

record Person( String name , Integer height , Integer mass , String hair_color , String skin_color , String eye_color , String birth_year , String gender ) { }
List < Person > persons = new ArrayList <>();
for ( String line : lines )
{
    String[] parts = line.split( ";" );

    Integer height = ( parts[ 1 ].equalsIgnoreCase( "unknown" ) ) ? null : Integer.valueOf( parts[ 1 ] );
    Integer mass = ( parts[ 2 ].equalsIgnoreCase( "unknown" ) ) ? null : Integer.valueOf( parts[ 2 ] );
    Person person = new Person( parts[ 0 ] , height , mass , parts[ 3 ] , parts[ 4 ] , parts[ 5 ] , parts[ 6 ] , parts[ 7 ] );
    persons.add( person );
}
System.out.println( "persons = " + persons );
List < Person > personsWithMass = persons.stream().filter( person -> Objects.nonNull( person.mass ) ).toList();
Person heaviestPerson = Collections.max( personsWithMass , Comparator.comparing( person -> person.mass ) );

System.out.println( "heaviest Person’s name = " + heaviestPerson.name );

heaviest Person’s name = Grievous