Capítulo 14: Arquivos

Este capítulo apresenta a ideia de programas “persistentes”, que mantêm dados em armazenamento permanente, e mostra como usar tipos diferentes de armazenamento permanente, como arquivos e bancos de dados.

14.1 - Persistência

A maioria dos programas que vimos até agora são transitórios, porque são executados por algum tempo e produzem alguma saída, mas, quando terminam, seus dados desaparecem. Se executar o programa novamente, ele começa novamente do zero.

Outros programas são persistentes: rodam por muito tempo (ou todo o tempo); mantêm pelo menos alguns dos seus dados em armazenamento permanente (uma unidade de disco rígido, por exemplo); e se são desligados e reiniciados, continuam de onde pararam.

Exemplos de programas persistentes são sistemas operacionais, que rodam praticamente durante todo o tempo em que um computador está ligado, e servidores web, que rodam todo o tempo, esperando pedidos de entrada na rede.

Uma das formas mais simples para programas manterem seus dados é lendo e escrevendo arquivos de texto. Já vimos programas que leem arquivos de texto; neste capítulo veremos programas que os escrevem.

Uma alternativa é armazenar o estado do programa em um banco de dados. Neste capítulo apresentarei um banco de dados simples e um módulo, pickle, que facilita o armazenamento de dados de programas.

14.2 - Leitura e escrita

Um arquivo de texto é uma sequência de caracteres armazenados em um meio permanente como uma unidade de disco rígido, pendrive ou CD-ROM. Vimos como abrir e ler um arquivo em “Leitura de listas de palavras” na página 133.

Para escrever um arquivo texto, é preciso abri-lo com o modo 'w' como segundo parâmetro:

>>> fout = open('output.txt', 'w')

Se o arquivo já existe, abri-lo em modo de escrita elimina os dados antigos e começa tudo de novo, então tenha cuidado! Se o arquivo não existir, é criado um arquivo novo.

open retorna um objeto de arquivo que fornece métodos para trabalhar com o arquivo. O método write põe dados no arquivo:

>>> line1 = "This here's the wattle,\n"
>>> fout.write(line1)
24

O valor devolvido é o número de caracteres que foram escritos. O objeto de arquivo monitora a posição em que está, então se você chamar write novamente, os novos dados são acrescentados ao fim do arquivo:

>>> line2 = "the emblem of our land.\n"
>>> fout.write(line2)
24

Ao terminar de escrever, você deve fechar o arquivo:

>>> fout.close()

Se não fechar o arquivo, ele é fechado para você quando o programa termina.

14.3 - Operador de formatação

O argumento de write tem que ser uma string, então, se quisermos inserir outros valores em um arquivo, precisamos convertê-los em strings. O modo mais fácil de fazer isso é com str:

>>> x = 52
>>> fout.write(str(x))

Uma alternativa é usar o operador de formatação, %. Quando aplicado a números inteiros, % é o operador de módulo. No entanto, quando o primeiro operando é uma string, % é o operador de formatação.

O primeiro operando é a string de formatação, que contém uma ou várias sequências de formatação que especificam como o segundo operando deve ser formatado. O resultado é uma string.

Por exemplo, a sequência de formatação ‘%d’ significa que o segundo operando deve ser formatado como um número inteiro decimal:

>>> camels = 42
>>> '%d' % camels
'42'

O resultado é a string '42', que não deve ser confundida com o valor inteiro 42.

Uma sequência de formatação pode aparecer em qualquer lugar na string, então você pode embutir um valor em uma sentença:

>>> 'I have spotted %d camels.' % camels
'I have spotted 42 camels.'

Se houver mais de uma sequência de formatação na string, o segundo argumento tem que ser uma tupla. Cada sequência de formatação é combinada com um elemento da tupla, nesta ordem.

O seguinte exemplo usa '%d' para formatar um número inteiro, '%g' para formatar um número de ponto flutuante e '%s' para formatar qualquer objeto como uma string:

>>> 'In %d years I have spotted %g %s.' % (3, 0.1, 'camels')
'In 3 years I have spotted 0.1 camels.'

O número de elementos na tupla tem de corresponder ao número de sequências de formatação na string. Além disso, os tipos dos elementos têm de corresponder às sequências de formatação:

