如何解析格式类似于成绩簿的文本文件?

How to parse a text file that is formatted like a gradebook?

提问人:Nico 提问时间:8/26/2020 更新时间:8/27/2020 访问量:184

问:

我正在尝试读取数据格式如下的文本文件:

Name|Test1|Test2|Test3|Test4|Test5|Test6|Test7|Test8|Test9|Test10   
John Smith|82|89|90|78|89|96|75|88|90|96
Jane Doe|90|92|93|90|89|84|97|91|87|91
Joseph Cruz|68|74|78|81|79|86|80|81|82|87

我的目标是能够获得每个学生的平均考试分数,以及每个测试的平均分数(列)和总体平均分数。我无法将第一列(学生的名字)与他们的考试成绩“分开”。有没有办法忽略或跳过第一列?另外,存储这些测试分数的最佳方法是什么,以便我能够进行我提到的那些计算?

我已使用以下方法成功读取了文件的内容:

in.useDelimiter("\\|");
for(int i = 0; in.hasNextLine(); i++){
    System.out.println(in.next());}
Java 解析 java.util.scanner

评论

0赞 Zabuzard 8/26/2020
只需在进入循环之前调用,这样您就可以完全使用第一行。in.nextLine()
0赞 Nico 8/26/2020
是的,我忘了包括这一点,但这正是我所做的

答:

2赞 Zabuzard 8/26/2020 #1

溶液

你可以通过在进入循环之前完全消耗第一行来实现你想要的,只需调用

in.nextLine();

之前和第一行被消耗。


分裂

但是,我会以不同的方式处理这个问题,逐行解析,然后拆分,这样更容易处理每行给出的数据。|

in.nextLine();
while (in.hasNextLine()) {
    String line = in.nextLine();
    String[] data = line.split("\\|");

    String name = data[0];
    int[] testResults = new int[data.length - 1];
    for (int i = 0; i < testResults.length; i++) {
        testResults[i] = Integer.parseInt(data[i + 1]);
    }

    ...
}

适当的 OOP

理想情况下,您可以添加一些 OOP,创建一个包含类似字段的类Student

public class Student {
    private final String name;
    private final int[] testResults;

    // constructor, getter, ...
}

然后给它一个类似这样的方法:parseLine

public static Student parseLine(String line) {
    String[] data = line.split("\\|");

    String name = data[0];
    int[] testResults = new int[data.length - 1];
    for (int i = 0; i < testResults.length; i++) {
        testResults[i] = Integer.parseInt(data[i + 1]);
    }

    return new Student(name, testResults);
}

然后,您的解析将大大简化为:

