Python intermediário - III

Entrada e saída de dados

O fluxo de informações que ocorre entre o disco rígido de nosso computador e o que vemos impresso na tela, ou entre um ponto de comunicação remoto e o nosso computador local é geralmente realizado por meio de objetos-tipo arquivo (file objects). A esses objetos dá-se também o nome de input/output stream, de onde segue o termo I/O stream.

Tipos de arquivo

Para entendimento prático, streams são os tradicionais “arquivos” que transitam em nossas máquinas. Os dois tipos principais de arquivo são:

  • Texto formatado (Text file). Arquivos de texto recebem e produzem objetos str. Isto significa que um processo de codificação/decodificação dos dados ocorre durante o fluxo.

  • Binário (Binary file). Arquivos binários lidam com a objetos na forma de bytes. Isto significa que nenhum processo de codificação/decodificação dos dados ocorre durante o fluxo e que o texto é não formatado.

Operações com arquivos

Podemos realizar diversas operações com arquivos, tais como modo de apenas leitura (read-only), modo de escrita (write) e modo de escrita e leitura (read-write). Neste capítulo, aprenderemos a manipular o fluxo de entrada e saída de diversos tipos de arquivo. A forma mais fácil de criar uma stream é usando a função predefinida open.

Exemplo: ler arquivo de texto.

f = open('../etc/icd.yml')
f
<_io.TextIOWrapper name='../etc/icd.yml' mode='r' encoding='UTF-8'>

Comentários:

  • Por padrão, open abre um arquivo em modo de leitura.

  • O arquivo é especificado pelo seu caminho no disco.

  • f é um objeto abstrato que mostra o esquema de codificação usado para decodificar arquivo. Quando não especificado, o encoding padrão do sistema é utilizado.

  • Percebe-se que o encoding padrão do sistema é UTF-8.

Exemplo: ler arquivo de texto com especificação de modo.

# o arquivo aqui é uma imagem PNG
o = open('../database/eyes/SyntEyeKTC-106_a-net.png',mode='r')
o
<_io.TextIOWrapper name='../database/eyes/SyntEyeKTC-106_a-net.png' mode='r' encoding='UTF-8'>

Comentários:

  • O segundo argumento de open é mode, no qual especificamos o modo com que o arquivo será tratado.

  • Para modo de leitura, usamos 'r'; para modo de escrita, usamos 'w'; para modo de apensamento, usamos 'a' e, para modo binário, usamos 'b'. A seguir, veremos mais exemplos.

  • É importante notar que tanto f quanto o estão “abertos” e devem ser fechadas imediatamente após o uso para evitar consumo desnecessário de memória e outros problemas.

# as streams 'f' e 'o' estão abertas 
f.closed,o.closed
(False, False)
# fechando streams
f.close(), o.close()

# verifica novamente que foram fechadas
f.closed,o.closed
(True, True)

Exemplo: Ler arquivo e armazenar conteúdo.

with open('../etc/icd.yml') as f:
    content = f.read()

# imprime conteúdo do arquivo
print(content)
# Arquivo YAML para construir o ambiente 'icd'

name: icd # nome do ambiente
channels: # lista de canais a utilizar
  - defaults # canais padrão
  - conda-forge
dependencies: # pacotes dependentes
  - numpy
  - scipy
  - sympy
  - matplotlib
  - pandas
  - seaborn  
# não é necessário fechar!
f.closed
True

Comentários:

  • A keyword with possibilita que o fluxo de arquivos seja facilitado.

  • Com a keyword with o arquivo é automaticamente fechado.

  • É possível gerir a abertura de mais de um arquivo com apenas uma chamada de with usando a seguinte instrução

with open() as a, open() as b:

Note

Para saber mais acerca de with, consulte o PEP 343.

with open('../etc/icd.yml','r') as f, open('../database/bras-cubas.txt') as g:
    content_f = f.read()
    content_g = g.read()

# opera sobre conteúdos
len(content_f),type(content_g)    
(266, str)

Modos de operação de arquivos

Antes de prosseguirmos, vamos resumir os principais modos com os quais um arquivo é operado em Python.

Caracter

Significado

