El día de hoy, le dije a un colega de trabajo que re.finditer() en Python es generalmente la forma preferida de tokenizar la entrada, en lugar de hacer girar a mano un lexer de un iterable de caracteres. Dijo que requiere leer todo el archivo en memoria. En el acto, dije tímidamente que probablemente puedas leerlo línea por línea y llamar a re.finditer () repetidamente si la expresión regular del token no cruza el límite de la línea.
Eso es cierto,pero no es una gran respuesta. Resulta que la construcción de un re encadenado.finditer() de una cadena iterable es posible, aunque no del todo sencillo.
El primer intento aquí demuestra la idea principal: desde re.las coincidencias de finditer () no se superponen, después de que se agoten todas las coincidencias, el siguiente re.finditer () debe reanudarse desde el resto de la cadena no coincidente.
def finditer_chained_bad(pattern, strings, flags=0): last = '' for s in strings: m = None for m in re.finditer(pattern, last + s, flags): yield m if not m: last = '' ##1 continue # m is the last match object. last = s
La línea con el comentario ##1
puede ser un punto de discusión. If re.finditer () no había dado ninguna coincidencia, la decisión de implementación aquí es descartar s completamente asumiendo que nada en ella coincidiría, pero se podría argumentar que una coincidencia potencial podría cruzar el límite de la cadena de entrada. Si esto se cambiara a last += s
, entonces funcionaría si la coincidencia potencial cruza el límite de entrada, a expensas del crecimiento de memoria ilimitado si un largo intervalo de la entrada nunca produjo ninguna coincidencia. El crecimiento ilimitado es necesario porque eso significa que una coincidencia potencial es mayor que el tamaño del fragmento de entrada, por lo que para obtener la coincidencia completa, necesitamos aumentar dinámicamente el tamaño del fragmento, que es lo que estamos haciendo aquí.
Otro problema es que la última coincidencia podría ser una coincidencia parcial que continúa en la siguiente cadena. Si lo cedemos ingenuamente, la persona que llama obtendría una coincidencia parcial rota. Para ilustrar esto, supongamos que la entrada se divide en cadenas de 8 caracteres:
def main(): for m in finditer_chained_bad(r'\w+', ): print m.group(0)
Imprime:
helloworld
La palabra «mundo» se divide en dos coincidencias por error debido al límite de la cadena. Para arreglar esto, solo necesitamos aplazar la última coincidencia y reiniciar la siguiente re.finditer() desde el comienzo de la última partida. El código de trabajo completo se convierte en:
def finditer_chained(pattern, strings, flags=0): last_s = '' for s in strings: last_m = None last_s += s for m in re.finditer(pattern, last_s, flags): if last_m: yield last_m last_m = m if not last_m: continue assert last_m is m last_s = s if last_m: yield last_m
y imprime correctamente:
helloworld
El finditer_chained () resultante acepta un objeto similar a un archivo porque un archivo es un iterable de líneas, como si re.finditer() se aplica a todo el archivo. Y funciona sin leer todo el archivo en memoria.