正则表达式
开篇故事
正则表达式是文本处理的瑞士军刀。它能在一行代码中提取邮箱、验证手机、解析 URL、替换格式——这些事用字符串方法要写十几行。Ruby 的正则表达式语法强大、表达力强,是值得投资的技能。
本章适合谁
如果你需要处理文本模式匹配(验证表单、解析日志、提取数据),本章教你 Ruby 正则表达式的完整使用方法。
你会学到什么
- 创建正则表达式的三种方式
- 修饰符
/i、/m、/x - 匹配操作
=~、match - 捕获组(位置组和命名组)
- 贪婪 vs 惰性
- 字符串方法配合正则(
gsub/scan/split) - 实用示例
前置要求
第一个例子
# 运行: hello basic regex
text = "我的电话是 138-1234-5678"
# =~ 返回匹配的起始索引
index = text =~ /\d{3}-\d{4}-\d{4}/
puts "=~ 返回索引: #{index}" # 5
# match 返回 MatchData 对象
match = text.match(/(\d{3})-(\d{4})-(\d{4})/)
puts "完整匹配: #{match[0]}" # 138-1234-5678
puts "第1组: #{match[1]}" # 138
puts "第2组: #{match[2]}" # 1234
puts "第3组: #{match[3]}" # 5678
为什么用正则:如果用字符串方法,你需要写循环、判断、截取。一行正则搞定。
创建正则表达式
# 字面量斜杠(最常用)
pattern_slash = /hello/
# %r{} — 适合包含斜杠的正则(如 URL)
pattern_r = %r{https?://[\w.]+}
# Regexp.new — 适合动态构建正则
dynamic = Regexp.new("\d{3}-\d{4}")
修饰符
# /i — 忽略大小写
insensitive = /hello/i
puts "忽略大小写: #{insensitive.match?("Hello")}" # true
# /m — 多行模式(. 也匹配换行符)
multiline = /a.*b/m
puts "多行模式: #{multiline.match?("a\nb")}" # true
# /x — 忽略空白,允许注释(适合复杂正则)
verbose = /
\d + # 一位或多位数字
- # 连字符
\d + # 一位或多位数字
/x
puts "verbose 模式: #{verbose.match?("123-456")}" # true
匹配操作
text = "我的电话是 138-1234-5678"
# =~ 返回索引或 nil
index = text =~ /\d{3}-\d{4}-\d{4}/
puts "=~ 返回索引: #{index}" # 5
# !~ 不匹配返回 true/false
puts "不匹配: #{(text !~ /xyz/)}" # true
# match 返回 MatchData
match_data = text.match(/(\d{3})-(\d{4})-(\d{4})/)
puts "完整匹配: #{match_data[0]}"
puts "第1组: #{match_data[1]}"
# match? 只判断是否匹配(更快)
puts "包含数字?: #{text.match?(/\d+/)}" # true
常用元字符
sample = "abc123 XYZ def45"
puts "原始字符串: '#{sample}'"
# 字符类
puts "\d (数字): #{sample.scan(/\d/).inspect}" # ["1", "2", "3", "4", "5"]
puts "\w (单词字符): #{sample.scan(/\w/).inspect}" # 包括字母数字下划线
puts "\s (空白符): #{sample.scan(/\s/).inspect}" # [" ", " "]
puts "\D (非数字): #{sample.scan(/\D/).inspect}" # 非数字
puts "\W (非单词): #{sample.scan(/\W/).inspect}" # 非单词字符
量词
puts "+ (1次或多次): #{sample.scan(/\d+/).inspect}" # ["123", "45"]
puts "* (0次或多次): #{sample.scan(/\d*/).inspect}" # ["123", "45", "", "", "", ""]
puts "? (0次或1次): #{sample.scan(/\w\w?\d?/).inspect}" # 允许0次
puts "{2,3} (2到3次): #{sample.scan(/\w{2,3}/).inspect}" # 范围
puts "{3} (恰好3次): #{sample.scan(/\d{3}/).inspect}" # 精确次数
捕获组
位置捕获组 ( )
date = "2025-12-31"
date_match = date.match(/(\d{4})-(\d{2})-(\d{2})/)
puts "年: #{date_match[1]}" # 2025
puts "月: #{date_match[2]}" # 12
puts "日: #{date_match[3]}" # 31
命名捕获组 (?<name>pattern)
named_match = date.match(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/)
puts "year: #{named_match[:year]}" # 2025
puts "month: #{named_match[:month]}" # 12
puts "day: #{named_match[:day]}" # 31
非捕获组 (?:pattern)
# 分组但不捕获
non_capturing = "2025-12-31".match(/(?:\d{4})-(\d{2})-(\d{2})/)
puts "捕获组数量: #{non_capturing.captures.length}" # 2(不是3)
贪婪 vs 惰性
greedy = "<div>hello</div>".match(/<.*>/) # 匹配: `<div>hello</div>`
lazy = "<div>hello</div>".match(/<.*?>/) # 匹配: `<div>`
puts "贪婪 .*: #{greedy[0]}"
puts "惰性 .*?: #{lazy[0]}"
字符串方法配合正则
sentence = "Hello World Hello Ruby Hello World"
# sub — 单次替换
puts "sub: #{sentence.sub(/Hello/, "Hi")}"
# → "Hi World Hello Ruby Hello World"
# gsub — 全局替换
puts "gsub: #{sentence.gsub(/Hello/, "Hi")}"
# → "Hi World Hi Ruby Hi World"
# split — 用正则分割
csv = "apple,banana;cherry:dragon"
puts "split: #{csv.split(/[,;:]/).inspect}"
# → ["apple", "banana", "cherry", "dragon"]
# scan — 查找所有匹配
numbers = "a1b22c333d4444e5555".scan(/\d+/)
puts "scan: #{numbers.inspect}"
# → ["1", "22", "333", "4444"]
# grep — 数组匹配
words = %w{apple banana cherry date elderberry}
puts "grep: #{words.grep(/e$/).inspect}"
# → ["apple", "cherry", "date", "elderberry"]
特殊变量
匹配后 Ruby 设置了一些全局变量:
"hello 123 world" =~ /(\d+)/
puts "$1 (最后匹配的第1组): #{$1}" # "123"
puts "$& (最后完整匹配): #{$&}" # "123"
puts "$~ (MatchData): #{$~.class}" # MatchData
puts "$` (匹配前): #{$`}" # "hello "
puts "$' (匹配后): #{$'}" # " world"
Regexp.escape — 转义特殊字符
raw = "Price: $19.99 (50% off)"
escaped = Regexp.escape(raw)
puts "转义: #{escaped}"
# Price: \$19\.99 \(50% off\)
实用示例
邮箱验证
email_pattern = /\A[\w.+-]+@[\w-]+\.[\w.]+\z/
emails = [
"user@example.com",
"invalid@",
"good+tag@domain.co.uk"
]
emails.each do |email|
valid = email =~ email_pattern ? "✓" : "✗"
puts " #{valid} #{email}"
end
解析 URL
url_pattern = %r{(?<protocol>https?)//(?<host>[\w.:-]+)(?<path>/[\w/.-]*)?(?:\?(?<query>[\w=&-]+))?(?:#(?<fragment>\w+))?}
url = "http://example.com:8080/path/to/page?query=1#anchor"
m = url.match(url_pattern)
puts "协议: #{m[:protocol]}" # http
puts "主机: #{m[:host]}" # example.com:8080
puts "路径: #{m[:path]}" # /path/to/page
puts "查询: #{m[:query]}" # query=1
puts "锚点: #{m[:fragment]}" # anchor
常见错误
错误 1:忘记锚点
# 只匹配包含,不匹配完整
"hello world".match?(/hello/) # true
"hello world".match?(/\Ahello\z/) # false(多了 world)
修复:用 \A 和 \z 限制完整匹配。
错误 2:贪婪匹配
"<div>hello</div>".match(/<.*>/)[0]
# → "<div>hello</div>"(贪婪匹配整个)
"<div>hello</div>".match(/<.*?>/) # → "<div>"(惰性,只匹配第一个标签)
错误 3:用正则解析 HTML
# ❌ 正则不适合解析 HTML
# HTML 结构复杂,嵌套、属性顺序、实体字符等
# 应使用 Nokogiri 等 HTML 解析器
动手练习
练习 1:提取中文数字
# 从文本提取所有连续的中文数字
# "今天买了三斤苹果,花了二十元" → ["三", "二十"]
参考答案
text = "今天买了三斤苹果,花了二十元"
text.scan(/[\u4e00-\u9fff]+/)
# 需要更复杂的逻辑处理中文数字
练习 2:驼峰转蛇形
# "helloWorld" → "hello_world"
# "XMLHttpRequest" → "xml_http_request"
参考答案
def camel_to_snake(str)
str.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '')
end
故障排除 (FAQ)
Q: 为什么 \d 在 Ruby 里和 Python 不一样?
A: Ruby 的 \d 默认只匹配 ASCII 数字(0-9)。如需匹配 Unicode 数字,用 \p{Nd}。
Q: 匹配特殊字符如 / 的 / 需要转义吗?
A: 需要。/ 是正则的边界字符,所以 / 需要写 \/。或者用 %r{} 代替:%r{https?://}。
Q: /i /m /x 可以一起用吗?
A: 可以:/pattern/imx。
小结
核心要点:
- 字面量
/pattern/最常用 =~返回索引 /match返回 MatchData /match?返回布尔- 捕获组
(...)和命名组(?<name>pattern) - 量词区分贪婪 vs 惰性
gsub/scan/split配合正则处理字符串\A锚定开头/结尾
继续学习
- 上一章: 符号(Symbol)
- 下一章: 文件管理
- 返回: 基础入门
运行 hello basic regex 查看完整示例代码.