如何包装java.io.Writer以在写入字符时执行正则表达式替换?
首先明确:这个问题并非不可解,但能否实现流式处理(不缓冲全部内容)完全取决于你使用的正则表达式结构。
核心问题拆解
你需要找到的"安全刷新点",是指该位置之前的字符,无论后续输入什么内容,都不可能和后续字符组成正则的匹配项。比如正则[ab]c,输入ab时,a就是安全点——因为a后面已经跟着b,不可能再和后续字符组成[ab]c的匹配;而b需要保留,因为后续可能出现c形成匹配。
为什么Matcher的hitEnd()/requireEnd()不够用
Java标准库的Matcher提供的这两个方法只能告诉你当前匹配是否因输入结束而中断,或者是否需要更多输入才能完成匹配,但它们无法给出具体的安全索引位置——也就是哪些字符绝对不会参与后续的匹配计算。
可行的实现思路
1. 基于正则的"最大前瞻长度"缓冲
对于大多数正则,你可以分析出它的最大前瞻长度:即匹配一个模式最多需要向前查看多少个后续字符。比如:
- 正则
[ab]c的最大前瞻长度是1(要匹配c,只需要保留前一个字符) - 正则
a{2}bc的最大前瞻长度是3(需要验证连续两个a之后的b和c)
基于这个长度,你只需要在内存中保留最近的N个字符(N=最大前瞻长度),前面的所有字符都可以安全刷新到目标Writer中。
2. 自定义缓冲+Matcher区域匹配
实现一个包装Writer,每次接收新字符后,用Matcher在缓冲中查找匹配,处理完匹配后,将超过最大前瞻长度的部分刷新出去。以下是简化的代码示例:
class RegexFilterWriter extends Writer { private final Writer target; private final Pattern pattern; private final String replacement; private final int maxLookahead; private final CharBuffer buffer; public RegexFilterWriter(Writer target, Pattern pattern, String replacement, int maxLookahead) { this.target = target; this.pattern = pattern; this.replacement = replacement; this.maxLookahead = maxLookahead; this.buffer = CharBuffer.allocate(4096); // 可根据性能调整缓冲大小 } @Override public void write(char[] cbuf, int off, int len) throws IOException { buffer.put(cbuf, off, len); buffer.flip(); Matcher matcher = pattern.matcher(buffer); int lastProcessed = 0; // 处理所有找到的匹配 while (matcher.find()) { // 写入匹配前的未处理内容 target.write(buffer.array(), lastProcessed, matcher.start() - lastProcessed); // 写入替换结果 target.write(replacement); lastProcessed = matcher.end(); } // 计算可安全刷新的位置:保留最后maxLookahead个字符,其余刷新 int safeFlushPos = buffer.limit() - maxLookahead; if (safeFlushPos > lastProcessed) { target.write(buffer.array(), lastProcessed, safeFlushPos - lastProcessed); lastProcessed = safeFlushPos; } // 保留缓冲中需要继续等待后续输入的部分 buffer.compact(); // 调整缓冲位置,保留lastProcessed之后的内容 int remaining = buffer.limit() - lastProcessed; buffer.position(remaining); buffer.limit(buffer.capacity()); } @Override public void flush() throws IOException { // 最终刷新时处理缓冲中剩余的所有内容 buffer.flip(); target.write(buffer.array(), 0, buffer.limit()); buffer.clear(); target.flush(); } @Override public void close() throws IOException { flush(); target.close(); } }
无法流式处理的正则场景
如果你的正则包含无限长度的可变结构,比如.*、a+这类可以匹配任意长度字符的模式,或者带有无限长度的前瞻/后顾断言,那么确实无法确定安全刷新点——因为任何前面的字符都可能被后续输入纳入匹配范围,这种情况下必须缓冲全部内容直到输入结束才能进行替换。
总结
只要你的正则是固定前瞻长度的,就可以通过上述方法实现流式替换,无需缓冲全部内容;只有当正则包含无限长度的可变结构时,才无法在流式场景下安全处理。
内容的提问来源于stack exchange,提问作者Robert Elliot

