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 automaticamenteModos 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.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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 manualmenteFile.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 existirAppend (modo a) para registrar eventos
File.open("eventos.log", "a") do |f|
f.puts "#{Time.now} - usuário fez login"
endPasso 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
endProcessamento 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
endEncoding: 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 }
endDetectar 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 conteudoUse 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 abs2Checagens 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.readpara casos em que o tamanho é controlado. - Use bloco em
File.openpara 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
endNesse 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.