← 返回目录

第六章:文件操作

文件读写、目录操作与数据格式处理

1 文件读写

Ruby 的文件操作 API 简洁优雅,File 类提供了丰富的类方法和实例方法。使用 File.open 配合块是最推荐的方式——块结束时文件自动关闭,无需手动管理。

一次性读写(适合小文件)

content = File.read('config.txt')
puts content

content = File.read('config.txt', encoding: 'UTF-8')

File.write('output.txt', "Hello, Ruby!\n第二行内容\n")

File.write('log.txt', "新日志条目\n", mode: 'a')

File.open 块(推荐方式)

File.open('data.txt', 'r') do |file|
  content = file.read
  puts content
end

File.open('output.txt', 'w') do |file|
  file.puts '第一行'
  file.puts '第二行'
  file.print '不带换行'
  file.write "手动换行\n"
end

File.open('log.txt', 'a') do |file|
  file.puts "[#{Time.now}] 应用启动"
end

文件打开模式

模式 说明 文件不存在时
'r'只读(默认)报错
'w'只写(清空已有内容)创建
'a'追加写入创建
'r+'读写报错
'w+'读写(清空已有内容)创建
'a+'读写追加创建

逐行读取(适合大文件)

File.foreach('large_log.txt') do |line|
  puts line if line.include?('ERROR')
end

lines = File.readlines('data.txt')
puts "共 #{lines.size} 行"
puts "第一行: #{lines.first.chomp}"

IO.foreach('data.txt').with_index do |line, index|
  puts "#{index + 1}: #{line.chomp}"
end

File.open('huge_file.txt') do |file|
  file.lazy.each_line.select { |line| line.include?('WARNING') }.first(10).each do |line|
    puts line
  end
end

⚡ 读写要点

  • 始终使用 File.open { } 块形式,确保文件自动关闭
  • File.read 一次性读入内存,大文件用 foreach 逐行处理
  • puts 自动加换行,print / write 不加
  • chomp 去除行尾换行符,strip 去除首尾空白

2 目录操作

Dir 基础操作

puts Dir.pwd

Dir.chdir('/tmp') do
  puts Dir.pwd
end

Dir.mkdir('new_folder')
Dir.mkdir('nested/deep/folder') rescue nil

entries = Dir.entries('.')
puts entries.reject { |e| e.start_with?('.') }

Dir.children('.').each { |name| puts name }

rb_files = Dir.glob('**/*.rb')
puts "找到 #{rb_files.size} 个 Ruby 文件"

Dir.glob('app/**/*.{rb,erb}').each { |f| puts f }

Dir.glob('*.log').sort_by { |f| File.mtime(f) }.reverse.each do |f|
  puts "#{f} (#{File.mtime(f)})"
end

FileUtils 工具模块

require 'fileutils'

FileUtils.mkdir_p('path/to/nested/directory')

FileUtils.cp('source.txt', 'backup.txt')

FileUtils.cp_r('src_dir', 'backup_dir')

FileUtils.mv('old_name.txt', 'new_name.txt')

FileUtils.rm('temp.txt')
FileUtils.rm_f('maybe_exists.txt')

FileUtils.rm_rf('temp_directory')

FileUtils.touch('marker.txt')

Pathname 类(推荐)

require 'pathname'

path = Pathname.new('/Users/dev/project/src/main.rb')

puts path.dirname
puts path.basename
puts path.extname
puts path.basename('.rb')

config = Pathname.new('config')
db_config = config / 'database.yml'
puts db_config

path = Pathname.new('src/../config/./database.yml')
puts path.cleanpath

project = Pathname.new('/Users/dev/project')
project.children.each do |child|
  type = child.directory? ? '📁' : '📄'
  puts "#{type} #{child.basename}"
end

project.glob('**/*.rb') { |f| puts f }

💡 Dir vs FileUtils vs Pathname

  • Dir — 目录遍历和 glob 匹配
  • FileUtils — 文件/目录的复制、移动、删除等操作
  • Pathname — 面向对象的路径操作,推荐用于路径拼接和解析

3 CSV 处理

Ruby 标准库的 csv 模块提供了完善的 CSV 读写能力,支持 header 映射、类型转换和自定义分隔符。

读取 CSV

require 'csv'

