Ruby do Zero: Leitura e Escrita de Arquivos — File, IO e Boas Práticas

Capítulo 20

Tempo estimado de leitura: 11 minutos

+ Exercício

File e IO: o que são e como se relacionam

Em Ruby, IO representa um fluxo de entrada/saída (stream): algo de onde você lê bytes/caracteres e para onde você escreve. Um arquivo no disco é um caso específico de stream, e por isso a classe File herda de IO. Na prática, você vai usar File para abrir caminhos no sistema de arquivos e receber um objeto que responde a métodos de leitura e escrita.

Dois pontos guiam boas práticas com arquivos: (1) sempre fechar o arquivo (ou usar bloco para fechamento automático) e (2) separar “ler/processar/escrever” em funções pequenas para reduzir efeitos colaterais.

Abrindo arquivos com segurança: File.open com bloco

Ao usar File.open com bloco, Ruby garante o fechamento do arquivo ao final do bloco, mesmo se ocorrer uma exceção. Isso evita vazamento de recursos e arquivos “presos”.

File.open("dados.txt", "r") do |f|  # r = read (leitura)
  conteudo = f.read
  puts conteudo
end  # arquivo fechado automaticamente

Modos comuns:

  • "r": leitura (arquivo deve existir)
  • "w": escrita (cria ou sobrescreve)
  • "a": append (cria ou adiciona ao final)
  • "r+", "w+", "a+": leitura e escrita (variam em comportamento)

Leitura completa: File.read e IO#read

Para arquivos pequenos/médios, a leitura completa é simples e direta. Você pode usar File.read (atalho) ou abrir e chamar read.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

Exemplo: ler tudo e contar caracteres e linhas

texto = File.read("entrada.txt", mode: "r")

num_chars = texto.length
num_linhas = texto.count("\n") + (texto.end_with?("\n") || texto.empty? ? 0 : 1)

puts "Caracteres: #{num_chars}"
puts "Linhas: #{num_linhas}"

Observação: contar linhas por \n é uma estratégia simples; para contagem robusta e eficiente em arquivos grandes, prefira leitura linha a linha.

Leitura linha a linha: each_line e foreach

Para arquivos grandes, ler linha a linha reduz uso de memória. Você pode usar:

  • File.foreach(caminho): itera linhas sem abrir manualmente
  • File.open(...){|f| f.each_line ...}: mais controle (encoding, etc.)

Passo a passo: filtrar linhas e contar ocorrências

Objetivo: ler um arquivo de log e manter apenas linhas que contenham ERROR, além de contar quantas foram encontradas.

erros = []
contador = 0

File.foreach("app.log") do |linha|
  if linha.include?("ERROR")
    erros << linha
    contador += 1
  end
end

puts "Total de erros: #{contador}"
puts erros.first(3)

Se o arquivo for muito grande, evite acumular todas as linhas em erros; escreva diretamente em outro arquivo (ver seção de escrita).

Escrita e append: File.write, puts/print e modos w/a

Para escrever conteúdo de uma vez, File.write é um atalho útil. Para escrita incremental, abra com File.open e use puts (adiciona quebra de linha) ou print (não adiciona).

Escrever sobrescrevendo (modo w)

conteudo = "Relatório\nItens: 10\n"
File.write("relatorio.txt", conteudo)  # sobrescreve se existir

Append (modo a) para registrar eventos

File.open("eventos.log", "a") do |f|
  f.puts "#{Time.now} - usuário fez login"
end

Passo a passo: filtrar um arquivo grande e salvar resultado sem estourar memória

Objetivo: copiar apenas linhas que começam com OK para outro arquivo.

entrada = "status.txt"
saida   = "status_ok.txt"

File.open(saida, "w") do |out|
  File.foreach(entrada) do |linha|
    out.write(linha) if linha.start_with?("OK")
  end
end

Processamento de texto: contagem, filtragem e transformação