>>> '%d %d %d' % (1, 2)
TypeError: not enough arguments for format string
>>> '%d' % 'dollars'
TypeError: %d format: a number is required, not str

No primeiro exemplo não há elementos suficientes; no segundo, o elemento é do tipo incorreto.

Para obter mais informações sobre o operador de formato, veja https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting. Você pode ler sobre uma alternativa mais eficiente, o método de formatação de strings, em https://docs.python.org/3/library/stdtypes.html#str.format.

14.4 - Nomes de arquivo e caminhos

Os arquivos são organizados em diretórios (também chamados de “pastas”). Cada programa em execução tem um “diretório atual”, que é o diretório-padrão da maior parte das operações. Por exemplo, quando você abre um arquivo de leitura, Python o procura no diretório atual.

O módulo os fornece funções para trabalhar com arquivos e diretórios (“os” é a abreviação de “sistema operacional” em inglês). os.getcwd devolve o nome do diretório atual:

>>> import os
>>> cwd = os.getcwd()
>>> cwd
'/home/dinsdale'

cwd é a abreviação de “diretório de trabalho atual” em inglês. O resultado neste exemplo é /home/dinsdale, que é o diretório-padrão de um usuário chamado “dinsdale”.

Uma string como '/home/dinsdale', que identifica um arquivo ou diretório, é chamada de caminho (path).

Um nome de arquivo simples, como memo.txt, também é considerado um caminho, mas é um caminho relativo, porque se relaciona ao diretório atual. Se o diretório atual é /home/dinsdale, o nome de arquivo memo.txt se referiria a /home/dinsdale/memo.txt.

Um caminho que começa com / não depende do diretório atual; isso é chamado de caminho absoluto. Para encontrar o caminho absoluto para um arquivo, você pode usar os.path.abspath:

>>> os.path.abspath('memo.txt')
'/home/dinsdale/memo.txt'

os.path fornece outras funções para trabalhar com nomes de arquivo e caminhos. Por exemplo, os.path.exists que verifica se um arquivo ou diretório existe:

>>> os.path.exists('memo.txt')
True

Se existir, os.path.isdir verifica se é um diretório:

>>> os.path.isdir('memo.txt')
False
>>> os.path.isdir('/home/dinsdale')
True

De forma similar, os.path.isfile verifica se é um arquivo.

os.listdir retorna uma lista dos arquivos (e outros diretórios) no diretório dado:

 >>> os.listdir(cwd)
['music', 'photos', 'memo.txt']

Para demonstrar essas funções, o exemplo seguinte “passeia” por um diretório, exibe os nomes de todos os arquivos e chama a si mesmo recursivamente em todos os diretórios:

def walk(dirname):
    for name in os.listdir(dirname):
        path = os.path.join(dirname, name)
        if os.path.isfile(path):
            print(path)
        else:
            walk(path)

os.path.join recebe um diretório e um nome de arquivo e os une em um caminho completo.

O módulo os fornece uma função chamada walk, que é semelhante, só que mais versátil. Como exercício, leia a documentação e use-a para exibir os nomes dos arquivos em um diretório dado e seus subdiretórios. Você pode baixar minha solução em http://thinkpython2.com/code/walk.py.

14.5 - Captura de exceções

Muitas coisas podem dar errado quando você tenta ler e escrever arquivos. Se tentar abrir um arquivo que não existe, você recebe um IOError:

>>> fin = open('bad_file')
IOError: [Errno 2] No such file or directory: 'bad\_file'

Se não tiver permissão para acessar um arquivo:

>>> fout = open('/etc/passwd', 'w')
PermissionError: [Errno 13] Permission denied: '/etc/passwd'

E se tentar abrir um diretório para leitura, recebe

>>> fin = open('/home')
IsADirectoryError: [Errno 21] Is a directory: '/home'

Para evitar esses erros, você pode usar funções como os.path.exists e os.path.isfile, mas levaria muito tempo e código para verificar todas as possibilidades (se “Errno 21” significa algo, pode ser que pelo menos 21 coisas podem dar errado).