table = CSV.read('users.csv', headers: true, encoding: 'UTF-8')
table.each do |row|
  puts "#{row['name']} - #{row['email']} (#{row['age']}岁)"
end

CSV.foreach('large_data.csv', headers: true) do |row|
  next if row['status'] == 'inactive'
  puts "#{row['id']}: #{row['name']}"
end

rows = CSV.read('data.csv')
rows.each { |row| puts row.inspect }

写入 CSV

CSV.open('output.csv', 'w', encoding: 'UTF-8') do |csv|
  csv << ['姓名', '邮箱', '年龄']
  csv << ['张三', 'zhangsan@example.com', 28]
  csv << ['李四', 'lisi@example.com', 32]
  csv << ['王五', 'wangwu@example.com', 25]
end

csv_string = CSV.generate do |csv|
  csv << ['id', 'name', 'score']
  csv << [1, '张三', 95]
  csv << [2, '李四', 88]
end
puts csv_string

高级用法

CSV.foreach('data.csv', headers: true, converters: :numeric) do |row|
  puts row['age'].class
end

CSV.foreach('data.tsv', col_sep: "\t", headers: true) do |row|
  puts row.to_h
end

users = CSV.read('users.csv', headers: true)
active_users = users.select { |row| row['status'] == 'active' }

sorted = users.sort_by { |row| row['name'] }
sorted.each { |row| puts row['name'] }

CSV.open('filtered.csv', 'w') do |out|
  out << ['name', 'email']
  CSV.foreach('users.csv', headers: true) do |row|
    out << [row['name'], row['email']] if row['age'].to_i >= 25
  end
end

4 JSON 文件处理

读取 JSON 文件

require 'json'

data = JSON.parse(File.read('config.json'))
puts data['database']['host']

data = JSON.parse(File.read('config.json'), symbolize_names: true)
puts data[:database][:host]

begin
  data = JSON.parse(File.read('data.json'))
rescue Errno::ENOENT
  puts '文件不存在'
  data = {}
rescue JSON::ParserError => e
  puts "JSON 格式错误: #{e.message}"
  data = {}
end

写入 JSON 文件

config = {
  database: {
    host: 'localhost',
    port: 5432,
    name: 'myapp_production'
  },
  redis: {
    url: 'redis://localhost:6379/0'
  },
  features: {
    dark_mode: true,
    notifications: false
  }
}

File.write('config.json', JSON.pretty_generate(config))

File.write('data.json', JSON.generate(data))

实用模式:JSON 配置管理

class JsonStore
  def initialize(path)
    @path = path
    @data = File.exist?(path) ? JSON.parse(File.read(path), symbolize_names: true) : {}
  end

  def get(key)
    @data[key.to_sym]
  end

  def set(key, value)
    @data[key.to_sym] = value
    save
  end

  def delete(key)
    @data.delete(key.to_sym)
    save
  end

  def all
    @data.dup
  end

  private

  def save
    File.write(@path, JSON.pretty_generate(@data))
  end
end

store = JsonStore.new('settings.json')
store.set(:theme, 'dark')
store.set(:language, 'zh-CN')
puts store.get(:theme)

5 YAML 处理

YAML 是 Ruby 生态的「母语」配置格式——Rails 的 database.ymlroutes.yml、GitHub Actions、Docker Compose 都使用 YAML。Ruby 标准库内置了 yaml 模块。

读取 YAML

require 'yaml'

config = YAML.load_file('config.yml')
puts config['database']['host']
puts config['database']['port']

config = YAML.load_file('config.yml', permitted_classes: [Symbol, Date, Time])

yaml_string = <<~YAML
  name: 我的应用
  version: 1.0
  features:
    - 用户管理
    - 权限控制
YAML
data = YAML.safe_load(yaml_string)
puts data['features']

写入 YAML

config = {
  'database' => {
    'adapter'  => 'postgresql',
    'host'     => 'localhost',
    'port'     => 5432,
    'database' => 'myapp_production',
    'pool'     => 5
  },
  'redis' => {
    'url' => 'redis://localhost:6379/0'
  }
}

File.write('config.yml', YAML.dump(config))

多环境配置模式

# database.yml
development:
  adapter: sqlite3
  database: db/development.db

test:
  adapter: sqlite3
  database: db/test.db

