如何在StreamTokenizer中获得行偏移?

时间:2016-06-17 23:30:21

标签: java stream bufferedreader

我正在为我的类使用java中的StreamTokenizer类的解析器。在解析错误的情况下,我希望能够打印出现错误的令牌的字符的确切行和偏移量。但是,虽然StreamTokenizer有一个lineno()方法来查找tokenizer所在的行,但是没有方法可以找到该行中的字符偏移量。

我希望以某种方式使用StreamTokenizerBufferedReader中的可用函数来获取此偏移量,这是StreamTokenizer构造函数的输入。

到目前为止,我尝试过使用类似的东西:

BufferedReader dataReader = new BufferedReader(new FileReader(filename));
StreamTokenizer st = new StreamTokenizer(dataReader);
st.eolIsSignificant(true);

然后,我在

周围做了一个包装
 StreamTokenizer.nextToken()

功能使它看起来像这样:

 public int nextTokenSpec(StreamTokenizer st) throws IOException{
        int token = st.nextToken();

        if (token == StreamTokenizer.TT_EOL){
            Linker2.offsetCounter = 0;
            token = st.nextToken();
        } else{
            Linker2.offsetCounter += st.sval.length();
        }
        return token;
    }

请注意Linker2是一个驱动程序类,其中包含调用上述代码(BufferedReaderStreamTokenizer)的主函数。

然而,问题在于它忽略了令牌分隔符,因为它只根据令牌的长度递增。

我怀疑可能有某种方法可以直接到BufferedReader获取相关信息,但我不确定。

有谁知道如何获得StreamTokenizer函数的确切行偏移量?

2 个答案:

答案 0 :(得分:1)

简短的回答是,您无法使用StringTokenizer获得确切的行/字符偏移量。您需要使用不同的机制进行标记。

  

我怀疑可能有某种方法直接进入BufferedReader以获取有关此信息,但我不确定。

那不可靠。 StringTokenizer需要提前读取(尝试)查找当前令牌的结尾或下一个令牌(如果您调用hasMoreTokens())。记录在阅读器中的位置是预读的“高水位线”,而不是令牌的开头。

答案 1 :(得分:0)

不支持在行内获取令牌的位置,并且没有可靠的解决方法。但是您可以考虑替换StreamTokenizer,因为它的封装模式匹配无论如何都不是很先进。你可能偶然发现未来的其他缺陷,你也无法解决这些缺陷,而如果你能控制模式,它们很容易做得更好。我不是在谈论重新发明轮子,而是使用正则表达式:

public static void parseStreamTokenizer(String filename) throws IOException {
    try(Reader r=new FileReader(filename);
        BufferedReader dataReader = new BufferedReader(r);) {
        StreamTokenizer st=new StreamTokenizer(dataReader);
        for(;;) {
            double d=Double.NaN;
            String w=null;
            switch(st.nextToken()) {
                case StreamTokenizer.TT_EOF: return;
                case StreamTokenizer.TT_EOL: continue;
                case StreamTokenizer.TT_NUMBER: d=st.nval; break;
                case StreamTokenizer.TT_WORD: case '"': case '\'': w=st.sval; break;
            }
            consumeToken(st.lineno(), -1, st.ttype, w, d);
        }
    }
}
static final Pattern ALL_TOKENS = Pattern.compile(
     "(-?(?:[0-9]+\\.?[0-9]*|\\.[0-9]*))"       // number
   +"|([A-Za-z][A-Za-z0-9\\.\\-]*)"        // word
   +"|([\"'])((?:\\\\?.)*?)\\3" // string with backslash escape
   +"|/.*"        // StreamTokenizer's "comment char" behavior
   +"|\\s*"        // white-space
);
public static void parseRegex(String filename) throws IOException {
    try(Reader r=new FileReader(filename);
        BufferedReader dataReader = new BufferedReader(r)) {
        String line;
        int lineNo=0;
        Matcher m=ALL_TOKENS.matcher("");
        while((line=dataReader.readLine())!=null) {
            lineNo++;
            m.reset(line);
            int last=0;
            while(m.find()) {
                double d=Double.NaN;
                String word=null;
                for(int e=m.start(); last<e; last++) {
                    consumeToken(lineNo, last+1, line.charAt(last), word, d);
                }
                last=m.end();
                int type;
                if(m.start(1)>=0) {
                    type=StreamTokenizer.TT_NUMBER;
                    String n=m.group();
                    d=n.equals(".")? 0: Double.parseDouble(m.group());
                }
                else if(m.start(2)>=0) {
                    type=StreamTokenizer.TT_WORD;
                    word=m.group(2);
                }
                else if(m.start(4)>=0) {
                    type=line.charAt(m.start(3));
                    word=parse(line, m.start(4), m.end(4));
                }
                else continue;
                consumeToken(lineNo, m.start()+1, type, word, d);
            }
        }
    }
}
// the most complicated thing is interpreting escape sequences within strings
private static String parse(String source, int start, int end) {
    for(int pos=start; pos<end; pos++) {
        if(source.charAt(pos)=='\\') {
            StringBuilder sb=new StringBuilder(end-start+16);
            sb.append(source, start, pos);
            for(; pos<end; pos++) {
                if(source.charAt(pos)=='\\') {
                    int oct=0;
                    switch(source.charAt(++pos)) {
                        case 'n': sb.append('\n'); continue;
                        case 'r': sb.append('\r'); continue;
                        case 't': sb.append('\t'); continue;
                        case 'b': sb.append('\b'); continue;
                        case 'f': sb.append('\f'); continue;
                        case 'v': sb.append('\13'); continue;
                        case 'a': sb.append('\7'); continue;
                        case '0': case '1': case '2': case '3':
                            int next=pos+1;
                            if(next<end && (source.charAt(next)&~'7')==0)
                                oct=source.charAt(pos++)-'0';
                            // intentionally no break
                        case '4': case '5': case '6': case '7':
                            oct=oct*8+source.charAt(pos)-'0';
                            next=pos+1;
                            if(next<end && (source.charAt(next)&~'7')==0)
                                oct=oct*8+source.charAt(pos=next)-'0';
                            sb.append((char)oct);
                            continue;
                    }
                }
                sb.append(source.charAt(pos));
            }
            return sb.toString();
        }
    }
    return source.substring(start, end);
}
// called from both variants, to the same result (besides col values)
static void consumeToken(int line, int col, int id, String word, double number) {
    String type;
    Object o;
    switch(id)
    {
        case StreamTokenizer.TT_NUMBER: type="number"; o=number; break;
        case StreamTokenizer.TT_WORD: type="word"; o=word; break;
        case '"': case '\'': type="string"; o=word; break;
        default: type="char"; o=(char)id;
    }
    System.out.printf("l %3d, c %3s: token %-6s %s%n",
            line, col<0? "???": col, type, o);
}

请注意,parseStreamTokenizerparseRegex会产生相同的结果(我让他们解析自己的源代码),唯一的区别是parseRegex能够提供列号,即在一条线内的位置。

使代码看起来很复杂的原因是尝试重现与StreamTokenizer相同的结果,因为您没有详细说明您的实际用例。我不知道你是否真的需要像\v\a这样的非标准转义序列或字符串中的八进制转义符,或者你是否真的希望将一个点解释为0.0或是否所有数字都应以double值提供,但这就是StreamTokenizer所做的。

但我认为,对于每个实际用例,您的解析器迟早都需要超过StreamTokenizer(超出列数)的功能,这使得使用更复杂的代码是不可避免的。另一方面,它还为您提供了更多的控制,并允许摆脱不必要的东西,所以上面的代码应该提供一个很好的起点......