如何在流令牌器中获取行偏移量?



我正在为我的类开发一个解析器,该解析器使用Java中的StreamTokenizer类。在解析错误的情况下,我希望能够打印发生错误的标记开头的字符的确切行和偏移量。但是,虽然StreamTokenizer具有查找分词器所在哪行的lineno()方法,但没有方法可以查找该行内的字符偏移量。

我希望有办法以某种方式使用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)的 main 函数。

但是,这样做的问题是它忽略了令牌分隔符,因为它仅根据令牌的长度递增。

我怀疑可能有一些方法可以直接去BufferedReader获取有关这方面的信息,但我不确定。

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

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

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

这不会可靠地工作。StringTokenizer需要提前阅读以(尝试)找到当前令牌或下一个令牌的末尾(如果调用hasMoreTokens())。 读取器中记录的位置是预读的"高水位标记",而不是令牌的开始。

不支持获取令牌在行内的位置,也没有可靠的方法来解决此问题。但是您可以考虑替换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相同的结果,因为您没有指定有关实际用例的更多信息。我不知道你是否真的需要非标准的转义序列,如va或字符串中的八进制转义,或者你是否真的希望将单个点解释为0.0,或者是否所有数字都应该作为double值提供,但这就是StreamTokenizer的作用。

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

最新更新