📄 令牌化输入流应用.txt
字号:
摘要:本文从实践的角度重点阐述Java语言中输入流类StreamTokenizer在编写HTML文件分析程序中的应用,并介绍了以字节为单位下载Web页面的例程。
一、概述
Web服务器的核心是对HTML文件中的各标记(Tag)作出正确的分析,一种编程语言的解释程序也是对源文件中的保留字进行分析再做解释的。实际应用中,我们也常常会遇到需要对某一特定类型文件进行关键字分析的情况,比如,需要将某个HTML文件下载并同时下载与之相关的.gif、.class等文件,此时就要求对HTML文件中的标记进行分离,找出所需的文件名及目录。在Java出现以前,类似工作需要对文件中的每个字符进行分析,从中找出所需部分,不仅编程量大,且易出错。笔者在近期的项目中利用Java的输入流类StreamTokenizer进行HTML文件的分析,效果较好。在此,我们要实现从已知的Web页面下载HTML文件,对其进行分析后,下载该页面中包含的HTML文件(如果在Frame中)、图像文件和Class(Java Applet)文件。
二、StreamTokenizer类
StreamTokenizer即令牌化输入流的作用是将一个输入流中变成令牌流。令牌流中的令牌实体有三类:单词(即多字符令牌)、单字符令牌和空白(包括Java和C/C++中的说明语句)。
StreamTokenizer类的构造器为: StreamTokenizer(InputStream in)
该类有一些公有实例变量:ttype、sval和nval ,分别表示令牌类型、当前字符串值和当前数字值。当我们需要取得令牌(即HTML中的标记)之间的字符时,应访问变量sval。而读向下一个令牌的方法是调用nextToken()。方法nextToken()的返回值是int型,共有四种可能的返回:
StreamTokenizer.TT_NUMBER: 表示读到的令牌是数字,数字的值是double型,可以从实例变量nval中读取。
StreamTokenizer.TT_WORD: 表示读到的令牌是非数字的单词(其他字符也在其中),单词可以从实例变量sval中读取。
StreamTokenizer.TT_EOL: 表示读到的令牌是行结束符。
如果已读到流的尽头,则nextToken()返回TT_EOF。
开始调用nextToken()之前,要设置输入流的语法表,以便使分析器辨识不同的字符。WhitespaceChars(int low, int hi)方法定义没有意义的字符的范围。WordChars(int low, int hi)方法定义构造单词的字符范围。
三、程序实现
1、HtmlTokenizer类的实现
对某个令牌流进行分析之前,首先应对该令牌流的语法表进行设置,在本例中,即是让程序分出哪个单词是HTML的标记。下面给出针对我们需要的HTML标记的令牌流类定义,它是StreamTokenizer的子类:
import java.io.*;
import java.lang.String;
class HtmlTokenizer extends
StreamTokenizer {
//定义各标记,这里的标记仅是本例中必须的,
可根据需要自行扩充
static int HTML_TEXT=-1;
static int HTML_UNKNOWN=-2;
static int HTML_EOF=-3;
static int HTML_IMAGE=-4;
static int HTML_FRAME=-5;
static int HTML_BACKGROUND=-6;
static int HTML_APPLET=-7;
boolean outsideTag=true; //判断是否在标记之中
//构造器,定义该令牌流的语法表。
public HtmlTokenizer(BufferedReader r) {
super(r);
this.resetSyntax(); //重置语法表
this.wordChars(0,255); //令牌范围为全部字符
this.ordinaryChar('< '); //HTML标记两边的分割符
this.ordinaryChar('>');
} //end of constructor
public int nextHtml(){
int token; //令牌
try{
switch(token=this.nextToken()){
case StreamTokenizer.TT_EOF:
//如果已读到流的尽头,则返回TT_EOF
return HTML_EOF;
case '< ': //进入标记字段
outsideTag=false;
return nextHtml();
case '>': //出标记字段
outsideTag=true;
return nextHtml();
case StreamTokenizer.TT_WORD:
//若当前令牌为单词,判断是哪个标记
if (allWhite(sval))
return nextHtml(); //过滤其中空格
else if(sval.toUpperCase().indexOf("FRAME")
!=-1 && !outsideTag) //标记FRAME
return HTML_FRAME;
else if(sval.toUpperCase().indexOf("IMG")
!=-1 && !outsideTag) //标记IMG
return HTML_IMAGE;
else if(sval.toUpperCase().indexOf("BACKGROUND")
!=-1 && !outsideTag) //标记BACKGROUND
return HTML_BACKGROUND;
else if(sval.toUpperCase().indexOf("APPLET")
!=-1 && !outsideTag) //标记APPLET
return HTML_APPLET;
default:
System.out.println ("Unknown tag: "+token);
return HTML_UNKNOWN;
} //end of case
}catch(IOException e){
System.out.println("Error:"+e.getMessage());}
return HTML_UNKNOWN;
} //end of nextHtml
protected boolean allWhite(String s){//过滤所有空格
//实现略
}// end of allWhite
} //end of class
以上方法由笔者在近期项目中测试通过,操作系统为Windows NT4,编程工具使用Inprise Jbuilder3。
浏览统计 Total:42 | Year:42 | Month:7 | Day:1
import java.io.*;
import java.util.*;
public class HtmlTokenizer {
public static int HTML_TEXT=-1;
public static int UNKNOWN=-2;
static int HTML_EOF=-3;
static int H=-4;
static int R=-5;
static int T=-6;
static int B=-7;
static int M=-8;
static int E=-9;
static int END=-10;
Vector Hv=new Vector();
Vector Rv=new Vector();
Vector Tv=new Vector();
Vector Bv=new Vector();
Vector Mv=new Vector();
Vector Ev=new Vector();
public StreamTokenizer HtmlStream;
boolean outsideTag=true;
public void HtmlTokenizer(String month)
{
BufferedReader reader=null;
int tagType;
int oldtagType=0;
String filename="/"+month;
try{
reader=new BufferedReader(new InputStreamReader(new FileInputStream(filename)));
}catch(FileNotFoundException e){
System.out.println(e.getMessage());
}
HtmlStream=new StreamTokenizer(reader);
HtmlStream.resetSyntax();
HtmlStream.wordChars(0,255);
HtmlStream.ordinaryChar('<');
HtmlStream.ordinaryChar('>');
}
public int nextHtml()
{
int token;
try{
switch(token=HtmlStream.nextToken())
{
case StreamTokenizer.TT_EOF: return HTML_EOF;
case '<': outsideTag=false; return nextHtml();
case '>': outsideTag=true; return nextHtml();
case StreamTokenizer.TT_WORD:
if(HtmlStream.sval.toUpperCase().equals("R")&& !outsideTag ) {return R;}
else if(HtmlStream.sval.toUpperCase().equals("H") && !outsideTag ) {return H;}
else if(HtmlStream.sval.toUpperCase().equals("B") && !outsideTag ) {return B;}
else if(HtmlStream.sval.toUpperCase().equals("T") && !outsideTag ) {return T;}
else if(HtmlStream.sval.toUpperCase().equals("M") && !outsideTag ) {return M;}
else if(HtmlStream.sval.toUpperCase().equals("E") && !outsideTag ) {return E;}
else if(HtmlStream.sval.toUpperCase().equals("/R") && !outsideTag ) {return END;}
default:return UNKNOWN;
}
}catch(IOException e){
System.out.println("Error:"+e.getMessage());
}
return UNKNOWN;
}
public void get(String card_no)
{
String Rtmp=null;
String Htmp=null;
String Ttmp=null;
String Btmp=null;
String Mtmp=null;
String Etmp=null;
String UNKNOWNtmp=null;
int tagType;
int oldtagType=0;
while((tagType=nextHtml())!=HTML_EOF){
if(tagType==UNKNOWN&&oldtagType==H){
Htmp=HtmlStream.sval;
Hv.addElement(Htmp);
}else if(tagType==UNKNOWN&&oldtagType==T){
Ttmp=HtmlStream.sval;
Tv.addElement(Ttmp);
}else if(tagType==UNKNOWN&&oldtagType==B){
Btmp=HtmlStream.sval;
Bv.addElement(Btmp);
}else if(tagType==UNKNOWN&&oldtagType==M){
Mtmp=HtmlStream.sval;
Mv.addElement(Mtmp);
}
if(tagType==END)
{
if(Ttmp.indexOf(card_no)!=-1)
{
break;
}
Hv.removeAllElements();
Tv.removeAllElements();
Bv.removeAllElements();
Mv.removeAllElements();
}
oldtagType=tagType;
}
}
public boolean isnone()
{
return Hv.isEmpty();
}
public String getH()
{
return Hv.elementAt(0).toString();
}
public String getT()
{
return Tv.elementAt(0).toString();
}
public String getB()
{
return Bv.elementAt(0).toString();
}
public String getM(int i)
{
return Mv.elementAt(i).toString();
}
public int Msize()
{
return Mv.size();
}
}
/*
文件格式
213002|xxxxxxxxxxx|xxxxx|xxx
xxxxxxxxxxxxxxxxxxxxxxxxxxx |xxxx-xx-xx
xxxx| xxxx.xx
xxxx-xx-xx|xxxxxxx| |xxx.xx|xxxx.xx|xxxx.xx
xxxx-xx-xx|xxxxxxx| |xxx.xx|xxxx.xx|xxxx.xx
xxxx-xx-xx|xxxxxxx| |xxx.xx|xxxx.xx|xxxx.xx
xxxx-xx-xx|xxxxxxx| |xxx.xx|xxxx.xx|xxxx.xx
*/
10.6 StreamTokenizer
尽管StreamTokenizer并不是从InputStream或OutputStream衍生的,但它只随同InputStream工作,所以十分恰当地包括在库的IO部分中。
StreamTokenizer类用于将任何InputStream分割为一系列“记号”(Token)。这些记号实际是一些断续的文本块,中间用我们选择的任何东西分隔。例如,我们的记号可以是单词,中间用空白(空格)以及标点符号分隔。
下面是一个简单的程序,用于计算各个单词在文本文件中重复出现的次数:
//: 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();
}
}
} ///:~
最好将结果按排序格式输出,但由于Java 1.0和Java 1.1都没有提供任何排序方法,所以必须由自己动手。这个目标可用一个StrSortVector方便地达成(创建于第8章,属于那一章创建的软件包的一部分。记住本书所有子目录的起始目录都必须位于类路径中,否则程序将不能正确地编译)。
为打开文件,使用了一个FileInputStream。而且为了将文件转换成单词,从FileInputStream中创建了一个StreamTokenizer。在StreamTokenizer中,存在一个默认的分隔符列表,我们可用一系列方法加入更多的分隔符。在这里,我们用ordinaryChar()指出“该字符没有特别重要的意义”,所以解析器不会把它当作自己创建的任何单词的一部分。例如,st.ordinaryChar('.')表示小数点不会成为解析出来的单词的一部分。在与Java配套提供的联机文档中,可以找到更多的相关信息。
在countWords()中,每次从数据流中取出一个记号,而ttype信息的作用是判断对每个记号采取什么操作——因为记号可能代表一个行尾、一个数字、一个字串或者一个字符。
找到一个记号后,会查询Hashtable counts,核实其中是否已经以“键”(Key)的形式包含了一个记号。若答案是肯定的,对应的Counter(计数器)对象就会增值,指出已找到该单词的另一个实例。若答案为否,则新建一个Counter——因为Counter构建器会将它的值初始化为1,正是我们计算单词数量时的要求。
SortedWordCount并不属于Hashtable(散列表)的一种类型,所以它不会继承。它执行的一种特定类型的操作,所以尽管keys()和values()方法都必须重新揭示出来,但仍不表示应使用那个继承,因为大量Hashtable方法在这里都是不适当的。除此以外,对于另一些方法来说(比如getCounter()——用于获得一个特定字串的计数器;又如sortedKeys()——用于产生一个枚举),它们最终都改变了SortedWordCount接口的形式。
在main()内,我们用SortedWordCount打开和计算文件中的单词数量——总共只用了两行代码。随后,我们为一个排好序的键(单词)列表提取出一个枚举。并用它获得每个键以及相关的Count(计数)。注意必须调用cleanup(),否则文件不能正常关闭。
采用了StreamTokenizer的第二个例子将在第17章提供。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -