domingo, 7 de outubro de 2007

Manipulando arquivos grandes em Python

Marcelo Toledo escreveu um artigo comparando a sua implmentação em C de um corretor ortográfico poposto por Peter Norgiv com a versão original em Python.

Porém Marcelo Toledo ao realizar essa comparação não levou em consideração que o exemplo desenvolvido por Peter Norvig era apensa um protótipo. Sendo assim, ele resolveu comparar ambos os programas, em C e Python, utilizando arquivos cada vez maiores e ilustrando a diferença de performance entre eles.

No código de Peter Norvig ele lê o arquivo de uma vez. Dá para imaginar o que acontenceu, baixa performance e "crash" com arquivos maiores de 100M devido a falta de RAM. :-(
Essa é a linha na qual o programa de Peter Norvig lê o arquivo e processa ele:
NWORDS = train(words(file('big.txt').read()))
Infelizmente Marcelo Toledo não procurou saber qual era o "bug" do código, deixando no ar uma idéia de que C é robusto é Python é uma linguagem não confiável.

Como eu fui questionado por um colega (Robson Peixoto) sobre o "porque" do problema com o código do corretor ortográfico. Resolvi então escrever um pequeno script que copia arquivos binários da forma correta. E assim fica claro que Python não tem problemas em manipular arquivos maiores que 100MB. ;-)

Segue um teste de uso do script copyfile.py

ruda@zion /tmp $ du -sh livecd-i686-installer-2007.0.iso
416M livecd-i686-installer-2007.0.iso

ruda@zion /tmp $ time ./copyfile.py livecd-i686-installer-2007.0
.iso teste.iso

real 0m39.708s
user 0m4.517s
sys 0m3.747s

Ou seja, sem usar muita CPU e nem mesmo RAM, esse script demorou 40
segundos para copiar um arquivo de 416MB, ou seja, 10MB/s aproximadamente.

Abaixo o código do script copyfile.py

#!/bin/env python
# -*- coding: utf-8 -*-
import sys

try:
origem = sys.argv[1]
destino = sys.argv[2]
except IndexError:
print "Modo de usar: copyfile.py origem destino"
sys.exit(1)

#Exemplo de leitura e gravação de arquivos grandes - usando modo binário
input = file(origem, 'rb')
output = file(destino, "wb")
for line in input:
output.write(line)

#Fechando os arquivos
input.close()
output.close()

7 comentários:

Gustavo Sverzut Barbieri disse...

Rudá,

No caso de um arquivo binário (como o tal .iso), seria mais aconselhável usar file.read(tamanho) (http://docs.python.org/lib/bltin-file-objects.html#l2h-302), pois ler por linhas pode chegar ao extremo do arquivo inteiro ser considerado apenas uma (na falta de um \n).

Fora isso muito bom post, eu também odeio este tipo de difamação gratuita.

Rudá Porto Filgueiras disse...

Gustavo,

Você tem razão.
Nesse caso ficaria assim:

data = input.read(4192)
while data:
output.write(data)
data = input.read(4192)

Os tempos de execução foram bem parecidos, mas poderia não ser. ;-)

real 0m41.049s
user 0m0.403s
sys 0m1.317s

A minha dúvida, e para isso teria que ver o código em C do objeto file, é se ele não é "esperto" na iteração com arquivos binários..?

sidnei disse...

So para dar um ultimo toque, tem um jeito mais simples ainda de fazer isso, que eh usar a funcao 'copyfileobj' do modulo 'shutil':

input = open(sys.argv[0], 'rb')
output = open(sys.argv[1], 'wb')
shutil.copyfileobj(input, output)

Rudá Porto Filgueiras disse...

Opa Sidnei,

Valeu pela dica. ;-)

De qualquer forma eu queria mostrar um exemplo de como ler o arquivo em blocos ou em linhas e não ele de uma vez como fez o Peter Norvig.

Foi ai que surgiu a idéia do script para copiar um arquivo grande, provando que Python, desde que programado de forma correta, consegue trabalhar muito bem com arquvos grandes.

Eu não quiz corrigir o código do corretor, pois achei que seria mais complicado de fazer e de testar.

Inclusive por que ele processa tudo de uma vez e joga na RAM. Na verdade seria necessário criar uma estrutura de armazenamento do arquivo processado, com índices de busca para que ese corretor funcione com dicionários maiores. :-(

Osvaldo Santana Neto disse...

Quando eu precisei recuperar um backup de minhas fotos em umas mídias "bixadas" eu precisava percorrer arquivos binários (10 arquivos de 650MB) de um lado para outro (índice no final, pedaços de arquivo espalhado, etc...)

Usando file.read() e .seek() deixou o programa extremamente ineficaz. Solução: mmap (http://docs.python.org/lib/module-mmap.html).

Rudá Porto Filgueiras disse...

Osvaldo,

Muito boa dica, eu nunca tinha ouvido falar no módulo "mmap". :-(

Nada como aprender coisas novas, ainda mais como os mestres. :-)

Osvaldo Santana Neto disse...

Tem um artigo recente (4 dias atrás) do Fredrik Lundh falando de um problema semelhante: http://effbot.org/zone/wide-finder.htm