Java中带位置索引的未知长度流分词解决方案咨询
解决Java字节流分词时的位置索引提取问题
我完全懂你现在的困境——要处理未知长度的字符流(没法等流全读完转成String,也不敢随便分块怕把单词拆成两半),还要给每个分词带上准确的起始/结束位置索引,但现有的StreamTokenizer只给你分词结果,完全没位置信息对吧?而且SimpleTokenizer还只支持String输入,根本没法直接用在流场景。
其实核心思路很简单:给你的输入Reader套一层“位置跟踪包装器”,让它在每次读取字符时自动累计全局字符位置,这样就能在StreamTokenizer识别出单词后,反向算出单词的起始和结束位置。
具体解决方案步骤
1. 实现跟踪字符位置的Reader包装类
我们需要自定义一个TrackingReader,它内部包装原始Reader,同时维护一个全局的字符位置计数器,每次读取字符时自动更新这个计数器:
import java.io.IOException; import java.io.Reader; class TrackingReader extends Reader { private final Reader delegate; private long currentPosition; public TrackingReader(Reader delegate) { this.delegate = delegate; this.currentPosition = 0; } @Override public int read(char[] cbuf, int off, int len) throws IOException { int charsRead = delegate.read(cbuf, off, len); if (charsRead > 0) { currentPosition += charsRead; } return charsRead; } @Override public void close() throws IOException { delegate.close(); } // 获取当前累计的字符位置 public long getCurrentPosition() { return currentPosition; } }
2. 修改现有分词逻辑,加入位置计算
接下来调整你的process方法,用TrackingReader包装原始输入Reader,然后在每次识别到单词时,通过当前位置和单词长度计算起始/结束索引(完全符合你要的左闭右开格式):
import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.io.StreamTokenizer; public class StreamTokenizerWithPosition { public static void main(String[] args) throws IOException { final Reader input = initializeInput(); final Writer output = initializeOutput(); try { final long count = process(input, output); output.flush(); } finally { input.close(); output.close(); } } protected static long process(Reader input, Writer output) throws IOException { // 用TrackingReader包装原始输入,跟踪字符位置 TrackingReader trackingReader = new TrackingReader(input); final StreamTokenizer st = new StreamTokenizer(trackingReader); st.eolIsSignificant(false); // 配置单词识别规则(可选,但显式设置更保险) // 允许数字作为单词的一部分(比如Java8) st.wordChars('0', '9'); // 允许大小写字母 st.wordChars('a', 'z'); st.wordChars('A', 'Z'); // 允许下划线 st.wordChars('_', '_'); // 把标点符号标记为普通字符,避免被识别为单词的一部分 st.ordinaryChar('!'); st.ordinaryChar('.'); st.ordinaryChar(' '); // 空格默认会被跳过,这里显式标记也没问题 long count = 0; int token; while ((token = st.nextToken()) != StreamTokenizer.TT_EOF) { if (token == StreamTokenizer.TT_WORD) { final String word = st.sval; // 当前位置是单词结束后的下一个字符位置,所以结束索引就是当前位置 long endPos = trackingReader.getCurrentPosition(); // 起始索引 = 结束索引 - 单词长度 long startPos = endPos - word.length(); // 按照你需要的格式输出结果 String tokenResult = String.format("(%s, %d, %d)", word, startPos, endPos); output.write(tokenResult + "\n"); output.flush(); count++; } } return count; } private static Writer initializeOutput() throws IOException { return new java.io.OutputStreamWriter(System.out, "UTF-8"); } private static Reader initializeInput() throws IOException { return new java.io.InputStreamReader(System.in, "UTF-8"); } }
为什么这个方案可行?
TrackingReader会准确跟踪所有被读取的字符,包括StreamTokenizer自动跳过的空白、标点等分隔符,因为StreamTokenizer的nextToken()内部最终会调用Reader的read方法,所有读取操作都会被计数。- 当
StreamTokenizer返回TT_WORD时,currentPosition正好指向单词结束后的下一个字符位置,减去单词长度就能得到单词的起始位置,完美匹配你要的(单词, 起始索引, 结束索引)格式(左闭右开)。 - 完全不需要把整个流转换成String,也不会拆分单词——
StreamTokenizer会自己处理跨缓冲的单词识别,我们只需要跟踪全局位置就行。
测试一下
输入你给的例子:Hello world! Java8 program.,输出会是:
(Hello, 0, 5) (world, 6, 11) (Java8, 13, 18) (program, 19, 25)
和你预期的结果完全一致!
内容的提问来源于stack exchange,提问作者podludek
相关产品推荐
相关产品推荐