É melhor ir em frente e tentar, e lidar com problemas se eles surgirem, que é exatamente o que a instrução try faz. A sintaxe é semelhante à da instrução if…else:

try:
    fin = open('bad_file')
except:
    print('Something went wrong.')

O Python começa executando a cláusula try. Se tudo for bem, ele ignora a cláusula except e prossegue. Se ocorrer uma exceção, o programa sai da cláusula try e executa a cláusula except.

Lidar com exceções usando uma instrução try chama-se capturar uma exceção. Neste exemplo, a cláusula except exibe uma mensagem de erro que não é muito útil. Em geral, a captura de uma exceção oferece a oportunidade de corrigir o problema ou tentar novamente, ou, ao menos, de terminar o programa adequadamente.

14.6 - Bancos de dados

Um banco de dados é um arquivo organizado para armazenar dados. Muitos bancos de dados são organizados como um dicionário, porque mapeiam chaves a valores. A maior diferença entre um banco de dados e um dicionário é que o banco de dados está em um disco (ou outro armazenamento permanente), portanto persiste depois que o programa termina.

O módulo dbm fornece uma interface para criar e atualizar arquivos de banco de dados. Como exemplo, criarei um banco de dados que contém legendas de arquivos de imagem.

Abrir um banco de dados é semelhante à abertura de outros arquivos:

>>> import dbm
>>> db = dbm.open('captions', 'c')

O modo ‘c’ significa que o banco de dados deve ser criado, se ainda não existir. O resultado é um objeto de banco de dados que pode ser usado (para a maior parte das operações) como um dicionário.

Quando você cria um novo item, dbm atualiza o arquivo de banco de dados:

>>> db['cleese.png'] = 'Photo of John Cleese.'

Quando você acessa um dos itens, dbm lê o arquivo:

>>> db['cleese.png']
b'Photo of John Cleese.'

O resultado é um objeto bytes, o que explica o prefixo b. Um objeto bytes é semelhante a uma string, em muitos aspectos. Quando você avançar no Python, a diferença se tornará importante, mas, por enquanto, podemos ignorá-la.

Se fizer outra atribuição a uma chave existente, o dbm substitui o valor antigo:

>>> db['cleese.png'] = 'Photo of John Cleese doing a silly walk.'
>>> db['cleese.png']
b'Photo of John Cleese doing a silly walk.'

Alguns métodos de dicionário, como keys e items, não funcionam com objetos de banco de dados. No entanto, a iteração com um loop for, sim:

for key in db:
    print(key, db[key])

Como em outros arquivos, você deve fechar o banco de dados quando terminar:

>>> db.close()

14.7 - Usando o Pickle

Uma limitação de dbm é que as chaves e os valores têm que ser strings ou bytes. Se tentar usar algum outro tipo, vai receber um erro.

O módulo pickle pode ajudar. Ele traduz quase qualquer tipo de objeto em uma string conveniente para o armazenamento em um banco de dados, e então traduz strings de volta em objetos.

pickle.dumps recebe um objeto como parâmetro e retorna uma representação de string:

>>> import pickle
>>> t = [1, 2, 3]
>>> pickle.dumps(t)
b'\x80\x03]q\x00(K\x01K\x02K\x03e.'

O formato não é óbvio para leitores humanos; o objetivo é que seja fácil para o pickle interpretar. pickle.loads reconstitui o objeto:

>>> t1 = [1, 2, 3]
>>> s = pickle.dumps(t1)
>>> t2 = pickle.loads(s)
>>> t2
[1, 2, 3]

Embora o novo objeto tenha o mesmo valor que o antigo, não é (em geral) o mesmo objeto:

>>> t1 == t2
True
>>> t1 is t2
False

Em outras palavras, usar o pickle.dumps e pickle.loads tem o mesmo efeito que copiar o objeto.

Você pode usar o pickle para guardar variáveis que não são strings em um banco de dados. Na verdade, esta combinação é tão comum que foi encapsulada em um módulo chamado shelve.

14.8 - Pipes

A maior parte dos sistemas operacionais fornece uma interface de linha de comando, conhecida como shell. Shells normalmente fornecem comandos para navegar nos sistemas de arquivos e executar programas. Por exemplo, em Unix você pode alterar diretórios com cd, exibir o conteúdo de um diretório com ls e abrir um navegador web digitando (por exemplo) firefox.

