O problema básico é que preciso gerar todas as correspondências de uma regex dentro de um arquivo, mas há algumas propriedades que tornam a solução complicada:
- O arquivo pode ser muito grande, então não é possível trazê-lo para a memória e digitalizá-lo inteiro de uma só vez.
- O regex é fornecido pelo usuário, portanto não há estimativa de quão grande uma correspondência poderia ser
- A expressão regular pode ter várias linhas, o que torna as correspondências possíveis ainda mais longas, e a varredura por linha pode fazer com que uma correspondência que cruze as linhas seja perdida.
- Cada correspondência deve ser retornada com sua posição no arquivo inteiro.
- As correspondências devem ser avaliadas lentamente (não há necessidade de encontrar todas as correspondências no arquivo de uma vez)
Como exemplo para 3, este algoritmo
while (not end of file):
line = file.readNextLine()
for (each match in regex.find(line)):
results.append(match)
return results
quebras no caso de
// inline modifier to turn on DOTALL
regex = "(?s)start.*end"
content = "start
end"
porque start
então end
seriam apresentados individualmente, quando o ponto pudesse corresponder a um caractere de nova linha.
Minha ideia atual é implementar Iterator<MatchResult>
, e ler em a CharBuffer
como uma janela deslizante. java.util.regex.Matcher
Também tem hitEnd()
, que pode sinalizar se mais entradas teriam alterado o resultado da última find()
operação (se a expressão regular for "foobar" e o correspondente tiver sido avaliado em "foo", hitEnd()
retornaria verdadeiro. Se o correspondente tiver sido avaliado em "bar", retornaria falso). Se o buffer estiver cheio e mais entradas puderem encontrar uma correspondência, o tamanho do buffer será dobrado até que uma correspondência seja encontrada. Eu poderia implementar Spliterator
em vez disso, mas não sei se isso é mais ou menos complicado.
Até agora, tenho esta estrutura básica (omitindo o tratamento de exceções por questões de brevidade):
public class RegexStreamIterator implements Iterator<MatchResult> {
private static final int MIN_BUFFER_SIZE_BYTES = 8192;
private static final int MAX_BUFFER_EXPANSIONS = 4;
private static final int MAX_BUFFER_SIZE_BYTES = (int) Math.pow(2, MAX_BUFFER_EXPANSIONS) * MIN_BUFFER_SIZE_BYTES;
private final Reader reader;
private final Pattern pattern;
private int readerIndex = 0;
private Matcher matcher;
private CharBuffer buffer = CharBuffer.allocate(MIN_BUFFER_SIZE_BYTES);
private MatchResult currentMatch;
public RegexStreamIterator(Reader reader, Pattern pattern) {
this.reader = reader;
this.pattern = pattern;
}
public boolean hasNext() {
if (currentMatch != null) return true;
advance();
return currentMatch != null;
}
public MatchResult next() {
if (currentMatch == null)
advance();
if (currentMatch == null)
throw new NoSuchElementException();
MatchResult ret = currentMatch;
currentMatch = null;
return ret;
}
private void advance() {
// pseudocode while I'm stuck on actual implementation
if matcher is null
fill buffer
if buffer is null
return
matcher = pattern.matcher(buffer)
while not end of file:
if matcher found result and not matcher.hitEnd() // match found and won't grow or get rejected with more input
this.currentMatch = match
advance matcher to next match
return
if matcher.hitEnd()
if no match was found in buffer (matcher start index at 0)
double buffer size
if new size bigger than max size, throw exception
fill from file
reset matcher
else (part of buffer consumed)
move remaining buffer to new buffer
fill from file
reset matcher
else
clear buffer (resetting size)
fill from file
reset matcher
}
}
Estou muito preso a problemas como o tratamento de nulos e a criação de um loop que possa buscar a próxima correspondência e seja estável entre várias chamadas para advance()
. Isso parece ser um problema que já foi resolvido antes. Se bibliotecas comuns como Apache ou Guava tiverem algo que cubra isso, seria fantástico.