Java Scanner无法完全读取.txt文件中的每一行

3

这个程序试图将文本文件分割成单词,并计算每个单词被使用的次数。扫描器似乎只读取了每行的部分内容,我不知道为什么。这是我第一次使用这种扫描方法。

import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Scanner;


public class WordStats {

    public static void main(String args[]){
        ArrayList<String> words = new ArrayList<>(1);
        ArrayList<Integer> num = new ArrayList<>(1);
        Scanner sc2 = null;
        try {
            sc2 = new Scanner(new File("source.txt"));
        } catch (FileNotFoundException e) {
            e.printStackTrace();  
        }
        while (sc2.hasNextLine()) {
            Scanner s2 = new Scanner(sc2.nextLine());
            boolean set=false;
            while (s2.hasNext()) {
                num.add(1);
                String s = s2.next().replaceAll("[^A-Za-z ]", " ").toLowerCase().trim();
                for(int i=0;i<words.size(); i++){
                    if(s.equals(words.get(i))){
                        num.set(i,num.get(i)+1);
                        set=true;
                    }
                }
                if(!set){
                words.add(s);
                num.add(1);
                }
            }
        }
        for(int i=0;i<words.size();i++){
            System.out.println(words.get(i)+" "+num.get(i));
        }
    }
}

这个文本文件是《葛底斯堡演说》:

亚伯拉罕·林肯,“葛底斯堡演说”(1863年11月19日)

八十七年前,我们的祖先在这个大陆上创立了一个新国家,在自由中孕育,在保证所有人平等的前提下奉献。

现在我们正在进行一场伟大的内战,考验着那个国家,或者任何一个以这样的方式构建和奉献的国家是否能够长久存在。我们在这场战争的伟大战场上相遇了。我们来到这里,为了纪念那些为了使这个国家生存而献出生命的人们,在这片领土上献上他们的最后安息之地。我们应该这样做,这是完全适当和正确的。

但是,从更广泛的意义上讲,我们不能奉献-我们要奉圣-我们不能将这片土地变得神圣。在这里挣扎着的勇敢的人们,无论是活着的还是死去的,都使它成为了神圣的,远远超过了我们微薄的能力来增加或者减少。世界会很少注意,也不会长久记住我们在这里说了什么,但它永远不会忘记他们在这里做了什么。对于我们活着的人来说,更重要的是,在这里致力于还未完成的工作,即为那些在这里战斗所取得的辉煌进展增加奉献。对于我们而言,更应该在这里致力于剩余的伟大任务-从这些受到崇敬的死者身上,使我们更加献身于他们献出最后一份献身精神的事业-我们在这里誓言这些死者不会白白牺牲,这个国家,在上帝的帮助下,必将获得新的自由诞生-而那个以人民为本、由人民组成、为人民服务的政府不会从地球上消失。

原始的行分隔符被保留。 我的输出似乎只计算每行的一部分,并且将空格视为两个单词。 输出:

abraham 1
lincoln 1
gettysburg 1
address 1
 2
november 1
fourscore 1
and 5
seven 1
years 1
ago 1
our 2
fathers 1
brought 1
forth 1
on 2
this 3
continent 1
a 7
new 2
nation 5
conceived 2
in 4
liberty 1
now 1
we 8
are 2
engaged 1
but 2

它可能不是扫描方法,但我更熟悉代码的那一部分,我认为那不是问题所在。


5
考虑使用一个HashMap<String, Integer>来维护单词出现次数而不是两个ArrayList?这可能并不是你的错误,但肯定会让问题更简洁。 - Mshnik
我本来会用哈希表的,但这是一项课堂作业,我们还没有正式学习过映射。我相信我可以弄明白,但我更喜欢使用数组列表。 - dilucidis
3个回答

1
逻辑有些偏差。您有平行的列表,应该具有相同数量的元素,但并未同时添加。
    Map<String, Integer> wordFrequencies = new TreeMap<>();

    while (sc2.hasNextLine()) {
        Scanner s2 = new Scanner(sc2.nextLine());
        while (s2.hasNext()) {
            String word = s2.next().replaceAll("[^A-Za-z ]", " ")
                .toLowerCase().trim();
            Integer n = wordFrequencies.get(word);
            wordFrequencies.put(word, n == null ? 1 : 1 + n);
        }
    }
    for (Map.Entry<String, Integer> entry : wordFrequencies.entrySet()) {
        System.out.printf("%-40s %5d%n", entry.getKey(), entry.getValue());
    }

1
你需要在这个 while 循环的开头重置你的布尔集合。
 while (s2.hasNext()) {
 set = false;

一旦在每行中遇到第一个重复的单词,集合将始终为真,并且不会向列表中添加新单词。
空格计数是由于您的replaceAll如何处理“(19”和“1863)”,因为这些“单词”中没有字母字符。

我还没有检查字数,但现在程序已经运行到了结尾。谢谢。 - dilucidis

1
问题在于你的代码在每次循环迭代时无条件向 num 列表中添加 1。这会导致 num 相对于 words 发生偏移,产生错误的输出。
从嵌套的 while 循环中删除 num.add(1); 可以解决问题。然而,更好的方法是创建一个 Map<String,Integer> 来跟踪计数。除了确保计数和单词始终处于同步状态之外,这种更改还可以让您完全删除嵌套的 while 循环,并使用基于映射算法的快速查找。

单词计数似乎是正确的选择,但使用地图可能更好。不过这种方法也没有问题。 - Justin
@Justin 不,它们对于非空文件没有正确的机会。检查 numwords 的大小,num 将始终具有更多的项。 - Sergey Kalinichenko
请注意,列表中的所有计数都是正确的。还要注意,只有在将元素添加到num时,才会将元素添加到words中。这确保了words和num列表具有完全相同的大小。 - Justin
@Justin 不正确的程序有时会产生看起来正确的结果。对 num 进行无条件加 1 是不正确的行为,因为它破坏了 numwords 列表之间作为“并行数组”(在高级编程语言中很少使用的好东西)的关系。 - Sergey Kalinichenko
@Justin “还要注意的是,只有在将元素添加到num时,才会将元素添加到words中。” 这是明显错误的:num.add(1)被调用了两次而不是一次。 - Sergey Kalinichenko
哦,我现在明白了,你是正确的。但是虽然代码写得很糟糕,但它并不会影响输出结果。你可以在开始 while 循环之前将 nums 加 1 百万次,数字计数仍然是正确的,但显然效率很低。纠正输出中缺失单词的方法是因为他没有将布尔值重置为 false。 - Justin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接