【问题标题】:How to parse a text file that is formatted like a gradebook?如何解析格式类似于成绩簿的文本文件?
【发布时间】:2020-08-26 08:19:03
【问题描述】:

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

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

【问题讨论】:

  • 在进入循环之前调用in.nextLine(),这样你就可以完全消耗第一行。
  • 是的,我忘了包括那一点,但这正是我所做的

标签: java parsing java.util.scanner


【解决方案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

或者,如果您喜欢流,只需使用 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();
}

和流变体:

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

【讨论】:

  • 这可能是个愚蠢的问题,但有没有办法在不使用 OOP 的情况下计算列总数(Test1 total、Test2 total 等)?
  • 不确定你的意思。编程就是很好地构建代码,以便您可以轻松地完成任务并创建可读和可维护的代码。在不使用类和方法的情况下将所有内容都填充到一个方法中并不能真正实现这一点。
  • 是的,我明白这一点,非常感谢您的帮助。但我只是想知道是否还有其他方法,因为我还没有达到你的水平。
  • 我只是不太确定你所说的另一种方式究竟是什么意思,即你期望什么样的回应或方法。
【解决方案2】:

我的想法是将您读取的数据存储在Map 中。其中每个学生的姓名是“键”,分数存储在 List&lt;Integer&gt; 中,您将其作为值放入映射中。

像这样:

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

【讨论】:

  • 请注意,“地图”只能保留唯一键,因此如果您有两个同名的学生,第二个学生将覆盖第一个学生的分数...决定。正如@Zabuzard 建议的那样,更合适的解决方案是为每个学生提供一个对象的 OOP 方式
  • Edit 我刚刚意识到您也想知道如何阅读/跳过第一行,所以我添加了那部分。如果您不确定文件的内容,您还可以在 while 循环中读取所有行(包括第一行)并在处理它们之前分析这些行。
【解决方案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("\\|");
}

【讨论】:

  • 这不会跳过第一行,这是 OP 的主要问题。
  • @Zabuzard OP 是什么?
  • 原帖,问题的作者,即NRMA
猜你喜欢
  • 2021-04-04
  • 1970-01-01
  • 1970-01-01
  • 2012-02-12
  • 1970-01-01
  • 2018-07-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多