[Python-es] Buscar, leer y escribir archivos grandes con Python
lasizoillo
lasizoillo en gmail.com
Vie Jun 3 13:37:21 CEST 2011
El día 3 de junio de 2011 12:39, Kiko <kikocorreoso en gmail.com> escribió:
> Usando ipython defino tres funciones:
>
> def abrekiko(entrada, salida):
> input = open(entrada, 'r')
> output = open(salida, 'w')
> for l in input:
> output.write(l.split(',')[-1])
> input.close()
> output.close()
>
> def abrejoe(entrada,salida):
> with open(entrada,'r') as input:
> with open(salida,'w') as output:
> [output.write(l.split(',')[-1]) for l in input]
>
> def abrelasi(entrada,salida):
> with open(entrada,'r') as input:
> with open(salida,'w') as output:
> [output.write(l.rsplit(',',1)[-1]) for l in input]
>
> Los resultados que me salen son:
> In [15]: timeit abrekiko(entrada,salida)
> 1 loops, best of 3: 735 ms per loop
>
> In [16]: timeit abrejoe(entrada,salida)
> 1 loops, best of 3: 766 ms per loop
>
> In [17]: timeit abrelasi(entrada,salida)
> 1 loops, best of 3: 563 ms per loop
>
> Mi entrada es un fichero con 300.000 líneas y 8 columnas separadas por coma.
>
> Claramente, rsplit parece que funciona mejor. He hecho mi función (abrekiko)
> con una list comprehesion y también va un poquito más lenta que con el for a
> pelo y tenía entendido que usar list comprehensions era más efectivo.
>
list comprehensions genera una lista con todos los datos de la
iteración, lo cual no es muy bueno para el uso de la memoria. Usando
generadores (como list comprehensions pero usando parentesis en vez de
corchetes) se consume menos memoría, lo cual también es bueno.
Respecto a lo de que vaya un poquito más rápido o un poquito más lento
puede ser por temas varios. Si ejecutas el test dos veces verás que
hay cierta desviación, por lo que pequeñas mejoras no son
representativas.
> ¿La diferencia de rsplit con respecto a split es solo que empieza el split
> por la derecha?
>
Si, esa es la única diferencia. Pero el truquito en cuestión está en
el segundo parámetro. Ese 1 le indica que pare al cortar un elemento.
En este caso concreto solo queremos el último, por lo que no tiene
demasiado sentido seguir buscando en la cadena y troceando el resto de
los campos. Aparte del recorrido en la cadena, la creación de esas
nuevas cadenas para representar cada uno de los capos no es gratis.
> ¿Alguien tiene formas más rápidas de lectura de ficheros de texto?
>
Respuesta corta: Si, no, a veces.
Respuesta larga (me pongo en modo chapas así que dejar de leer si se quiere):
Moraleja 1: Si el cuello de botella es el acceso a disco poco hay por hacer
Un día me puse hacer una guerra de lenguajes implementando un wc
(programa para contar lineas) muy simple en c, haskell y python.
En C las pasé canutas porque la función readline del estandar C99 no
estaba en la librería estandar del OSX, por lo que tuve que hacer una
implementación que hiciera uso de buffers. Quedó un código muy largo y
me costo cierto tiempo hacerlo.
En haskell, quizá por ser un lenguaje que tampoco domino, me quedó un
programa corto y eficiente, pero cada caracter del codigo había que
pensarlo con cautela. La forma de hacer el plegado de funciones para
que la evaluación perezosa no fuese un problema, la forma de que la
lectura usará bien buffers, ... todo eso hizo que me costara horrores
escribir unas pocas lineas de código.
En python el programa era ligeramente más largo que haskell, pero me
salió solito y sin pensarlo mucho.
Los resultados fueron curiosos. Cuando el fichero no estaba en cache
de disco, el cuello de botella era el acceso a este, por lo que los
tres códigos tardaban lo mismo en ejecutarse. Estando el fichero en
cache (cosa que tampoco sería demasiado normal, supongo) la versión c
y haskell eran mucho más rápidas que la versión en python. La
diferencia entre c y haskell no era muy grande siendo la primera algo
más rápida.
Moraleja 2: Depende de la estructura de ese fichero de texto.
Si en vez de tener que leer el último campo en el ejemplo de este
correo, se hubiera tenido que leer uno de en medio la optimización
realizada sería menos evidente.
Si el fichero tiene alguna cadena larga fácil de buscar, algoritmos
como el de Boyer-Moore[1] pueden ayudar bastante.
Si el fichero en vez del tipo csv fuese uno con campos fijos, se
podría hacer un mmap y acceder directamente a las zonas de memoria
donde estan los datos, evitando un recorrido sobre el fichero.
Si el trabajo a hacer sobre el fichero es pesado (cuello de botella en
cpu) se puede probar a parelizar con multiproceso. Eso si, a veces la
sincrinización/comunicación entre procesos puede quitar más
rendimiento del que aporta.
Si ...
Por desgracia no conozco recetas mágicas para usar en todos los casos.
Pero si que hay algunas herramientas que se pueden tener en reserva
para aplicar en casos concretos. De ahí que la respuesta corta fuese
"si, no, a veces" ;-)
[1] http://en.wikipedia.org/wiki/Boyer%E2%80%93Moore_string_search_algorithm
Moraleja 3: No centrarse solo en la ejecución del código.
Si el objetivo es tener el resultado de una ejecución concreta, desde
que eres consciente del problema hasta que obtienes el resultado, el
tiempo de ejecución del script que hace la tarea suele ser el menor de
todos. Un programa en C que se ejecuta en 2 segundos y que tarda en
escribirse 2 horas es peor solución a un programa ineficiente en
python que tarda en escribirse 5 minutos y 10 en ejecutarse. En este
caso concreto yo hubiera escrito lo siguiente:
cat fichero | awk 'BEGIN{FS="|"}{print $9}' > resultado
Simplemente porque un oneliner en la consola me cuesta menos que abrir
el vim y escribir un script de python.
Un saludo:
Javi
Más información sobre la lista de distribución Python-es