java.util.Scanner的用法

来源:互联网 发布:电脑麦克风软件 编辑:程序博客网 时间:2024/05/01 00:31
java.util.Scanner的用法

Java5添加了java.util.Scanner类,这是一个用于扫描输入文本的新的实用程序。它是以前的StringTokenizer和Matcher类之间的某种结合。在上一节中,使用Matcher在一个String内搜索来查找匹配某个给定模式的数据,这是很有用的,但是局限在仅匹配单个模式。由于任何数据都必须通过同一模式的捕获组检索或通过使用一个索引来检索文本的各个部分。于是可以结合使用正则表达式和从输入流中检索特定类型数据项的方法。这样,除了能使用正则表达式之外,Scanner类还可以任意地对字符串和基本类型(如int和double)的数据进行分析。借助于Scanner,可以针对任何要处理的文本内容编写自定义的语法分析器。

下面,使用Scanner类来读取一个输入源并有计划地从文本中选取数据项。例如,从美国人口普查局使用的文件格式中读取数据。该数据汇总了1990年人口普查中前90%的名和姓(名和姓是分别统计的,因此无法标识个人)的统计分布。人口普查局向公众提供了此数据清单以便系谱专家和统计学家使用。数据包含三个分离的文件:姓氏(dist.all.last)、女性名(dist.female.first)和男性名(dist.male.first)。每个文件包含数行文本,其中用空白符分隔以下的数据:

● 名字

● 用百分比表示的频率

● 用百分比表示的累积频率

● 次序

下面给出姓氏文件中的前两行文本:

SMITH 1.006 1.006 1

JOHNSON 0.810 1.816 2

这表明Smith占人口姓氏的1.006%,Johnson占人口姓氏的0.81%。这是一种简单的文件结构,可以使用Matcher和如下带有捕获组的正则表达式来读取此数据的每一行:

(\S+)\s+(\S+)\s+(\S+)\s+(\S+)

或者可以使用String的split方法:

// for each line of text, assume it's in a variable calledline

String[] dataArray = line.split("\\s+");

String name = dataArray[0];

String frequency = dataArray[1];

String cumulativeFrequency = dataArray[2];

String rank = dataArray[3];

如果有必要,还可以将每个数据项的String转换成float和int类型。如果每一行均有相同的结构,则可以使用regex处理每一行,并且可以方便地使用Matcher或Stringsplit方法来读取数据(若想了解更多的细节,请参考本章后面的有关章节)。但是将String的split方法用于这个姓氏示例有一个缺点:当处理每一行时需创建一个没必要的String数组。在整个输入文本上使用Matcher将更加高效,但是这样做首先需要将整个数据流缓存到一个String中。使用Scanner类可以同时完成多个操作:从一个有效的输入流中读取数据,高效地分析每一行文本,使用多个正则表达式进行扫描,将检索出来的数据元素直接放入所需的基本类型的变量。以下的代码使用Scanner类读取surname数据文件(由于name文件也使用相同的结构,可以按照相同的方式读取这些文件)。

import java.io.FileReader;

import java.util.Scanner;

public class SurnameReader {

public ArrayList getNames() throws IOException {

ArrayList surnames = new ArrayList();

FileReader fileReader =newFileReader("/census/dist.all.last");

// create a scanner from the data file

Scanner scanner = new Scanner(fileReader);

// repeat while there is a next item to be scanned

while (scanner.hasNext()) {

// retrieve each data element

String name = scanner.next();

float frequency = scanner.nextFloat();

float cumulativeFrequency = scanner.nextFloat();

int rank = scanner.nextInt();

surnames.add(name);

}

scanner.close(); // also closes the FileReader

return surnames;

}

public SurnameReader() {

for (String s : getNames()) {

System.out.println(s);

}

}

}

Scanner使用空白符作为默认的分隔符,用户可以很容易地更改分隔符的默认设置。默认的分隔符可以方便地为我们服务。上面这个while循环中的hasNext方法用于检查输入串是否有要处理的下一个标记(token)。除了空白符以外,每一行只有四个项。由于依次处理每一项,因此可以假定当next方法检索每个名字时它会移至下一行读取输入。在这里务必要小心,因为如果文件不匹配代码所期望的内容(例如数据丢失或数据类型错误),那么Scanner将抛出一个异常。

