Ruby 文件与 IO

这是本系列的最后一章。任何非平凡程序都要和文件打交道——读配置、写日志、处理 CSV。Ruby 的文件 API 设计得很优雅,尤其是 File.open 配块的"自动关闭"模式。本章还讲异常处理(begin/rescue),它是健壮代码的基石。

1. 读与写:最简形式

# 最简单的读:一次读全文件成字符串
content = File.read("data.txt")

# 最简单的写:覆盖写
File.write("out.txt", "Hello, File!")

# 追加写
File.write("log.txt", "新的一行\n", mode: "a")

# 按行读(大文件友好)
File.foreach("big.log") do |line|
  puts line.chomp      # chomp 去掉行尾换行
end

# 按行读到数组
lines = File.readlines("data.txt")
lines.first            # 第一行
lines.length           # 总行数

File.read / File.write一次性操作——读小文件、写小文件用它们最爽。大文件必须用 foreach / readlines 按行读,避免一次性把几百 MB 全加载进内存。

2. File.open + 块:自动关闭

这是 Ruby 文件操作的最佳实践——把文件操作放进块,块结束时自动 close,即使中间抛异常也会关闭:

# File.open + 块:自动关闭文件(强烈推荐!)
File.open("data.txt", "r") do |f|
  f.each_line do |line|
    puts line
  end
end   # 文件在块结束时自动 close,即使中间抛异常

# 不用块:必须手动 close(容易忘,容易泄漏)
f = File.open("data.txt")
begin
  puts f.gets      # 读一行
ensure
  f.close          # 必须关闭
end

# 模式字符
#   "r"  只读(默认)
#   "w"  只写(文件不存在则创建,存在则清空)
#   "a"  追加(不存在则创建)
#   "r+" 读写(文件必须存在)
#   "w+" 读写(清空)
#   "b"  二进制(Windows 需要,如 "rb")

原则:永远用块形式。手动 open + close 容易忘关、容易在异常时泄漏文件句柄。块形式让你写起来既简洁又安全。

3. 文件查询与路径操作

# 文件判断(都是 File 类方法)
File.exist?("data.txt")       # 是否存在
File.file?("data.txt")        # 是不是普通文件
File.directory?("/tmp")       # 是不是目录
File.readable?("data.txt")    # 可读?
File.writable?("data.txt")    # 可写?
File.size("data.txt")         # 字节大小
File.size?("empty.txt")       # 大小,nil 如果是 0 或不存在
File.zero?("empty.txt")       # 是否为空

# 文件信息
File.stat("data.txt").mtime   # 修改时间(Time 对象)
File.extname("data.txt")      # ".txt"
File.basename("/a/b/c.txt")   # "c.txt"
File.dirname("/a/b/c.txt")    # "/a/b"
File.join("a", "b", "c.txt")  # "a/b/c.txt"(跨平台拼路径)

# 重命名 / 删除
File.rename("old.txt", "new.txt")
File.delete("new.txt")

File.join 是跨平台拼路径的方法——Windows 上拼成反斜杠,Unix 上拼成正斜杠。永远不要手写字符串拼接路径,用 File.joinPathname(更高级)。

4. 目录操作

# 列出目录内容
Dir.entries("/tmp")              # [".", "..", "file1", ...]
Dir.glob("*.rb")                 # 当前目录所有 .rb
Dir.glob("lib/**/*.rb")          # 递归所有子目录
Dir["src/*.rb"]                  # glob 的方括号简写

# 遍历
Dir.foreach("/tmp") { |name| puts name }
Dir.glob("*.rb").each { |path| puts path }

# 创建与删除目录
Dir.mkdir("new_dir")
Dir.mkdir("new_dir", 0o755)      # 带权限(八进制)
Dir.rmdir("new_dir")             # 只能删空目录

# 递归创建(Rails 的 FileUtils)
require 'fileutils'
FileUtils.mkdir_p("a/b/c")       # 一次创建多层
FileUtils.cp("a.txt", "b.txt")   # 复制
FileUtils.mv("a.txt", "dir/")    # 移动
FileUtils.rm_rf("big_dir")       # 递归删(谨慎!)

