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.join 或 Pathname(更高级)。
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 类型 => e:捕获特定类型的异常(类型在前,变量在后,用
=>分隔)。 - retry:重新执行整个 begin 块(用于重试网络请求、外部服务)。
- ensure:无论是否异常都执行(关连接、释放资源、记日志)。
- raise:重新抛出当前异常(在 rescue 块里裸 raise)。
经验:不要裸 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 的全部核心。下一步建议:
- 用 Rails 写一个完整 Web 应用,把所学付诸实战。
- 读 Why's (Poignant) Guide to Ruby 感受 Ruby 文化。
- 看 Eloquent Ruby 学习社区惯用法。
- 参与开源——Ruby 社区友善、PR 友好。
Ruby 的设计哲学是"让程序员开心"。希望这个系列也让你写得开心。
← 上一篇 Ruby Gems 与 Bundler
← 返回 Ruby 教程目录