'r'

abre para leitura (padrão)

'w'

abre para escrita, truncando o arquivo

'x'

cria um novo arquivo e o abre para escrita

'a'

abre para escrita apensando conteúdo no fim do arquivo

'b'

abre em modo binário

't'

abre em modo texto (padrão)

Para uma discussão mais ampla sobre todas as possibilidades, consulte este post e este.

Exemplo: Ler arquivo linha por linha.

with open('../database/bras-cubas.txt','rt',encoding='utf-8') as f:
    for linha in f:
        print(linha.lower())
não durou muito a evocação; a realidade dominou logo; o presente expeliu o passado. talvez eu exponha ao leitor, em algum canto deste livro, a minha teoria das edições humanas. o que por agora importa saber é que virgília — chamava-se virgília — entrou na alcova, firme, com a gravidade que lhe davam as roupas e os anos, e veio até o meu leito. o estranho levantou-se e saiu. era um sujeito, que me visitava todos os dias para falar do câmbio, da colonização e da necessidade de desenvolver a viação férrea; nada mais interessante para um moribundo. saiu; virgília deixou-se estar de pé; durante algum tempo ficamos a olhar um para o outro, sem articular palavra. quem diria? de dois grandes namorados, de duas paixões sem freio, nada mais havia ali, vinte anos depois; havia apenas dois corações murchos, devastados pela vida e saciados dela, não sei se em igual dose, mas enfim saciados. virgília tinha agora a beleza da velhice, um ar austero e maternal; estava menos magra do que quando a vi, pela última vez, numa festa de são joão, na tijuca; e porque era das que resistem muito, só agora começavam os cabelos escuros a intercalar-se com alguns fios de prata.

Discussão:

  • Note que especificar o modo 'rt' é totalmente decorativo, já que são as opções padrão.

  • O parâmetro encoding indica a codificação na qual o texto deve ser lido. UTF-8 é o padrão. Entretanto, veremos adiante outros sistemas de encoding de caracteres.

Escrita de arquivos de texto

Escrever conteúdo para arquivos é uma das tarefas mais frequentes do processamento de dados. Para escrever conteúdo em um arquivo, devemos assumir ou que ele é inexistente e precisa ser criado, ou que ele existe e queremos adicionar informações nele.

Exemplo: escrever um simulacro de “jogo da velha” com caracteres em um arquivo .txt.

def velha(n):
    '''Simula um jogo da velha fictício.'''
    
    from random import sample
    
    c = ['o','x']            
    l = f'  -   -   -  \n'
    ll = l
    
    for _ in range(3):
        l += f'| {sample(c,1)[0]} | {sample(c,1)[0]} | {sample(c,1)[0]} |\n'
    l += ll # sobrecarga de +
    
    l = f'Game: #{n}\n' + l + '\n' 
    
    return l


# cria log de n partidas
def n_games_log(n):
    
    with open('../etc/velha-log.txt','w') as v:
        for i in range(1,n+1):
            v.write(velha(i)) # escreve em arquivo
            
# executa função para 4 partidas            
n_games_log(4)            

Para visualizarmos o conteúdo do arquivo podemos usar a função cat via terminal ou abrir uma nova stream de leitura (exercício):

!cat '../etc/velha-log.txt'
Game: #1
  -   -   -  
| x | o | o |
| x | x | o |
| o | o | o |
  -   -   -  

Game: #2
  -   -   -  
| x | x | o |
| x | o | x |
| o | o | x |
  -   -   -  

Game: #3
  -   -   -  
| o | o | x |
| x | x | x |
| o | o | x |
  -   -   -  

Game: #4
  -   -   -  
| o | o | x |
| o | x | o |
| x | o | o |
  -   -   -  

Exemplo: escrever arquivo e apensar dados.

# escreve
with open('../etc/lista.txt','w') as f:
    f.write( str(list(range(5))).strip('[]')) # escreve str e purga '[' e ']'

!cat ../etc/lista.txt
0, 1, 2, 3, 4
# abre para apensar
with open('../etc/lista.txt','a') as f:
    f.write(', ') # o que temos sem isto?
    f.write(str(list(range(5,11))).strip('[]'))