List<Student> students = new ArrayList<>();
in.nextLine();
while (in.hasNextLine()) {
    students.add(Student.parseLine(in.nextLine());
}

流和蔚来

或者,如果您喜欢流,只需使用 NIO 读取文件:

List<Student> students = Files.lines(Path.of("myFile.txt"))
    .skip(1)
    .map(Student::parseLine)
    .collect(Collectors.toList());

非常清晰、紧凑、可读。


平均分

我的目标是能够获得每个学生的平均考试分数,以及每个测试的平均分数(列)和总体平均分数。

如图所示,使用适当的 OOP 结构,这相当简单。首先,一个学生的平均分,只需在班级中添加一个方法:Student

public double getAverageScore() {
    double total = 0.0;
    for (int testResult : testResults) {
        total += testResult;
    }
    return total / testResults.length;
}

替代流解决方案:

return IntStream.of(testResults).average().orElseThrow();

接下来,每列的平均分数:

public static double averageTestScore(List<Student> students, int testId) {
    double total = 0.0;
    for (Student student : students) {
        total += student.getTestScores()[testId];
    }
    return total / students.size();
}

而流解决方案:

 return students.stream()
       .mapToInt(student -> student.getTestScores[testId])
       .average().orElseThrow();

最后是总体平均分,可以通过取每个学生平均分的平均值来计算:

public static double averageTestScore(List<Student> students) {
    double total = 0.0;
    for (Student student : students) {
        total += student.getAverageScore();
    }
    return total / students.size();
}

和 Stream 变体:

return students.stream()
    .mapToDouble(Student::getAverageScore)
    .average().orElseThrow();

评论

0赞 Nico 8/26/2020
这可能是一个愚蠢的问题,但是有没有办法在不使用 OOP 的情况下计算列总计(Test1 总计、Test2 总计等)?
0赞 Zabuzard 8/26/2020
不知道你到底是什么意思。编程是关于很好地构建您的代码,以便您可以轻松完成任务并创建可读且可维护的代码。在不使用类和方法的情况下将所有内容都塞进一个方法并不能真正实现这一点。
0赞 Nico 8/26/2020
是的,我理解这一点,我真的很感谢你的帮助。但我只是想知道是否有另一种方法,因为我还没有达到你的水平。
0赞 Zabuzard 8/26/2020
我只是不太确定你所说的另一种方式到底是什么意思,即你期待什么样的回应或方法。
1赞 GameDroids 8/26/2020 #2

我的想法是将您读取的数据存储在 .其中每个学生的名字是“键”,分数存储在您作为地图中的值放置的 an 中。MapList<Integer>

这样:

Map<String, List<Integer>> scores = new HashMap<>();

List<Integer> studentScores = new ArrayList<>();
// then you read the scores one by one and add them 
studentScores.add(82);
studentScores.add(89);
....
// when you are finished with the student you add him to the map
scores.put("John Smith", studentScores);

// in the end, when you need the values (for your calculation for example) you can get them like this:

scores.get("John Smith").get(0)   // which will be the 1st value from John's list => 82

现在到实际阅读:我认为你不需要分隔符,只需阅读整行,然后:split

scanner.nextLine();                      // I almost forgot: this reads and forgets the very first line of your file

while(scanner.hasNextLine()){
     String line = scanner.nextLine();   // this is a whole line like "John Smith|82|89|....."
     // now you need to split it
     String[] columns = line.split("|"); // straight forward way to get an array that looks like this: ["John Smith", "82", "89", ...]

    
     String studentName = columns[0];   // first we get the name
     List<Integer> studentScores = new ArrayList<>();
     for(int i=1;i<columns; i++){       // now we get the scores
        studentScores.add(Integer.valueOf(columns[i])); // will read the score at index i, cast it to an Integer and add it to the score list
     }
     // finally you put everything in your map
     scores.put(studentName, studentScores);
}

评论

2赞 GameDroids 8/26/2020
请注意,“地图”只能保留唯一的键,因此如果您有两个同名的学生,第二个学生将覆盖第一个学生的分数......由您决定。更合适的解决方案是 OOP 方式,@Zabuzard建议的每个学生都有一个对象
0赞 GameDroids 8/27/2020
编辑我刚刚意识到您也想知道如何阅读/跳过第一行,所以我添加了该部分。如果您不确定文件的内容,您还可以读取 while 循环中的所有行(包括第一行),并在处理它们之前分析这些行。
0赞 Ela Łabaj 8/26/2020 #3

也许尝试使用:in.nextLine()

//to skip first line with headers
in.nextLine();

while (in.hasNextLine()) {
        String studentLine = in.nextLine();
        int firstColumnEnd = studentLine.indexOf("|");

        String name = studentLine.substring(0, firstColumnEnd - 1);
        String[] tests = studentLine.substring(firstColumnEnd + 1).split("\\|");
}

评论

0赞 Zabuzard 8/26/2020
这不会跳过第一行,这是 OP 的主要问题。
0赞 Ela Łabaj 8/26/2020
@Zabuzard 什么是OP?
1赞 Zabuzard 8/26/2020
原始发帖人,问题的作者,即 .NRMA