Ruby 实战项目
学了 16 篇基础,是时候把它们串起来做点真东西了。这一章给三个完整、可运行的项目——待办 CLI、网页爬虫、Sinatra API,每个都给出完整代码、运行结果和要点讲解。三个项目对应 Ruby 最常见的三个方向:命令行工具(自动化)、网络采集(抓数据)、Web 服务(做后端 API)。建议你把代码复制到本地亲手跑一遍。
实战 1:待办 CLI(class + YAML + OptionParser)
需求:做一个待办管理 CLI,支持 add / list / done / delete,数据用 YAML 持久化。
涉及知识:Struct、class、YAML 序列化、OptionParser(标准库)、case/when、异常 rescue。
# todo.rb — 待办管理 CLI(支持 add / list / done / delete)
# 用法:
# ruby todo.rb add 买牛奶
# ruby todo.rb list
# ruby todo.rb done 1
# ruby todo.rb delete 2
# 知识点:类、YAML 持久化、OptionParser、Block、异常处理
require "yaml"
require "optparse"
require "time"
# 单条待办:用 Struct 一行定义数据类(类似 attr_accessor 的精简版)
Todo = Struct.new(:id, :title, :done, :created_at) do
def to_s
mark = done ? "[x]" : "[ ]"
"#{mark} #{id}. #{title} (#{created_at})"
end
end
# 待办列表:封装增删查改与持久化
class TodoList
FILE = "todos.yml"
def initialize
@items = load
end
def add(title)
id = @items.empty? ? 1 : @items.map(&:id).max + 1
t = Todo.new(id, title, false, Time.now.strftime("%Y-%m-%d %H:%M"))
@items << t
save
puts "已添加: #{t}"
end
def list(all: false)
items = all ? @items : @items.reject(&:done)
if items.empty?
puts "(空空如也)"
else
items.each { |t| puts t }
end
end
def done(id)
t = @items.find { |x| x.id == id }
if t
t.done = true
save
puts "已完成: #{t.title}"
else
puts "未找到 id=#{id}"
end
end
def delete(id)
before = @items.size
@items.reject! { |x| x.id == id }
if @items.size < before
save
puts "已删除 id=#{id}"
else
puts "未找到 id=#{id}"
end
end
private
def load
return [] unless File.exist?(FILE)
YAML.load_file(FILE) || [] # YAML 反序列化,直接还原成 Todo 对象数组
rescue => e
warn "读取失败: #{e.message}"
[]
end
def save
File.write(FILE, YAML.dump(@items)) # 序列化成 YAML 写文件
end
end
# === 命令行入口 ===
list = TodoList.new
options = { all: false }
OptionParser.new do |opts|
opts.banner = "用法: ruby todo.rb [命令] [参数]"
opts.on("-a", "--all", "显示全部(含已完成)") { options[:all] = true }
opts.on("-h", "--help", "显示帮助") { puts opts; exit }
end.parse!
command = ARGV.shift
case command
when "add" then list.add(ARGV.join(" "))
when "list" then list.list(all: options[:all])
when "done" then list.done(ARGV.shift.to_i)
when "delete" then list.delete(ARGV.shift.to_i)
else
warn "未知命令: #{command}(可用:add / list / done / delete)"
end
# === 运行示例 ===
# $ ruby todo.rb add 买牛奶
# 已添加: [ ] 1. 买牛奶 (2024-06-01 10:30)
# $ ruby todo.rb add 写季度报告
# 已添加: [ ] 2. 写季度报告 (2024-06-01 10:31)
# $ ruby todo.rb list
# [ ] 1. 买牛奶 (2024-06-01 10:30)
# [ ] 2. 写季度报告 (2024-06-01 10:31)
# $ ruby todo.rb done 1
# 已完成: 买牛奶
# $ ruby todo.rb list --all
# [x] 1. 买牛奶 (2024-06-01 10:30)
# [ ] 2. 写季度报告 (2024-06-01 10:31)要点讲解:
Struct:Ruby 一行定义数据类的方式,自动生成构造器 + 取值器,比手写class+attr_accessor更短,适合纯数据对象。- YAML 持久化:
YAML.dump把对象序列化成可读的文本,YAML.load_file反序列化直接还原对象——比 JSON 更适合调试(人能直接读)。 OptionParser:标准库的命令行解析器,自动生成-h帮助;比手写ARGV判断优雅得多。case/when:Ruby 的when支持范围、类型、正则等多种匹配,这里是简单字符串分发子命令。rescue => e:Ruby 的异常捕获,类名 + 消息一起打印,比begin/rescue/end简洁——方法体直接 rescue 即可。
实战 2:网页爬虫(net/http + Nokogiri)
需求:抓取 quotes.toscrape.com 首页全部名言和作者,保存到 CSV。先装依赖:gem install nokogiri。
涉及知识:Net::HTTP(标准库)、URI、Nokogiri 解析、CSS 选择器、CSV、超时与异常。
# spider.rb — 网页爬虫:抓取 quotes.toscrape.com 的名言
# 依赖:gem install nokogiri
# 知识点:net/http(标准库)、Nokogiri(HTML 解析)、CSS 选择器、异常处理
require "net/http"
require "uri"
require "nokogiri"
require "csv"
require "fiber"
URL = "https://quotes.toscrape.com/"
# 封装 HTTP 请求:超时、User-Agent、异常
def fetch(url)
uri = URI(url)
req = Net::HTTP::Get.new(uri)
req["User-Agent"] = "Mozilla/5.0 (compatible; MyBot/1.0)"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if uri.scheme == "https"
http.open_time = 10
http.read_time = 10
http.request(req) # 返回 Net::HTTPResponse
rescue => e
warn "[错误] #{e.class}: #{e.message}"
nil
end
# 用 CSS 选择器解析,返回哈希数组
def parse(html)
doc = Nokogiri::HTML(html)
doc.css(".quote").map do |q|
{
text: q.at_css(".text").text.strip,
author: q.at_css(".author").text.strip,
}
end
end
# 写 CSV
def save(items, path = "quotes.csv")
CSV.open(path, "wb") do |csv|
csv << %w[text author]
items.each { |it| csv << [it[:text], it[:author]] }
end
puts "[完成] 已保存 #{items.size} 条到 #{path}"
end
puts "[开始] 爬取 #{URL}"
resp = fetch(URL)
if resp && resp.code == "200"
items = parse(resp.body)
puts "[预览] 共 #{items.size} 条,前 3 条:"
items.first(3).each { |it| puts " - #{it[:author]}: #{it[:text][0, 40]}" }
save(items)
end
# === 输出 ===
# [开始] 爬取 https://quotes.toscrape.com/
# [预览] 共 10 条,前 3 条:
# - Albert Einstein: The world as we have created it is a proc
# - J.K. Rowling: It is our choices, Harry, that show what we
# - Albert Einstein: There are only two ways to live your life
# [完成] 已保存 10 条到 quotes.csv
# === 不装 Nokogiri 也能用正则(应急方案,不推荐长期用)===
# HTML 结构复杂,正则容易在嵌套标签上踩坑,生产环境还是用 Nokogiri
def parse_with_regex(html)
html.scan(/<span class="text" itemprop="text">(.+?)<\/span>/m).flatten.map(&:strip)
end要点讲解:
Net::HTTP:Ruby 标准库,无需装 gem;use_ssl = true处理 https,这是新手常忘的点。- Nokogiri:Ruby 生态最主流的 HTML/XML 解析器(类似 Python 的 BeautifulSoup);
doc.css(".quote")用 CSS 选择器批量定位。 at_cssvscss:前者返回第一个匹配(单个),后者返回全部(集合)——按需选择。- 异常包裹:网络请求不可控(超时、断网、404),整段
rescue优雅降级而不是让程序崩。 - 礼貌爬取:每次请求留间隔、带
User-Agent、尊重robots.txt——爬虫的基本礼仪。 - 正则方案:应急时可用正则,但 HTML 嵌套复杂正则易踩坑,生产环境务必用 Nokogiri。
实战 3:Sinatra 简易 API(gem + route)
需求:用 Sinatra 写一个待办 REST API,支持 GET / POST / PUT / DELETE。启动后能用 curl 测试。先装依赖:gem install sinatra。
涉及知识:Sinatra DSL、HTTP 方法路由、JSON 返回、halt 中断、参数 params、状态码。
# api.rb — 用 Sinatra 写一个待办 REST API
# 依赖:gem install sinatra
# gem install thin # 推荐搭配 thin 服务器,默认 WEBrick 也行
# 启动:ruby api.rb
# 默认监听 http://localhost:4567
# 知识点:Sinatra 路由、JSON 返回、HTTP 方法、参数解析
#
# 测试(开另一个终端):
# curl http://localhost:4567/todos
# curl -X POST -d '{"title":"买牛奶"}' -H "Content-Type: application/json" http://localhost:4567/todos
# curl -X PUT -d '{"done":true}' -H "Content-Type: application/json" http://localhost:4567/todos/1
# curl -X DELETE http://localhost:4567/todos/1
require "sinatra"
require "json"
# 内存存储(重启清空);真实项目换 PostgreSQL + Sequel
$todos = []
$next_id = 1
before "/todos*" do
content_type :json # 所有 /todos* 路由都返回 JSON
end
# 列出全部 — GET /todos
get "/todos" do
$todos.to_json
end
# 查一条 — GET /todos/:id
get "/todos/:id" do
t = $todos.find { |x| x[:id] == params[:id].to_i }
halt 404, { error: "未找到" }.to_json unless t
t.to_json
end
# 新增 — POST /todos,请求体是 JSON
post "/todos" do
body = JSON.parse(request.body.read) rescue {}
halt 400, { error: "缺少 title" }.to_json unless body["title"]
todo = { id: $next_id += 1, title: body["title"], done: false }
$todos << todo
status 201 # 201 Created
todo.to_json
end
# 改一条 — PUT /todos/:id
put "/todos/:id" do
t = $todos.find { |x| x[:id] == params[:id].to_i }
halt 404, { error: "未找到" }.to_json unless t
body = JSON.parse(request.body.read) rescue {}
t[:title] = body["title"] if body["title"]
t[:done] = body["done"] if body.key?("done")
t.to_json
end
# 删一条 — DELETE /todos/:id
delete "/todos/:id" do
before_size = $todos.size
$todos.reject! { |x| x[:id] == params[:id].to_i }
halt 404, { error: "未找到" }.to_json if $todos.size == before_size
status 204 # 204 No Content
end
# === 同样的 API 用 Rails 写要 5 个文件,用 Sinatra 一个文件就够 ===
# 这正是 Ruby 的魅力:DSL + 约定 + 极少样板,适合快速原型/内部工具
# 启动后用 curl 测试:
# $ curl http://localhost:4567/todos
# []
# $ curl -X POST -d '{"title":"买牛奶"}' -H "Content-Type: application/json" http://localhost:4567/todos
# {"id":2,"title":"买牛奶","done":false}
# $ curl http://localhost:4567/todos
# [{"id":2,"title":"买牛奶","done":false}]
# $ curl -X PUT -d '{"done":true}' -H "Content-Type: application/json" http://localhost:4567/todos/2
# {"id":2,"title":"买牛奶","done":true}要点讲解:
- Sinatra DSL:
get "/todos"、post "/todos"等就是 Ruby 方法,内部把块注册到路由表——这是 Ruby 拿手好戏"用 DSL 写框架"。 - 路由参数:
:id这种冒号占位符自动进params[:id],Sinatra 内部用正则匹配 URL。 halt:立即中断请求并返回,类似 Rails 的render && return,写 API 错误处理的高频技巧。content_type :json:用before过滤器统一设置响应头,避免每个路由重复写。- 状态码:
status 201(Created)、204(No Content)、404——遵循 HTTP 语义,客户端体验更好。 - Sinatra vs Rails:Sinatra 一个文件搞定,适合小型 API / 工具 / 原型;Rails 是全家桶,适合大型应用。两者都基于 Rack 中间件。
小结
三个项目分别对应 Ruby 的三大主力场景:CLI 工具(自动化日常)、网络采集(抓数据)、Web 服务(做后端)。它们都用到了前面学的基础——类、块、文件 I/O、异常——只是组合起来解决真实问题。建议你至少把其中一个改造成自己的版本(给 CLI 加优先级、给爬虫加分页、给 API 加用户认证),做过一遍比读十遍更扎实。
恭喜!到这里 Ruby 系列就真正完结了。接下来可以挑一个方向深入——Ruby on Rails(全栈 Web 框架)、Hanami(现代化 Ruby 框架)、Jekyll(静态博客)、Hotwire(现代 Web 交互)、给开源项目贡献(Rails、Sinatra 都是 Ruby 写的)——Ruby 的世界以"写得开心"为核心,边做边玩进步最快。
← 上一篇 Ruby 文件与 IO
返回 Ruby 教程目录