!cat ../etc/lista.txt    
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

Comentários:

  • No modo de apensamento, o arquivo original não é sobrescrito, mas alterado em seu final.

  • No exemplo anterior, incluímos a complementaridade da sequência de inteiros de 5 a 10 que não estava no arquivo anterior.

Exemplo: apensar em arquivo redirecionando a saída de print.

with open('../etc/lista.txt','a') as f:
    print('!!!',file=f) # mesmo objeto f

!cat ../etc/lista.txt 
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10!!!

Comentário:

  • Neste exemplo, a string '!!!', que seria impressa na tela por print, é apensada no arquivo lista.txt via redirecionamento.

Hint

Em ambiente UNIX, o redirecionamento pode ser feito via Terminal com o operador > e o apensamento, com >>. Por exemplo, o comando cat > lista.txt cria um arquivo vazio e redireciona as linhas digitáveis em tela para o arquivo. O comando cat >> lista.txt, por outro lado, permite que mais linhas digitáveis em tela sejam adicionadas ao arquivo. Veja um exemplo aqui.

Exemplo: escrever para arquivo com redirecionamento, star expression e sem with.

dado = ('graus C',16,22.5) # tupla com diferentes dados

f = open('../etc/lista-2.txt','w')
print(*dado,sep=',',file=f) 
f.close() # sem 'with', é necessário fechar a stream 

!cat ../etc/lista-2.txt
graus C,16,22.5

Discussão:

  • Neste exemplo, *dado desempacota a tupla – que tem tipos de dado diferentes –, separa os elementos por ,, imprime na tela e redireciona para o arquivo de texto lista-2.txt.

  • Observe que o arquivo é aberto em modo de escrita.

Escrita de arquivos binários

Exemplo: pré-visualizar um arquivo PDF no Jupyter Notebook.

from IPython.display import IFrame
IFrame('../etc/logo-icd.pdf',width=500,height=500)

Comentários:

  • Arquivos PDF, assim como áudios, imagens e executáveis, possuem conteúdo em formato binário. O que visualizamos acima é compreensível por humanos.

  • O exemplo a seguir mostra o real conteúdo do arquivo PDF em termos de bytes.

Warning

Tente reproduzir este exemplo com o Jupyter Notebook executando localmente em sua máquina. Em alguns navegadores, o livro online pode não reproduzir esta pré-visualização.

Exemplo: ler arquivo binário e imprimir seu conteúdo.

# modo 'rb'
with open('../etc/logo-icd.pdf','rb') as f:
    pdf = f.read()

# 200 primeiros caracteres
pdf[:200]
b'%PDF-1.3\n%\xc4\xe5\xf2\xe5\xeb\xa7\xf3\xa0\xd0\xc4\xc6\n3 0 obj\n<< /Filter /FlateDecode /Length 87 >>\nstream\nx\x01+T\x08T(T0\x00BSKS\x05\x0b\x13#\x85\xa2T\x85p\x85<\x05\xfd\x80\xd4\xa2\xe4\xd4\x82\x92\xd2\xc4\x1c\x85\xa2L\xa0\x1acCc=S\xb0Jc\x03K=sS\x05C\x13\x03\x10edf\xa6ghd\xca\x95\x9c\xab\xa0\xef\x99k\xa8\xe0\x92\x0f42\x10\x00\xc7\xbe\x15)\nendstream\nendobj\n1 0 obj\n<< /Type /Pa'
# verifica tipo da variável
type(pdf)
bytes

Comentários:

  • A variável pdf guarda dados em formato binário.

  • O prefixo 'b' sugere que o tipo de dado é binário e está “codificado” em linguagem de bytes.

  • Perceba que nós, humanos, enxergamos caracteres, mas o computador enxerga apenas bytes.

Exemplo: escrever arquivo binário.

# modo 'wb'
with open('../etc/logo-icd-part.pdf','wb') as f:
    f.write(pdf[:200])    
from IPython.display import IFrame
IFrame('../etc/logo-icd-part.pdf',width=500,height=500)