production:
  adapter: postgresql
  host: db.example.com
  port: 5432
  database: myapp_production
  username: deploy
  password: <%= ENV['DB_PASSWORD'] %>
  pool: 25
require 'yaml'
require 'erb'

env = ENV['RACK_ENV'] || 'development'
raw = File.read('config/database.yml')
parsed = ERB.new(raw).result
all_configs = YAML.safe_load(parsed)
db_config = all_configs[env]

puts "当前环境: #{env}"
puts "数据库: #{db_config['adapter']}://#{db_config['host']}/#{db_config['database']}"

🔄 配置文件格式对比

格式 可读性 注释 数据类型 常见用途
YAML优秀支持 #丰富(日期、多行字符串)Rails 配置、CI/CD
JSON一般不支持基础类型API 数据交换
TOML优秀支持 #丰富Rust/Go 配置
INI简单支持 ; #仅字符串PHP/Python 配置

6 文件元信息

文件检查

puts File.exist?('config.yml')
puts File.file?('config.yml')
puts File.directory?('app')
puts File.symlink?('link')
puts File.readable?('data.txt')
puts File.writable?('data.txt')
puts File.executable?('script.sh')
puts File.empty?('log.txt')
puts File.zero?('log.txt')

文件属性

puts File.size('data.bin')

def human_size(bytes)
  units = %w[B KB MB GB TB]
  return '0 B' if bytes == 0
  exp = (Math.log(bytes) / Math.log(1024)).to_i
  exp = units.size - 1 if exp >= units.size
  "%.1f %s" % [bytes.to_f / 1024**exp, units[exp]]
end

puts human_size(File.size('large_file.dat'))

puts File.mtime('data.txt')
puts File.atime('data.txt')
puts File.ctime('data.txt')

stat = File.stat('script.sh')
puts "大小: #{stat.size}"
puts "权限: #{stat.mode.to_s(8)}"
puts "所有者 UID: #{stat.uid}"
puts "硬链接数: #{stat.nlink}"

路径操作

puts File.basename('/path/to/file.rb')
puts File.basename('/path/to/file.rb', '.rb')
puts File.dirname('/path/to/file.rb')
puts File.extname('archive.tar.gz')
puts File.join('path', 'to', 'file.rb')
puts File.expand_path('~/projects')
puts File.expand_path('../config', __FILE__)

puts File.absolute_path?('/usr/local')
puts File.absolute_path?('relative/path')

实用示例:目录大小统计

require 'pathname'

def dir_size(path)
  Pathname.new(path)
    .glob('**/*')
    .select(&:file?)
    .sum(&:size)
end

def dir_summary(path)
  root = Pathname.new(path)
  stats = Hash.new(0)

  root.glob('**/*').select(&:file?).each do |file|
    ext = file.extname.downcase
    ext = '(无扩展名)' if ext.empty?
    stats[ext] += 1
  end

  stats.sort_by { |_, count| -count }.first(10).each do |ext, count|
    puts "  #{ext}: #{count} 个文件"
  end
end

puts "项目大小: #{human_size(dir_size('.'))}"
puts "文件类型分布:"
dir_summary('.')

⚡ 文件权限与安全

  • File.chmod(0644, 'file.txt') — 设置文件权限
  • File.chown(uid, gid, 'file.txt') — 更改所有者
  • 写入敏感文件时先写临时文件再重命名,避免部分写入导致数据损坏
  • 使用 Tempfile 创建安全的临时文件,自动清理

7 本章要点

📖 文件读写

File.read/File.write 快捷操作,File.open { } 块自动关闭,foreach 逐行处理大文件

📁 目录操作

Dir.glob 模式匹配,FileUtils 复制移动删除,Pathname 面向对象路径操作

📊 CSV

标准库内置,headers: true 自动映射列名,支持流式处理和自定义分隔符

📋 JSON

JSON.parse/JSON.generatepretty_generate 美化输出,symbolize_names 转 Symbol

⚙️ YAML

Ruby 生态首选配置格式,YAML.load_file 读取,支持 ERB 模板和多环境配置

🔍 元信息

File.exist?/File.size/File.mtime 检查文件属性,File.stat 获取完整信息

下一章预告:第七章将介绍 Ruby 的常用库与工具——Bundler 深入用法、RSpec 测试框架、Rake 构建工具,以及热门 Gem 推荐,完善你的 Ruby 工具箱。