Qualquer programa que possa ser aberto no shell também pode ser aberto no Python usando um objeto pipe, que representa um programa em execução.

Por exemplo, o comando Unix ls -l normalmente exibe o conteúdo do diretório atual no formato longo. Você pode abrir ls com os.popen[1]:

>>> cmd = 'ls -l'
>>> fp = os.popen(cmd)

O argumento é uma string que contém um comando shell. O valor de retorno é um objeto que se comporta como um arquivo aberto. É possível ler a saída do processo ls uma linha por vez com readline ou receber tudo de uma vez com read:

>>> res = fp.read()

Ao terminar, feche o pipe como se fosse um arquivo:

>>> stat = fp.close()
>>> print(stat)
None

O valor de retorno é o status final do processo ls; None significa que terminou normalmente (sem erros).

Por exemplo, a maior parte dos sistemas Unix oferece um comando chamado md5sum, que lê o conteúdo de um arquivo e calcula uma assinatura digital. Você pode ler sobre o MD5 em http://en.wikipedia.org/wiki/Md5. Este comando fornece uma forma eficiente de verificar se dois arquivos têm o mesmo conteúdo. A probabilidade de dois conteúdos diferentes produzirem a mesma assinatura digital é muito pequena (isto é, muito pouco provável que aconteça antes do colapso do universo).

Você pode usar um pipe para executar o md5sum do Python e receber o resultado:

>>> filename = 'book.tex'
>>> cmd = 'md5sum ' + filename
>>> fp = os.popen(cmd)
>>> res = fp.read()
>>> stat = fp.close()
>>> print(res)
1e0033f0ed0656636de0d75144ba32e0 book.tex
>>> print(stat)
None

14.9 - Escrevendo módulos

Qualquer arquivo que contenha código do Python pode ser importado como um módulo. Por exemplo, vamos supor que você tenha um arquivo chamado wc.py com o seguinte código:

def linecount(filename):
    count = 0
    for line in open(filename):
        count += 1
    return count

print(linecount('wc.py'))

Quando este programa é executado, ele lê a si mesmo e exibe o número de linhas no arquivo, que é 7. Você também pode importá-lo desta forma:

>>> import wc
7

Agora você tem um objeto de módulo wc:

>>> wc
<module 'wc' from 'wc.py'>

O objeto de módulo fornece o linecount:

>>> wc.linecount('wc.py')
7

Então é assim que se escreve módulos no Python.

O único problema com este exemplo é que quando você importa o módulo, ele executa o código de teste no final. Normalmente, quando se importa um módulo, ele define novas funções, mas não as executa.

Os programas que serão importados como módulos muitas vezes usam a seguinte expressão:

if __name__ == '__main__':
    print(linecount('wc.py'))

__name__ é uma variável integrada, estabelecida quando o programa inicia. Se o programa estiver rodando como um script, __name__ tem o valor '__main__'; neste caso, o código de teste é executado. Do contrário, se o módulo está sendo importado, o código de teste é ignorado.

Como exercício, digite este exemplo em um arquivo chamado wc.py e execute-o como um script. Então execute o interpretador do Python e import wc. Qual é o valor de __name__ quando o módulo está sendo importado?

Atenção: se você importar um módulo que já tenha sido importado, o Python não faz nada. Ele não relê o arquivo, mesmo se tiver sido alterado.

Se quiser recarregar um módulo, você pode usar a função integrada reload, mas isso pode causar problemas, então o mais seguro é reiniciar o interpretador e importar o módulo novamente.

14.10 - Depuração

Quando estiver lendo e escrevendo arquivos, você pode ter problemas com whitespace. Esses erros podem ser difíceis para depurar, porque os espaços, tabulações e quebras de linha normalmente são invisíveis:

>>> s = '1 2\t 3\n 4'
>>> print(s)
1 2      3
 4

A função integrada repr pode ajudar. Ela recebe qualquer objeto como argumento e retorna uma representação em string do objeto. Para strings, representa caracteres de whitespace com sequências de barras invertidas:

>>> print(repr(s))
'1 2\t 3\n 4'