Discussão:

  • O conteúdo parcial tomado da string de bytes 'pdf' é escrito em um segundo arquivo.

  • Ao ler o conteúdo e tentar visualizá-lo, um erro será lançado. Isto é naturalmente esperado, visto que a sequência de bytes foi, propositalmente, danificada.

Warning

Tente reproduzir este exemplo com o Jupyter Notebook executando localmente em sua máquina. Em alguns navegadores, o livro online pode não reproduzir esta pré-visualização.

Como evitar sobrescrição acidental

Quando temos um arquivo existente no disco e operamos com escrita de arquivos com nomes similares, é altamente provável que sobrescrevamos o conteúdo daquele arquivo acidentalmente. Para evitar este problema, podemos usar o modo 'x', que garante a “exclusividade” do arquivo existente.

Exemplo: escrever arquivo em modo de “exclusividade”.

with open('../etc/lista.txt','x') as f:
    print('???',file=f) # mesmo objeto f
---------------------------------------------------------------------------
FileExistsError                           Traceback (most recent call last)
<ipython-input-20-692bb94f1e63> in <module>
----> 1 with open('../etc/lista.txt','x') as f:
      2     print('???',file=f) # mesmo objeto f

FileExistsError: [Errno 17] File exists: '../etc/lista.txt'

Discussão:

  • Tentamos escrever outro conteúdo para o arquivo lista.txt, o que geraria uma sobrescrição de seu conteúdo original.

  • Utilizando o modo 'x', o interpretador se encarrega de verificar se o arquivo existe e só permite que a operação seja concluída em caso negativo.

  • Como o arquivo existe no disco, um erro de FileExistsError é lançado.

  • O próximo exemplo é bem-sucedido, visto que lista-3.txt não existe ainda no disco.

with open('../etc/lista-3.txt','x') as f:
    print('OK!',file=f) # mesmo objeto f
    
!cat ../etc/lista-3.txt    
OK!
# remove o arquivo para reproduzir teste
!rm ../etc/lista-3.txt
# verifica remoção
!cat ../etc/lista-3.txt   
cat: ../etc/lista-3.txt: No such file or directory

Testando a existência de arquivos

Uma forma de verificar a existência de arquivos no disco é valer-se do módulo os – discutiremos um pouco sobre este módulo à frente –. O exemplo a seguir praticamente alcança o mesmo objetivo que o modo 'x' ao checar a pré-existência de arquivos.

from os.path import exists

if exists('../etc/lista-3.txt'):
    print('O arquivo existe... Pé no freio... :( ')
else:
    print('O arquivo não existe! Pé na tábua! 8) ')
O arquivo não existe! Pé na tábua! 8) 

Note

O módulo os fornece meios poderosos para navegar pelo sistema operacional e é bastante útil na leitura e escrita de arquivos.

Leitura e escrita de arquivos comprimidos

A compressão de dados baseia-se em reduzir a quantidade de bits utilizadas para armazenar os dados, mas assegurar a sua integridade genuína.

Em Python, temos à disposição, por exemplo, os módulos bz2 e gzip para operar com arquivos comprimidos. Ambos são baseados na biblioteca zlib. A seguir, veremos exemplos de como podemos manipular arquivos comprimidos.

Exemplo: escrever arquivos comprimidos.

# sem compressão
with open('../etc/texto.txt','w') as f:
    f.write('[]'*500000)

# compressão com bz2
import bz2
with bz2.open('../etc/texto.bz2','w') as f:
    f.write(b'[]'*500000) # deve ser string de bytes
    
# compressão com gzip
import gzip
with gzip.open('../etc/texto.gz','w') as f:
    f.write(b'[]'*500000) # deve ser string de bytes      
! ls -l ../etc/texto.*
-rw-r--r--@ 1 gustavo  staff       73 Sep  2 12:36 ../etc/texto.bz2
-rw-r--r--@ 1 gustavo  staff     1011 Sep  2 12:36 ../etc/texto.gz
-rw-r--r--@ 1 gustavo  staff  1000000 Sep  2 12:36 ../etc/texto.txt