FileUtils.mkdir_p 是高频方法——一次创建多层目录(类似 mkdir -p),写部署脚本、缓存目录时常用。rm_rf核武器,用之前确认路径!

5. CSV 标准库

处理 CSV 不需要第三方 gem,Ruby 标准库自带 CSV:

# CSV:标准库,需要 require
require 'csv'

# 一次性读全表
rows = CSV.read("data.csv")
rows.first         # ["name", "age", "city"]
rows[1]            # ["Alice", "30", "NYC"]

# 带表头读成 hash
CSV.foreach("data.csv", headers: true) do |row|
  puts "#{row['name']} - #{row['age']}"
end

# 写 CSV
CSV.open("out.csv", "w") do |csv|
  csv << ["name", "age"]               # 表头
  csv << ["Alice", 30]
  csv << ["Bob", 25]
end

# 字符串直接转 CSV
CSV.parse("a,b,c\n1,2,3")           # [["a","b","c"], ["1","2","3"]]
CSV.generate { |csv| csv << [1, 2] }  # "1,2\n"

headers: true 让每行变成 CSV::Row,可以按列名访问,极大提升可读性。处理 Excel 导出的数据时这特别有用。

6. 异常处理:begin/rescue/ensure

文件操作、网络请求、解析 JSON——这些都可能在运行时失败begin/rescue/ensure 是 Ruby 版的 try/catch/finally:

# 异常:begin/rescue/else/ensure(类似 try/catch/finally)
def read_config(path)
  begin
    content = File.read(path)        # 可能抛 Errno::ENOENT
    JSON.parse(content)              # 可能抛 JSON::ParserError
  rescue Errno::ENOENT => e
    puts "文件不存在: #{e.message}"
    {}                               # 返回空配置作为降级
  rescue JSON::ParserError => e
    puts "JSON 格式错误: #{e.message}"
    retry if (tries = (tries || 0) + 1) < 3   # 重试最多 3 次
    raise                            # 重试用完,重新抛出
  rescue => e                        # 兜底:捕获所有 StandardError
    puts "未知错误: #{e.class}: #{e.message}"
    raise                            # 别默默吞掉异常!
  ensure
    puts "无论是否异常都执行(关连接、记日志)"
  end
end

# 主动抛异常
raise "参数不能为空"                 # 抛 RuntimeError
raise ArgumentError, "缺少 name"     # 抛特定类型
raise CustomError.new("自定义")      # 抛自定义异常类

经验:不要裸 rescue 把异常吞掉(rescue => e; end 不记日志),这是 bug 温床。捕获后要么处理、要么记日志后重新抛出。

7. 自定义异常类

大型项目需要自己的异常类型,继承 StandardError(不要继承 Exception,后者会捕获 SystemExit 等不该捕的):

# 自定义异常类:继承 StandardError(不要继承 Exception)
class PaymentError < StandardError
  attr_reader :order_id

  def initialize(order_id, message = "支付失败")
    @order_id = order_id
    super(message)
  end
end

# 抛与捕获
begin
  raise PaymentError.new(12345, "余额不足")
rescue PaymentError => e
  puts "订单 #{e.order_id} 失败: #{e.message}"
  # 订单 12345 失败: 余额不足
end

# 异常层次(常见)
#   Exception
#     NoMemoryError, ScriptError, ...
#     StandardError           <- 你的异常应继承这里
#       ArgumentError, TypeError, RuntimeError
#       IOError, SystemCallError(含 Errno::*)
#       自定义异常

给自定义异常加属性(如 order_id),让 catch 方能拿到结构化上下文,比单纯靠 message 字符串好得多。

系列完结

恭喜!你完成了 Ruby 16 章系列。从环境安装、基础语法,一路学到块、面向对象、模块、Gems、文件 IO——你已经掌握了 Ruby 的全部核心。下一步建议:

Ruby 的设计哲学是"让程序员开心"。希望这个系列也让你写得开心。

← 上一篇 Ruby Gems 与 Bundler

← 返回 Ruby 教程目录

✈️💬