10.6 StreamTokenizer.md 9.9 KB
Newer Older
W
10.6~8  
wizardforcel 已提交
1
# 10.6 `StreamTokenizer`
Q
quanke 已提交
2

W
ch10  
wizardforcel 已提交
3
尽管`StreamTokenizer`并不是从`InputStream``OutputStream`派生的,但它只随同`InputStream`工作,所以十分恰当地包括在库的IO部分中。
Q
quanke 已提交
4

W
10.6~8  
wizardforcel 已提交
5
`StreamTokenizer`类用于将任何`InputStream`分割为一系列“记号”(`Token`)。这些记号实际是一些断续的文本块,中间用我们选择的任何东西分隔。例如,我们的记号可以是单词,中间用空白(空格)以及标点符号分隔。
Q
quanke 已提交
6
下面是一个简单的程序,用于计算各个单词在文本文件中重复出现的次数:
Q
quanke 已提交
7 8

```
Q
quanke 已提交
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
//: SortedWordCount.java
// Counts words in a file, outputs
// results in sorted form.
import java.io.*;
import java.util.*;
import c08.*; // Contains StrSortVector

class Counter {
  private int i = 1;
  int read() { return i; }
  void increment() { i++; }
}

public class SortedWordCount {
  private FileInputStream file;
  private StreamTokenizer st;
  private Hashtable counts = new Hashtable();
  SortedWordCount(String filename)
    throws FileNotFoundException {
    try {
      file = new FileInputStream(filename);
      st = new StreamTokenizer(file);
      st.ordinaryChar('.');
      st.ordinaryChar('-');
    } catch(FileNotFoundException e) {
      System.out.println(
        "Could not open " + filename);
      throw e;
    }
  }
  void cleanup() {
    try {
      file.close();
    } catch(IOException e) {
      System.out.println(
        "file.close() unsuccessful");
    }
  }
  void countWords() {
    try {
      while(st.nextToken() !=
        StreamTokenizer.TT_EOF) {
        String s;
        switch(st.ttype) {
          case StreamTokenizer.TT_EOL:
            s = new String("EOL");
            break;
          case StreamTokenizer.TT_NUMBER:
            s = Double.toString(st.nval);
            break;
          case StreamTokenizer.TT_WORD:
            s = st.sval; // Already a String
            break;
          default: // single character in ttype
            s = String.valueOf((char)st.ttype);
        }
        if(counts.containsKey(s))
          ((Counter)counts.get(s)).increment();
        else
          counts.put(s, new Counter());
      }
    } catch(IOException e) {
      System.out.println(
        "st.nextToken() unsuccessful");
    }
  }
  Enumeration values() {
    return counts.elements();
  }
  Enumeration keys() { return counts.keys(); }
  Counter getCounter(String s) {
    return (Counter)counts.get(s);
  }
  Enumeration sortedKeys() {
    Enumeration e = counts.keys();
    StrSortVector sv = new StrSortVector();
    while(e.hasMoreElements())
      sv.addElement((String)e.nextElement());
    // This call forces a sort:
    return sv.elements();
  }
  public static void main(String[] args) {
    try {
      SortedWordCount wc =
        new SortedWordCount(args[0]);
      wc.countWords();
      Enumeration keys = wc.sortedKeys();
      while(keys.hasMoreElements()) {
        String key = (String)keys.nextElement();
        System.out.println(key + ": "
                 + wc.getCounter(key).read());
      }
      wc.cleanup();
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~
Q
quanke 已提交
107
```
Q
quanke 已提交
108

W
10.6~8  
wizardforcel 已提交
109
最好将结果按排序格式输出,但由于Java 1.0和Java 1.1都没有提供任何排序方法,所以必须由自己动手。这个目标可用一个`StrSortVector`方便地达成(创建于第8章,属于那一章创建的软件包的一部分。记住本书所有子目录的起始目录都必须位于类路径中,否则程序将不能正确地编译)。
Q
quanke 已提交
110

W
10.6~8  
wizardforcel 已提交
111
为打开文件,使用了一个`FileInputStream`。而且为了将文件转换成单词,从`FileInputStream`中创建了一个`StreamTokenizer`。在`StreamTokenizer`中,存在一个默认的分隔符列表,我们可用一系列方法加入更多的分隔符。在这里,我们用`ordinaryChar()`指出“该字符没有特别重要的意义”,所以解析器不会把它当作自己创建的任何单词的一部分。例如,`st.ordinaryChar('.')`表示小数点不会成为解析出来的单词的一部分。在与Java配套提供的联机文档中,可以找到更多的相关信息。
Q
quanke 已提交
112

W
10.6~8  
wizardforcel 已提交
113
`countWords()`中,每次从数据流中取出一个记号,而`ttype`信息的作用是判断对每个记号采取什么操作——因为记号可能代表一个行尾、一个数字、一个字符串或者一个字符。
Q
quanke 已提交
114

W
wizardforcel 已提交
115
找到一个记号后,会查询`Hashtable counts`,核实其中是否已经以“键”(`Key`)的形式包含了一个记号。若答案是肯定的,对应的`Counter`(计数器)对象就会自增,指出已找到该单词的另一个实例。若答案为否,则新建一个`Counter`——因为`Counter`构造器会将它的值初始化为1,正是我们计算单词数量时的要求。
Q
quanke 已提交
116

W
10.6~8  
wizardforcel 已提交
117
`SortedWordCount`并不属于`Hashtable`(散列表)的一种类型,所以它不会继承。它执行的一种特定类型的操作,所以尽管`keys()``values()`方法都必须重新揭示出来,但仍不表示应使用那个继承,因为大量`Hashtable`方法在这里都是不适当的。除此以外,对于另一些方法来说(比如`getCounter()`——用于获得一个特定字符串的计数器;又如`sortedKeys()`——用于产生一个枚举),它们最终都改变了`SortedWordCount`接口的形式。
Q
quanke 已提交
118