Comentários:

  • Note a diferença no tamanho dos arquivos. A taxa de compressão é gigantesca! Os arquivos comprimidos por bz2 e gzip possuem 73 e 1011 bytes de tamanho, nesta ordem, ao passo que o não comprimido possui 1.000.000 de bytes (1 MB) de tamanho.

  • Para realizarmos a leitura dos arquivos, basta alterar o modo de 'w' para 'r'.

O módulo os

Ao trabalharmos com leitura e escrita de arquivos, é importante saber navegar pelo sistema operacional, ou listando diretórios, seja criando arquivos em lote, seja buscando por extensões específicas. Com o módulo os, podemos realizar uma série de operações para manipular caminhos de arquivos. Abaixo, discutimos algumas funções desse módulo.

Exemplo: manipular caminhos para coletar informações de diretórios.

import os

arq = '../database/bras-cubas.txt'
# última parte do caminho
os.path.basename(arq)
'bras-cubas.txt'
# diretório
os.path.dirname(arq)
'../database'
# cria caminho unindo partes
os.path.join('pasta','subpasta',os.path.basename(arq))
'pasta/subpasta/bras-cubas.txt'
# separa basename e extensão
os.path.splitext(arq)
('../database/bras-cubas', '.txt')
# separa pasta e nome
os.path.split(arq)
('../database', 'bras-cubas.txt')

Exemplo: realizar testes para verificar tipos de arquivo.

# testa se é arquivo
os.path.isfile('../etc/velha-log.txt')
True
# testa se é diretório
os.path.isdir('../etc/icd.yml')
False

Comentários:

  • Com o submódulo os.path, podemos acessar praticamente toda a hierarquia de arquivos e diretórios no sistema operacional.

Coletando metadados de arquivos

Metadados dos arquivos, tais como tamanho e data de modificação, podem ser obtidos por meio do módulo os também.

Exemplo: obtendo tamanho do arquivo.

# no. de bytes
os.path.getsize('../etc/logo-icd.pdf') 
23496
import time
time.ctime(os.path.getmtime('../etc/logo-icd.pdf'))
'Fri Aug 27 23:26:13 2021'

Discussão:

  • A função getmtime retorna a informação temporal da última modificação do arquivo especificado.

  • O módulo time fornece funções para manipular quantidades em unidade de tempo.

  • A função ctime converte uma medida de tempo em segundos para uma string de data/hora de acordo com as configurações locais da máquina.

Exemplo: listar diretórios.

os.listdir('../etc/')
['velha-log.txt',
 'texto.bz2',
 'texto.txt',
 'icd.yml',
 'texto.gz',
 'logo-icd-part.pdf',
 '.ipynb_checkpoints',
 'lista-2.txt',
 'lista.txt',
 'logo-icd.pdf']

Exemplo: buscar por todos os arquivos comuns em um diretório.

pasta = '../etc/'
arqs = [a for a in os.listdir(pasta)
       if os.path.isfile(os.path.join(pasta,a))]
arqs
['velha-log.txt',
 'texto.bz2',
 'texto.txt',
 'icd.yml',
 'texto.gz',
 'logo-icd-part.pdf',
 'lista-2.txt',
 'lista.txt',
 'logo-icd.pdf']

Exemplo: buscar por todos os diretórios.

pasta = '../'
dirs = [a for a in os.listdir(pasta)
       if os.path.isdir(os.path.join(pasta,a))]
dirs
['database',
 'papers',
 'figs',
 'etc',
 'ipynb',
 'todo',
 '_build',
 '.ipynb_checkpoints',
 '.git',
 'rise']

Exemplo: buscar por todos os arquivos de uma dada extensão.

# lista apenas .txt
txts = [a for a in os.listdir('../etc/')
       if a.endswith('.txt')]
txts
['velha-log.txt', 'texto.txt', 'lista-2.txt', 'lista.txt']

Codificação e decodificação de caracteres

Os caracteres que vemos impressos na tela de um dispositivo digital são apenas símbolos renderizados, isto é “marcas”. Como sabemos, um computador entende apenas uma linguagem binária. Isto significa que antes de um caracter ser mostrado exatamente como esperamos, digamos, em uma “tela”, é necessário que ele seja processado, grosso modo, por duas “camadas abstratas”. Uma é a camada de armazenamento, que associa um número binário ao caracter; a outra, é a camada textual, que usa pontos de código.