Arquivos de texto frequentemente exigem normalização (trim), transformação (uppercase/lowercase), remoção de ruído e contagens.

Exemplo: contagem de palavras (simples) linha a linha

contagem = Hash.new(0)

File.foreach("texto.txt") do |linha|
  palavras = linha.downcase.scan(/[\p{L}\p{N}']+/)
  palavras.each { |p| contagem[p] += 1 }
end

top_10 = contagem.sort_by { |_, v| -v }.first(10)
top_10.each { |palavra, qtd| puts "#{palavra}: #{qtd}" }

O scan acima tenta capturar letras e números Unicode (útil para português), além de apóstrofo em contrações. Ajuste o padrão conforme seu domínio.

Exemplo: transformação e limpeza (pipeline)

Objetivo: ler um CSV simples (uma coluna por linha), remover linhas vazias, normalizar espaços e salvar em outro arquivo.

def normalizar_linha(linha)
  linha.strip.gsub(/\s+/, " ")
end

File.open("limpo.txt", "w") do |out|
  File.foreach("bruto.txt") do |linha|
    n = normalizar_linha(linha)
    next if n.empty?
    out.puts n
  end
end

Encoding: cuidados práticos para evitar texto “quebrado”

Problemas de encoding aparecem quando o arquivo está em uma codificação (ex.: UTF-8, Windows-1252/ISO-8859-1) e você lê interpretando como outra. Em Ruby, strings têm encoding, e operações como downcase, regex e scan podem falhar ou produzir resultados incorretos se o encoding estiver errado.

Como especificar encoding ao abrir

Você pode passar encoding ou embutir no modo:

# Lê como UTF-8
File.open("dados.txt", "r:UTF-8") do |f|
  puts f.readline
end

# Converte de ISO-8859-1 para UTF-8 ao ler
File.open("legado.txt", "r:ISO-8859-1:UTF-8") do |f|
  f.each_line { |linha| puts linha }
end

Detectar e lidar com bytes inválidos

Se houver bytes inválidos, você pode optar por substituir ou remover durante a conversão:

conteudo = File.read("entrada.txt", encoding: "UTF-8", invalid: :replace, undef: :replace, replace: "?")
puts conteudo

Use substituição com cuidado: é melhor corrigir a origem quando possível, mas em pipelines de dados pode ser necessário para não interromper o processamento.

Manipulação de caminhos: File, Dir e FileUtils

Evite concatenar caminhos manualmente com "/", pois isso pode causar problemas entre sistemas. Prefira File.join e métodos de expansão.

Construindo caminhos de forma portátil

base = "dados"
arquivo = "entrada.txt"
caminho = File.join(base, arquivo)

puts caminho  # "dados/entrada.txt" (ou equivalente no sistema)

Caminho absoluto e relativo ao arquivo do script

# Absoluto a partir do diretório atual
abs = File.expand_path("dados/entrada.txt")

# Relativo ao diretório onde este arquivo Ruby está
abs2 = File.expand_path("../dados/entrada.txt", __dir__)

puts abs
puts abs2

Checagens comuns

path = "dados/entrada.txt"
puts File.exist?(path)
puts File.file?(path)
puts File.directory?("dados")
puts File.size(path) if File.exist?(path)

Estruturando funções para reduzir efeitos colaterais

Operações com arquivo são efeitos colaterais (dependem do ambiente e alteram o mundo externo). Para facilitar testes e manutenção, uma estratégia é separar:

  • Funções puras: recebem dados (strings/arrays/hashes) e retornam dados transformados.
  • Funções de borda (I/O): leem do disco e escrevem no disco, chamando as funções puras.

Exemplo: pipeline “ler → transformar → escrever”

def extrair_linhas_validas(texto)
  texto.each_line
       .map { |l| l.strip }
       .reject { |l| l.empty? || l.start_with?("#") }
end

def normalizar_linhas(linhas)
  linhas.map { |l| l.gsub(/\s+/, " ") }
end

def processar_arquivo(entrada_path, saida_path, encoding: "UTF-8")
  texto = File.read(entrada_path, encoding: encoding)
  linhas = extrair_linhas_validas(texto)
  linhas = normalizar_linhas(linhas)
  File.write(saida_path, linhas.join("\n") + "\n")
end

processar_arquivo("dados/bruto.txt", "dados/limpo.txt")

Note que extrair_linhas_validas e normalizar_linhas não sabem nada sobre arquivos: elas apenas transformam dados. Isso torna o código mais fácil de reaproveitar e testar.

Boas práticas adicionais ao trabalhar com arquivos

  • Prefira streaming (linha a linha) para arquivos grandes; reserve File.read para casos em que o tamanho é controlado.
  • Use bloco em File.open para fechamento automático.
  • Seja explícito com encoding quando a origem do arquivo for incerta ou quando houver acentos.
  • Evite misturar lógica de negócio com I/O: mantenha funções de transformação separadas.
  • Escreva de forma atômica quando necessário: em cenários críticos, escreva em arquivo temporário e depois renomeie (reduz risco de arquivo parcialmente escrito).

Exemplo completo: relatório de palavras filtradas e saída em dois arquivos

Cenário: você tem um arquivo de texto e quer (1) gerar um arquivo com linhas que contenham uma palavra-chave e (2) gerar um arquivo de estatísticas com contagem de palavras (top 20). O processamento deve ser eficiente e com encoding explícito.

def linhas_com_palavra(path, palavra, encoding: "UTF-8")
  enumerator = Enumerator.new do |y|
    File.open(path, "r:#{encoding}") do |f|
      f.each_line do |linha|
        y << linha if linha.downcase.include?(palavra.downcase)
      end
    end
  end
  enumerator
end

def contar_palavras(path, encoding: "UTF-8")
  contagem = Hash.new(0)
  File.open(path, "r:#{encoding}") do |f|
    f.each_line do |linha|
      linha.downcase.scan(/[\p{L}\p{N}']+/).each do |p|
        contagem[p] += 1
      end
    end
  end
  contagem
end

entrada = "dados/texto.txt"
saida_linhas = "dados/linhas_filtradas.txt"
saida_stats  = "dados/stats.txt"
palavra = "ruby"

File.open(saida_linhas, "w:UTF-8") do |out|
  linhas_com_palavra(entrada, palavra, encoding: "UTF-8").each do |linha|
    out.write(linha)
  end
end

contagem = contar_palavras(entrada, encoding: "UTF-8")
top_20 = contagem.sort_by { |_, v| -v }.first(20)

File.open(saida_stats, "w:UTF-8") do |out|
  top_20.each do |pal, qtd|
    out.puts "#{pal}\t#{qtd}"
  end
end

Nesse exemplo, a leitura é feita em streaming, a escrita é separada por responsabilidade (linhas filtradas vs. estatísticas) e o encoding é explicitado para reduzir surpresas com caracteres acentuados.

Agora responda o exercício sobre o conteúdo:

Ao processar um arquivo muito grande e gerar outro arquivo com apenas algumas linhas filtradas, qual abordagem é mais adequada para evitar alto consumo de memória e ainda garantir o fechamento do arquivo?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Para arquivos grandes, a leitura em streaming (linha a linha) reduz o uso de memória. Usar File.open com bloco garante o fechamento automático do arquivo, mesmo em caso de exceção, e escrever diretamente evita acumular linhas em arrays.

Próximo capitúlo

Ruby do Zero: Projeto Final — Aplicação de Linha de Comando com Coleções, Blocos e Organização

Arrow Right Icon
Capa do Ebook gratuito Ruby do Zero: Fundamentos, Coleções, Blocos e Organização de Código
95%

Ruby do Zero: Fundamentos, Coleções, Blocos e Organização de Código

Novo curso

21 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.