在上面的示例中,每次循环会使得一个新名字添加到ArrayList中。这些姓氏数据和人名信息对于使用实际数据构建测试数据库是非常有用的(可以在本书的网站上找到一个获得此数据的链接)。一旦在ArrayList中拥有数据,可以从列表中随机地选择名字。若想了解有关如何从列表中随机选择数据的信息,请参考“产生随机文本”一节。 

在上一节中,我们使用了Java5中的Scanner类读取一个数据文件。该文件非常简单,因为每一行都采用相同的结构。如果希望读取一个每行的文本结构不相同的数据文件,那么该怎么呢?Matcher无法胜任此任务,因为它只能使用单个regex。而Scanner类则可以,因为它可以在输入文本上使用正则表达式来预测即将出现在文本中的模式。由于可以逐个地读取标记来读取输入,所以可以用它为任意类型的文本编写自定义分析器。下面以大楼安全事件日志虚构一个文件格式为例来说明。日志文件的每一行采用如下的结构:

eventType year month day time type-dependent-data

每行最后一部分的结构取决于事件类型。对于这样的结构,将需要根据事件类型按照逻辑来读取正确的标记。下面创建一个简单的文件,事件类型包括:大楼入口(entry)、大楼出口(exit)和警报(alarm)。下面是一个样本文件:

entry 2005 04 02 1043 meeting Smith, John

exit 2005 04 02 1204 Smith, John

entry 2005 04 02 1300 work Eubanks, Brian

exit 2005 04 02 2120 Eubanks, Brian

alarm 2005 06 02 2301 fire This was a drill

每种事件类型要求读取不同的结构。在此文件的第一行,JohnSmith在上午10:43进入大楼去参加一个会议。他于下午12:04离开大楼。此后BrianEubanks在下午1:00进入大楼从事某项工作并于晚上9:20离开大楼。随后在晚上11:01出现了火警,并且带有注释来说明“这是一次演习”。我可以使用Scanner来读取此文件,如下面的代码所示:

Scanner scanner = new Scanner(newFileReader("logfile.txt"));

while (scanner.hasNext()) {

String type = scanner.next();

int year = scanner.nextInt();

int month = scanner.nextInt();

int day = scanner.nextInt();

int time = scanner.nextInt();

if (type.equals("entry")) {

String purpose = scanner.next(); // purpose of visit

// get the rest of the line and move to start of nextline

String restOfLine = scanner.nextLine();

else 
if (type.equals("exit")) {

String exitName = scanner.nextLine(); // rest of theline

else
if (type.equals("alarm")) {

String alarmType = scanner.next();

String comment = scanner.nextLine(); // rest of the line

}

}

scanner.close();

使用Scanner类时可采用的另一个技巧是使用findInLine方法。它可用来向前查找当前行中的模式。另一个可用的类似方法是findWithinHorizon,它可用来在当前行以外的数据流中查找模式。这种语法分析要求我们了解文件的文法或语法结构以便进行处理。在刚才编写的代码中,实际上隐含了有关内置于系统中的语法内容——即针对这种“日志”语言的分析器。对于更复杂的语法,如处理脚本语言的语法,若从头编写自己的分析器很可能会引起逻辑错误。因此,对于较大和较复杂的语法,采用某种语法描述语言来说明语法本身要好得多,如JavaCC中提供的语言(参考第3章中题为“使用JavaCC创建分析器”一节)。词法分析的生成器使用语法元语言来产生能够处理语法的分析器类。

参考资料:

语法分析和编译器原理的详细内容不在本书的范围之内,要了解这些内容,请参考Addison-Wesley出版社1986年出版的由AlfredV. Aho, Ravi Sethi和Jeffrey D. Ullman编写的有关编译器的经典书籍Compilers:Principles, Techniques, and Tools。