Existem vários sistemas de codificação de caracteres. Alguns bem conhecidos são ASCII, ISO-8859, CP-1252 e UTF-8. Entretanto, atualmente, o sistema UTF-8 se destaca pelo uso largamente difundido. No passado, devido à diferença de sistemas de codificação, o número que representava um caracter em um sistema não era o mesmo em outro sistema. Por isso, o padrão [Unicode] propôs a definição de um único número para cada caracter, permanente, e que valesse independentemente de plataforma, programa ou linguagem. Assim, hoje em dia, a melhor definição de caracter que existe é a de um caracter Unicode.

A identidade de um caracter Unicode é o seu ponto de código (code point), um número de 0 a 1.114.111 (em base 10) mostrado no padrão Unicode. O padrão Unicode é formado por 4 a 6 dígitos hexadecimais seguidos do prefixo “U+”. A tabela abaixo mostra alguns exemplos de caracteres, seu ponto de código e nome no padrão Unicode.

Caracter

Ponto de código

Nome Unicode

â

U+00E2

LATIN SMALL LETTER A WITH CIRCUMFLEX

ݔ

U+0754

ARABIC LETTER BEH WITH TWO DOTS BELOW AND DOT ABOVE

😛

U+1F61B

FACE WITH STUCK-OUT TONGUE

U+304E

HIRAGANA LETTER GI

Para imprimir em tela caracteres Unicode como os da tabela acima, temos duas maneiras:

  • usando uma string Unicode hexadecimal de 32-bits, em cujo caso escrevemos uma string iniciada por \U acompanhada de 8 caracteres. Os últimos caracteres correspondem ao ponto de código e as posições anteriores são preenchidas com 0. Neste caso, os caracteres acima poderiam ser impressos com:

'\U000000E2','\U00000754','\U0001f61b','\U0000304e'
  • usando o nome Unicode do caracter, em cujo caso escrevemos uma string iniciada por \N acompanhada do nome exato do caracter confinado entre chaves. Neste caso, os caracteres acima poderiam ser impressos com:

'\N{LATIN SMALL LETTER A WITH CIRCUMFLEX}',
'\N{ARABIC LETTER BEH WITH TWO DOTS BELOW AND DOT ABOVE}',
'\N{FACE WITH STUCK-OUT TONGUE}',
'\N{HIRAGANA LETTER GI}'

Todos os caracteres Unicode são encontrados em tabelas separadas por classes (planes), as chamadas [Code Charts].

bytes x texto

A conversão de pontos de código em bytes é chamada de codificação, ou encoding, ao passo que a conversão de bytes em pontos de código é chamada de decodificação, ou decoding.

Exemplo: codificação e decodificação.

s = 'balé'
len(s)
4
b = s.encode('utf8')
b
b'bal\xc3\xa9'
# caracter 'é' representado por dois bytes
# \xc3 e \xa9
len(b)
5
b.decode('utf8')
'balé'

Discussão:

  • encode leva o texto para bytes.

  • b' indica uma string literal de bytes.

  • bal está no intervalo ASCII imprimível, enquanto que \xc3 e \xa9 não estão.

  • decode leva de bytes para texto.

Comentários:

  • O próprio caracter ASCII é usado para bytes no intevalo imprimível.

  • Para bytes correspondendo ao TAB, newline, carriage return e contrabarra, as sequências de escape \t, \n, \r e \\ são usadas.

  • Para qualquer outro byte, usa-se uma sequência de escape hexadecimal.

Exemplo: decodificação para outros sistemas.

# erro! 
# 'é' não é ASCII
s.encode().decode('ascii')
---------------------------------------------------------------------------
UnicodeDecodeError                        Traceback (most recent call last)
<ipython-input-45-036775282faf> in <module>
      1 # erro!
      2 # 'é' não é ASCII
----> 3 s.encode().decode('ascii')

UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 3: ordinal not in range(128)
s.encode().decode('iso8859')
'balé'
s.encode().decode('cp1252')
'balé'

Referências complementares