W
10.6~8  
wizardforcel 已提交
119 120
`main()`内,我们用`SortedWordCount`打开和计算文件中的单词数量——总共只用了两行代码。随后,我们为一个排好序的键(单词)列表提取出一个枚举。并用它获得每个键以及相关的`Count`(计数)。注意必须调用`cleanup()`,否则文件不能正常关闭。
采用了`StreamTokenizer`的第二个例子将在第17章提供。
Q
quanke 已提交
121

W
10.6~8  
wizardforcel 已提交
122
10.6.1 `StringTokenizer`
Q
quanke 已提交
123

W
ch10  
wizardforcel 已提交
124
尽管并不必要IO库的一部分,但`StringTokenizer`提供了与`StreamTokenizer`极相似的功能,所以在这里一并讲述。
Q
quanke 已提交
125

W
10.6~8  
wizardforcel 已提交
126
`StringTokenizer`的作用是每次返回字符串内的一个记号。这些记号是一些由制表站、空格以及新行分隔的连续字符。因此,字符串`"Where is my cat?"`的记号分别是`"Where"``"is"``"my"``"cat?"`。与`StreamTokenizer`类似,我们可以指示`StringTokenizer`按照我们的愿望分割输入。但对于`StringTokenizer`,却需要向构造器传递另一个参数,即我们想使用的分隔字符串。通常,如果想进行更复杂的操作,应使用`StreamTokenizer`
Q
quanke 已提交
127

W
10.6~8  
wizardforcel 已提交
128
可用`nextToken()``StringTokenizer`对象请求字符串内的下一个记号。该方法要么返回一个记号,要么返回一个空字符串(表示没有记号剩下)。
Q
quanke 已提交
129

Q
quanke 已提交
130
作为一个例子,下述程序将执行一个有限的句法分析,查询键短语序列,了解句子暗示的是快乐亦或悲伤的含义。
Q
quanke 已提交
131 132

```
Q
quanke 已提交
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
//: AnalyzeSentence.java
// Look for particular sequences
// within sentences.
import java.util.*;

public class AnalyzeSentence {
  public static void main(String[] args) {
    analyze("I am happy about this");
    analyze("I am not happy about this");
    analyze("I am not! I am happy");
    analyze("I am sad about this");
    analyze("I am not sad about this");
    analyze("I am not! I am sad");
    analyze("Are you happy about this?");
    analyze("Are you sad about this?");
    analyze("It's you! I am happy");
    analyze("It's you! I am sad");
  }
  static StringTokenizer st;
  static void analyze(String s) {
    prt("\nnew sentence >> " + s);
    boolean sad = false;
    st = new StringTokenizer(s);
    while (st.hasMoreTokens()) {
      String token = next();
      // Look until you find one of the
      // two starting tokens:
      if(!token.equals("I") &&
         !token.equals("Are"))
        continue; // Top of while loop
      if(token.equals("I")) {
        String tk2 = next();
        if(!tk2.equals("am")) // Must be after I
          break; // Out of while loop
        else {
          String tk3 = next();
          if(tk3.equals("sad")) {
            sad = true;
            break; // Out of while loop
          }
          if (tk3.equals("not")) {
            String tk4 = next();
            if(tk4.equals("sad"))
              break; // Leave sad false
            if(tk4.equals("happy")) {
              sad = true;
              break;
            }
          }
        }
      }
      if(token.equals("Are")) {
        String tk2 = next();
        if(!tk2.equals("you"))
          break; // Must be after Are
        String tk3 = next();
        if(tk3.equals("sad"))
          sad = true;
        break; // Out of while loop
      }
    }
    if(sad) prt("Sad detected");
  }
  static String next() {
    if(st.hasMoreTokens()) {
      String s = st.nextToken();
      prt(s);
      return s;
W
10.6~8  
wizardforcel 已提交
201
    }
Q
quanke 已提交
202 203 204 205 206 207 208
    else
      return "";
  }
  static void prt(String s) {
    System.out.println(s);
  }
} ///:~
Q
quanke 已提交
209
```
Q
quanke 已提交
210

W
10.6~8  
wizardforcel 已提交
211
对于准备分析的每个字符串,我们进入一个`while`循环,并将记号从那个字符串中取出。请注意第一个if语句,假如记号既不是`"I"`,也不是`"Are"`,就会执行`continue`(返回循环起点,再一次开始)。这意味着除非发现一个`"I"`或者`"Are"`,才会真正得到记号。大家可能想用`==`代替`equals()`方法,但那样做会出现不正常的表现,因为`==`比较的是引用值,而`equals()`比较的是内容。
Q
quanke 已提交
212

W
10.6~8  
wizardforcel 已提交
213
`analyze()`方法剩余部分的逻辑是搜索`"I am sad"`(我很忧伤、`"I am nothappy"`(我不快乐)或者`"Are you sad?"`(你悲伤吗?)这样的句法格式。若没有`break`语句,这方面的代码甚至可能更加散乱。大家应注意对一个典型的解析器来说,通常都有这些记号的一个表格,并能在读取新记号的时候用一小段代码在表格内移动。
Q
quanke 已提交
214

W
10.6~8  
wizardforcel 已提交
215
无论如何,只应将`StringTokenizer`看作`StreamTokenizer`一种简单而且特殊的简化形式。然而,如果有一个字符串需要进行记号处理,而且`StringTokenizer`的功能实在有限,那么应该做的全部事情就是用`StringBufferInputStream`将其转换到一个数据流里,再用它创建一个功能更强大的`StreamTokenizer`