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

huangapple go评论89阅读模式
英文:

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

问题

我正试图读取一个文本文件,其中的数据格式如下:

姓名|测试1|测试2|测试3|测试4|测试5|测试6|测试7|测试8|测试9|测试10	
约翰·史密斯|82|89|90|78|89|96|75|88|90|96
简·多e|90|92|93|90|89|84|97|91|87|91
约瑟夫·克鲁兹|68|74|78|81|79|86|80|81|82|87

我的目标是能够获得每位学生的平均测试成绩,以及每个测试(列)的平均分和总体平均分。我在尝试“分离”第一列(学生的姓名)和他们的测试分数方面遇到了困难。有没有办法忽略或跳过第一列?此外,存储这些测试分数的最佳方法是什么,以便我能够进行我提到的那些计算?

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

in.useDelimiter("\\|");
for(int i = 0; in.hasNextLine(); i++){
    System.out.println(in.next());
英文:

I'm trying to read a text file that has data formatted as follows:

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

My goal is to be able to get each student's average test score, as well as the average score per test (column) and the average score overall. I am having trouble "separating" the first columns (the names of the students) from their test scores. Is there a way to ignore or skip the first column? Also, what is the best way to store those test scores so that I will be able to do those calculations I mentioned?

I have successfully read the contents of the file by using the below method:

in.useDelimiter("\\|");
for(int i = 0; in.hasNextLine(); i++){
    System.out.println(in.next());}

答案1

得分: 2

解决方案

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

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;

    // 构造方法,获取器,...
}

然后给它一个 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());

非常清晰、紧凑且易读。


平均分数

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

通过适当的面向对象结构,如上所示,这是相当简单的。首先,计算学生的平均成绩,只需在 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();
英文:

Solution

You can achieve what you want by fully consuming the first line before you enter your loop, just call

in.nextLine();

before and the first line is consumed.


Splitting

However, I would approach this differently, parsing line by line and then splitting on |, that way it is easier to work with the data given per line.

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

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

    ...
}

Proper OOP

Ideally you would add some OOP to that, create a class Student with fields like

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

    // constructor, getter, ...
}

and then give it a parseLine method like:

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

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

    return new Student(name, testResults);
}

Then your parsing simplifies heavily to just:

List&lt;Student&gt; students = new ArrayList&lt;&gt;();
in.nextLine();
while (in.hasNextLine()) {
    students.add(Student.parseLine(in.nextLine());
}

Streams and NIO

Or if you like streams, just read the file using NIO:

List&lt;Student&gt; students = Files.lines(Path.of(&quot;myFile.txt&quot;))
    .skip(1)
    .map(Student::parseLine)
    .collect(Collectors.toList());

very clear, compact and readable.


Average score

> My goal is to be able to get each student's average test score, as well as the average score per test (column) and the average score overall.

With the proper OOP structure, as shown, this is fairly simple. First, a students average score, just add a method to the Student class:

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

Alternative stream solution:

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

Next, the average score per column:

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

And the stream solution:

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

And finally the average score overall, which can be computed by taking the average of each students average score:

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

and the stream variant:

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

答案2

得分: 1

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

就像这样:

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

List<Integer> studentScores = new ArrayList<>();
// 然后逐个读取分数并添加
studentScores.add(82);
studentScores.add(89);
// ....
// 当您完成一个学生的读取后,将其添加到映射中
scores.put("John Smith", studentScores);

// 最后,当您需要这些值(例如进行计算)时,可以这样获取:

scores.get("John Smith").get(0)   // 这将是John的列表中的第一个值 => 82

现在针对实际的读取操作:我认为您不需要分隔符,只需读取整行,然后在之后使用split函数进行拆分:

scanner.nextLine();                      // 我差点忘记了:这会读取并忽略文件的第一行

while(scanner.hasNextLine()){
     String line = scanner.nextLine();   // 这是一整行,例如"John Smith|82|89|....."
     // 现在您需要拆分它
     String[] columns = line.split("\\|"); // 直接的方法,得到一个类似这样的数组:["John Smith", "82", "89", ...]

    
     String studentName = columns[0];   // 首先获取姓名
     List<Integer> studentScores = new ArrayList<>();
     for(int i=1; i<columns.length; i++){       // 现在获取分数
        studentScores.add(Integer.valueOf(columns[i])); // 读取索引为i处的分数,将其转换为整数,并添加到分数列表中
     }
     // 最后将所有内容放入映射中
     scores.put(studentName, studentScores);
}
英文:

My idea would be to store the data you read in a Map. Where each student's name is the "key" and the scores are stored in an List&lt;Integer&gt; which you put as the value in you map.

Like so:

Map&lt;String, List&lt;Integer&gt;&gt; scores = new HashMap&lt;&gt;();

List&lt;Integer&gt; studentScores = new ArrayList&lt;&gt;();
// 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(&quot;John Smith&quot;, studentScores);

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

scores.get(&quot;John Smith&quot;).get(0)   // which will be the 1st value from John&#39;s list =&gt; 82

Now to the actual reading: I don't think you need a delimiter, just read the whole line and split it afterwards:

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 &quot;John Smith|82|89|.....&quot;
     // now you need to split it
     String[] columns = line.split(&quot;|&quot;); // straight forward way to get an array that looks like this: [&quot;John Smith&quot;, &quot;82&quot;, &quot;89&quot;, ...]

    
     String studentName = columns[0];   // first we get the name
     List&lt;Integer&gt; studentScores = new ArrayList&lt;&gt;();
     for(int i=1;i&lt;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);
}

答案3

得分: 0

也许尝试使用 in.nextLine()

// 跳过包含标题的第一行
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("\\|");
}
英文:

Maybe try using in.nextLine():

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

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

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

huangapple
  • 本文由 发表于 2020年8月26日 16:19:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/63593456.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定