Isso pode ser útil para a depuração.

Outro problema que você pode ter é que sistemas diferentes usam caracteres diferentes para indicar o fim de uma linha. Alguns sistemas usam newline, representado por \n. Outros usam um caractere de retorno, representado por \r. Alguns usam ambos. Se mover arquivos entre sistemas diferentes, essas inconsistências podem causar problemas.

Para a maior parte dos sistemas há aplicações para converter de um formato a outro. Você pode encontrá-los (e ler mais sobre o assunto) em http://en.wikipedia.org/wiki/Newline. Ou, é claro, você pode escrever um por conta própria.

14.11 - Glossário

persistente
Relativo a um programa que roda indefinidamente e mantém pelo menos alguns dos seus dados em armazenamento permanente.
operador de formatação
Um operador, %, que recebe uma string de formatação e uma tupla e gera uma string que inclui os elementos da tupla formatada como especificado pela string de formatação.
string de formatação
String usada com o operador de formatação, que contém sequências de formatação.
sequência de formatação
Sequência de caracteres em uma string de formatação, como %d, que especifica como um valor deve ser formatado.
arquivo de texto
Sequência de caracteres guardados em armazenamento permanente, como uma unidade de disco rígido.
diretório
Uma coleção de arquivos nomeada, também chamada de pasta.
caminho
String que identifica um arquivo.
caminho relativo
Caminho que inicia no diretório atual.
caminho absoluto
Caminho que inicia no diretório de posição mais alta (raiz) no sistema de arquivos.
capturar
Impedir uma exceção de encerrar um programa usando as instruções try e except.
banco de dados
Um arquivo cujo conteúdo é organizado como um dicionário, com chaves que correspondem a valores.
objeto bytes
Objeto semelhante a uma string.
shell
Programa que permite aos usuários digitar comandos e executá-los para iniciar outros programas.
objeto pipe
Objeto que representa um programa em execução, permitindo que um programa do Python execute comandos e leia os resultados.

14.12 - Exercícios

Exercício 14.1

Escreva uma função chamada sed que receba como argumentos uma string-padrão, uma string de substituição e dois nomes de arquivo; ela deve ler o primeiro arquivo e escrever o conteúdo no segundo arquivo (criando-o, se necessário). Se a string-padrão aparecer em algum lugar do arquivo, ela deve ser substituída pela string de substituição.

Se ocorrer um erro durante a abertura, leitura, escrita ou fechamento dos arquivos, seu programa deve capturar a exceção, exibir uma mensagem de erro e encerrar.

Solução: http://thinkpython2.com/code/sed.py.

Exercício 14.2

Se você baixar minha solução do Exercício 12.2 em http://thinkpython2.com/code/anagram_sets.py, verá que ela cria um dicionário que mapeia uma string ordenada de letras à lista de palavras que podem ser soletradas com aquelas letras. Por exemplo, 'opst' mapeia à lista ['opts', 'post', 'pots', 'spot', 'stop', 'tops'].

Escreva um módulo que importe anagram_sets e forneça duas novas funções: store_anagrams deve guardar o dicionário de anagramas em uma “prateleira” (objeto criado pelo módulo sheve); read_anagrams deve procurar uma palavra e devolver uma lista dos seus anagramas.

Solução: http://thinkpython2.com/code/anagram_db.py.

Exercício 14.3

Em uma grande coleção de arquivos MP3 pode haver mais de uma cópia da mesma música, guardada em diretórios diferentes ou com nomes de arquivo diferentes. A meta deste exercício é procurar duplicatas.

  1. Escreva um programa que procure um diretório e todos os seus subdiretórios, recursivamente, e retorne uma lista de caminhos completos de todos os arquivos com um dado sufixo (como .mp3). Dica: os.path fornece várias funções úteis para manipular nomes de caminhos e de arquivos.

  2. Para reconhecer duplicatas, você pode usar md5sum para calcular uma “soma de controle” para cada arquivo. Se dois arquivos tiverem a mesma soma de controle, provavelmente têm o mesmo conteúdo.

  3. Para conferir o resultado, você pode usar o comando Unix diff.

Solução: http://thinkpython2.com/code/find_duplicates.py.