Ruby 正则表达式
正则表达式(Regular Expression,简称regex或regexp)是处理文本的强大工具。在Ruby中,正则表达式被广泛应用于字符串验证、文本搜索、数据提取和格式化等场景。Ruby对正则表达式提供了原生支持,使其成为处理文本数据的理想选择。本章将详细介绍Ruby中正则表达式的语法、方法和实际应用。
🎯 正则表达式基础
什么是正则表达式
正则表达式是一种特殊的文本字符串,用于描述搜索模式。它由普通字符和特殊字符(元字符)组成,可以用来匹配、查找、替换文本中的特定模式。
ruby
# 基本正则表达式匹配
# 检查字符串是否包含数字
text = "我的电话号码是13812345678"
if text.match?(/\d+/)
puts "文本包含数字"
end
# 查找匹配的内容
match = text.match(/\d+/)
if match
puts "找到数字: #{match[0]}" # 13812345678
end
# 使用字符串方法
puts "hello".match?(/h/) # true
puts "hello".match?(/H/) # false
puts "hello".match?(/H/i) # true (忽略大小写)正则表达式字面量
ruby
# 使用斜杠定义正则表达式
regex1 = /hello/
regex2 = /\d+/ # 匹配一个或多个数字
regex3 = /[a-z]+/ # 匹配一个或多个小写字母
regex4 = /hello/i # 忽略大小写
# 使用%r定义正则表达式(避免转义斜杠)
url_pattern = %r{https?://[\w\-\.]+\.[a-zA-Z]{2,}(/\S*)?}
email_pattern = %r{[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+}
# 使用Regexp.new创建
pattern = Regexp.new('\d+', Regexp::IGNORECASE)
# 正则表达式选项
# i - 忽略大小写
# m - 多行模式
# x - 扩展模式(忽略空白和注释)
regex_with_options = /hello world/imx🔤 基本元字符
字符匹配
ruby
# 普通字符
/hello/.match("hello world") # 匹配
# 点号 - 匹配任意字符(除换行符)
/a.c/.match("abc") # 匹配
/a.c/.match("a c") # 匹配
/a.c/.match("axc") # 匹配
/a.c/.match("ac") # 不匹配
# 转义字符
/\./.match("a.b") # 匹配点号
/\*/.match("a*b") # 匹配星号
/\?/.match("a?b") # 匹配问号
# 字符类
/[abc]/.match("a") # 匹配
/[abc]/.match("b") # 匹配
/[abc]/.match("d") # 不匹配
# 范围
/[a-z]/.match("m") # 匹配小写字母
/[A-Z]/.match("M") # 匹配大写字母
/[0-9]/.match("5") # 匹配数字
/[a-zA-Z0-9]/.match("K") # 匹配字母或数字
# 预定义字符类
/\d/.match("5") # 匹配数字 [0-9]
/\D/.match("a") # 匹配非数字 [^0-9]
/\w/.match("a") # 匹配单词字符 [a-zA-Z0-9_]
/\W/.match("@") # 匹配非单词字符 [^a-zA-Z0-9_]
/\s/.match(" ") # 匹配空白字符 [ \t\r\n\f]
/\S/.match("a") # 匹配非空白字符量词
ruby
# 基本量词
/a*/.match("") # 匹配零个或多个a
/a*/.match("a") # 匹配
/a*/.match("aaa") # 匹配
/a+/.match("a") # 匹配一个或多个a
/a+/.match("aaa") # 匹配
/a+/.match("") # 不匹配
/a?/.match("") # 匹配零个或一个a
/a?/.match("a") # 匹配
/a?/.match("aa") # 只匹配第一个a
# 精确量词
/a{3}/.match("aaa") # 匹配恰好3个a
/a{2,4}/.match("aa") # 匹配2到4个a
/a{2,}/.match("aaaa") # 匹配2个或更多a
# 贪婪与非贪婪匹配
/<.+>/.match("<b>bold</b>") # 贪婪匹配: <b>bold</b>
/<.+?>/.match("<b>bold</b>") # 非贪婪匹配: <b>
# 实际应用示例
text = "手机号码: 13812345678, 座机: 010-12345678"
# 提取手机号
phone_match = text.match(/1[3-9]\d{9}/)
puts phone_match[0] if phone_match # 13812345678
# 提取座机号
landline_match = text.match(/0\d{2,3}-?\d{7,8}/)
puts landline_match[0] if landline_match # 010-12345678🏗️ 高级正则表达式
分组和捕获
ruby
# 基本分组
/(ab)+/.match("ababab") # 匹配ab的重复
// 使用分组捕获
match = /(\d{4})-(\d{2})-(\d{2})/.match("2023-12-25")
if match
puts "年: #{match[1]}" # 2023
puts "月: #{match[2]}" # 12
puts "日: #{match[3]}" # 25
puts "完整匹配: #{match[0]}" # 2023-12-25
end
// 命名分组
match = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/.match("2023-12-25")
if match
puts "年: #{match[:year]}" # 2023
puts "月: #{match[:month]}" # 12
puts "日: #{match[:day]}" # 25
end
// 非捕获分组
/(?:abc)+/.match("abcabc") # 匹配但不捕获
// 分组引用
/(.)\1/.match("aa") # 匹配重复字符
/(..)\1/.match("abab") # 匹配重复的两个字符
// 实际应用:解析URL
url = "https://www.example.com:8080/path/to/page?param1=value1¶m2=value2#section"
url_pattern = %r{(?<protocol>https?)://(?<host>[\w\-\.]+)(?::(?<port>\d+))?(?<path>/[^?#]*)?(?:\?(?<query>[^#]*))?(?:#(?<fragment>.*))?}
match = url_pattern.match(url)
if match
puts "协议: #{match[:protocol]}" # https
puts "主机: #{match[:host]}" # www.example.com
puts "端口: #{match[:port]}" # 8080
puts "路径: #{match[:path]}" # /path/to/page
puts "查询: #{match[:query]}" # param1=value1¶m2=value2
puts "片段: #{match[:fragment]}" # section
end锚点和边界
ruby
// 行首和行尾锚点
/^hello/.match("hello world") // 匹配行首
/world$/.match("hello world") // 匹配行尾
// 单词边界
/\bword\b/.match("word") // 匹配完整单词
/\bword\b/.match("wording") // 不匹配
/\bword/.match("wording") // 匹配单词开头
/word\b/.match("reword") // 匹配单词结尾
// 字符串边界
/\Ahello/.match("hello world") // 匹配字符串开头
/world\z/.match("hello world") // 匹配字符串结尾
// 实际应用:验证输入
def valid_username?(username)
// 用户名只能包含字母、数字、下划线,长度3-16位
username.match?(/\A[a-zA-Z0-9_]{3,16}\z/)
end
def valid_email?(email)
// 简单的邮箱验证
email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
end
puts valid_username?("user_123") // true
puts valid_username?("us") // false
puts valid_email?("user@example.com") // true
puts valid_email?("invalid.email") // false选择和条件
ruby
// 或操作符
/(cat|dog)/.match("I have a cat") // 匹配cat
/(cat|dog)/.match("I have a dog") // 匹配dog
// 复杂选择
/(https?|ftp):\/\//.match("https://example.com") // 匹配协议
/(https?|ftp):\/\//.match("ftp://example.com") // 匹配协议
// 条件匹配
// 如果前面匹配了某个模式,则后面必须匹配另一个模式
/(?<=\d{3})\d{4}/.match("13812345678") // 匹配1234(前面有3位数字)
// 负向先行断言
/(?<!\d{3})\d{4}/.match("12345678") // 匹配1234(前面不是3位数字)
// 实际应用:提取多种格式的日期
text = "今天是2023-12-25,明天是2023/12/26,后天是Dec 27, 2023"
date_patterns = [
/(\d{4})-(\d{2})-(\d{2})/, // 2023-12-25
/(\d{4})\/(\d{2})\/(\d{2})/, // 2023/12/26
/(\w{3})\s+(\d{1,2}),\s+(\d{4})/ // Dec 27, 2023
]
text.scan(Regexp.union(date_patterns)) do |match|
puts "找到日期: #{match.compact}"
end🔧 正则表达式方法
字符串中的正则表达式方法
ruby
text = "联系方式:电话13812345678,邮箱user@example.com"
// match方法 - 返回匹配对象
match = text.match(/1[3-9]\d{9}/)
if match
puts "电话: #{match[0]}"
end
// match?方法 - 返回布尔值
puts text.match?(/1[3-9]\d{9}/) // true
// scan方法 - 查找所有匹配
text = "价格:100元,折扣:80元,运费:10元"
prices = text.scan(/\d+/)
puts prices.inspect // ["100", "80", "10"]
// gsub方法 - 全局替换
masked_text = text.gsub(/1[3-9]\d{9}/, "****")
puts masked_text
// sub方法 - 替换第一个匹配
first_replaced = text.sub(/\d+/, "***")
puts first_replaced
// split方法 - 使用正则表达式分割
data = "apple,banana;orange:grape"
fruits = data.split(/[,;:]/)
puts fruits.inspect // ["apple", "banana", "orange", "grape"]
// 实际应用:数据清洗
def clean_phone_numbers(text)
// 提取并格式化电话号码
text.gsub(/(\d{3})(\d{4})(\d{4})/) do |match|
"#{$1}-#{$2}-#{$3}"
end
end
text = "联系电话:13812345678,备用电话:13987654321"
puts clean_phone_numbers(text)
// 联系电话:138-1234-5678,备用电话:139-8765-4321Regexp类方法
ruby
// 编译正则表达式
pattern = Regexp.new('\d+', Regexp::IGNORECASE)
puts pattern.match?("123abc") // true
// 转义特殊字符
special_chars = "hello*world?.+"
escaped = Regexp.escape(special_chars)
puts escaped // hello\*world\?\.\+
// 联合多个正则表达式
patterns = [/cat/, /dog/, /bird/]
union_pattern = Regexp.union(patterns)
puts "I have a cat".match?(union_pattern) // true
// 获取正则表达式选项
regex = /hello/i
puts regex.options // 1 (对应Regexp::IGNORECASE)
// 检查正则表达式是否相等
regex1 = /hello/
regex2 = /hello/
puts regex1 == regex2 // true🎯 实用正则表达式示例
数据验证
ruby
class DataValidator
// 邮箱验证
EMAIL_PATTERN = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
// 手机号验证
PHONE_PATTERN = /\A1[3-9]\d{9}\z/
// 身份证验证
ID_CARD_PATTERN = /\A\d{17}[\dXx]\z/
// 邮编验证
POSTAL_CODE_PATTERN = /\A\d{6}\z/
// URL验证
URL_PATTERN = /\Ahttps?:\/\/[\w\-\.]+\.[a-zA-Z]{2,}(/\S*)?\z/
// 密码强度验证
PASSWORD_PATTERN = /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*(),.?":{}|<>]).{8,}\z/
def self.valid_email?(email)
email.match?(EMAIL_PATTERN)
end
def self.valid_phone?(phone)
phone.match?(PHONE_PATTERN)
end
def self.valid_id_card?(id_card)
id_card.match?(ID_CARD_PATTERN)
end
def self.valid_postal_code?(code)
code.match?(POSTAL_CODE_PATTERN)
end
def self.valid_url?(url)
url.match?(URL_PATTERN)
end
def self.strong_password?(password)
password.match?(PASSWORD_PATTERN)
end
end
// 使用数据验证器
puts DataValidator.valid_email?("user@example.com") // true
puts DataValidator.valid_phone?("13812345678") // true
puts DataValidator.valid_id_card?("110101199001011234") // true
puts DataValidator.strong_password?("Password123!") // true文本处理和提取
ruby
class TextProcessor
// 提取所有邮箱地址
def self.extract_emails(text)
email_pattern = /[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+/i
text.scan(email_pattern)
end
// 提取所有电话号码
def self.extract_phones(text)
phone_pattern = /1[3-9]\d{9}|\d{3,4}-?\d{7,8}/
text.scan(phone_pattern)
end
// 提取所有URL
def self.extract_urls(text)
url_pattern = %r{https?://[\w\-\.]+\.[a-zA-Z]{2,}(/\S*)?}
text.scan(url_pattern)
end
// 提取HTML标签内容
def self.extract_html_content(html)
html.gsub(/<[^>]*>/, "")
end
// 清理多余空白
def self.clean_whitespace(text)
text.gsub(/\s+/, " ").strip
end
// 格式化电话号码
def self.format_phone(phone)
phone.gsub(/(\d{3})(\d{4})(\d{4})/, '\1-\2-\3')
end
// 提取引号内的内容
def self.extract_quoted_text(text)
text.scan(/["']([^"']*)["']/).flatten
end
end
// 使用文本处理器
text = <<~TEXT
联系我们:
邮箱:contact@example.com, support@company.org
电话:13812345678, 010-12345678
网站:https://www.example.com
"这是一段引用文本"
'这是另一段引用文本'
TEXT
puts "邮箱地址:"
TextProcessor.extract_emails(text).each { |email| puts " #{email}" }
puts "电话号码:"
TextProcessor.extract_phones(text).each { |phone| puts " #{phone}" }
puts "URL:"
TextProcessor.extract_urls(text).each { |url| puts " #{url}" }
puts "格式化电话: #{TextProcessor.format_phone('13812345678')}"
puts "清理空白: '#{TextProcessor.clean_whitespace(" 多个 空白 字符 ")}'"日志分析
ruby
class LogAnalyzer
// Apache访问日志模式
APACHE_LOG_PATTERN = /
^(\S+)\s+ // IP地址
(\S+)\s+ // 标识符
(\S+)\s+ // 用户ID
\[([^\]]+)\]\s+ // 时间戳
"(\S+)\s+(\S+)\s+(\S+)"\s+ // 请求行
(\d{3})\s+ // 状态码
(\d+|-)\s+ // 响应大小
"(.*?)"\s+ // 引用页
"(.*?)" // 用户代理
/x
// 错误日志模式
ERROR_LOG_PATTERN = /
^\[([^\]]+)\]\s+ // 时间戳
\[([^\]]+)\]\s+ // 日志级别
\[([^\]]+)\]\s+ // 进程ID
(.*) // 错误消息
/x
def self.parse_apache_log(line)
match = line.match(APACHE_LOG_PATTERN)
return nil unless match
{
ip: match[1],
timestamp: match[4],
method: match[5],
path: match[6],
protocol: match[7],
status: match[8].to_i,
size: match[9] == '-' ? 0 : match[9].to_i,
referrer: match[10],
user_agent: match[11]
}
end
def self.parse_error_log(line)
match = line.match(ERROR_LOG_PATTERN)
return nil unless match
{
timestamp: match[1],
level: match[2],
pid: match[3],
message: match[4]
}
end
// 统计访问次数
def self.count_visits(log_lines)
visits = Hash.new(0)
log_lines.each do |line|
record = parse_apache_log(line)
next unless record
visits[record[:ip]] += 1
end
visits
end
// 统计状态码
def self.count_status_codes(log_lines)
status_codes = Hash.new(0)
log_lines.each do |line|
record = parse_apache_log(line)
next unless record
status_codes[record[:status]] += 1
end
status_codes
end
end
// 使用日志分析器
apache_log_line = '127.0.0.1 - - [25/Dec/2023:14:30:45 +0800] "GET /index.html HTTP/1.1" 200 1234 "http://example.com" "Mozilla/5.0"'
error_log_line = '[2023-12-25 14:30:45] [ERROR] [12345] Database connection failed'
apache_record = LogAnalyzer.parse_apache_log(apache_log_line)
error_record = LogAnalyzer.parse_error_log(error_log_line)
puts "Apache日志记录:"
puts apache_record.inspect if apache_record
puts "错误日志记录:"
puts error_record.inspect if error_record📊 性能优化
正则表达式性能考虑
ruby
// 避免回溯灾难
// 不好的模式
bad_pattern = /a+b+a+b+a+b+a+b+a+b+a+b+a+b+a+b+a+b+/
// 好的模式 - 使用原子组或占有量词
good_pattern = /a++b+a++b+a++b+a++b+a++b+a++b+a++b+a++b+/
// 预编译常用的正则表达式
class RegexCache
@@cache = {}
def self.get(pattern)
@@cache[pattern] ||= Regexp.new(pattern)
end
end
// 使用缓存的正则表达式
def find_emails_cached(text)
email_pattern = RegexCache.get('[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+')
text.scan(email_pattern)
end
// 避免不必要的捕获
// 不需要捕获时使用非捕获分组
non_capturing = /(?:abc)+/
capturing = /(abc)+/
// 使用锚点提高匹配效率
// 好的做法
/^start.*end$/.match("start something end")
// 避免在大文本中使用贪婪匹配
// 使用非贪婪匹配或更精确的模式
text = "a" * 1000 + "b"
// 慢
slow_pattern = /a+b/
// 快
fast_pattern = /a++b/
// 批量处理优化
def process_large_text(text)
// 分块处理大文本
chunk_size = 10000
results = []
0.step(text.length, chunk_size) do |i|
chunk = text[i, chunk_size]
results.concat(chunk.scan(/\b\w{4,}\b/))
end
results
end正则表达式调试
ruby
// 调试正则表达式
def debug_regex(pattern, text)
regex = Regexp.new(pattern)
match = text.match(regex)
if match
puts "匹配成功!"
puts "完整匹配: #{match[0]}"
puts "匹配位置: #{match.begin(0)}-#{match.end(0)}"
match.captures.each_with_index do |capture, index|
puts "分组#{index + 1}: #{capture} (位置: #{match.begin(index + 1)}-#{match.end(index + 1)})"
end
else
puts "匹配失败"
end
end
// 使用调试函数
debug_regex(/(\d{4})-(\d{2})-(\d{2})/, "今天是2023-12-25")
// 性能测试
def benchmark_regex(pattern, text, iterations = 1000)
regex = Regexp.new(pattern)
start_time = Time.now
iterations.times do
text.match?(regex)
end
end_time = Time.now
puts "执行#{iterations}次耗时: #{end_time - start_time}秒"
end
// benchmark_regex('\d+', 'abc123def456ghi789')🛡️ 正则表达式最佳实践
1. 编写可维护的正则表达式
ruby
// 使用扩展模式提高可读性
complex_pattern = /
^ // 行首
(?<protocol>https?) // 协议组
:\/\/ // 分隔符
(?<host> // 主机组
[\w\-\.]+ // 域名字符
\. // 点号
[a-zA-Z]{2,} // 顶级域名
)
(?::(?<port>\d+))? // 可选端口组
(?<path>\/[^?#]*)? // 可选路径组
(?:\?(?<query>[^#]*))? // 可选查询组
(?:\#(?<fragment>.*))? // 可选片段组
$ // 行尾
/x
// 分解复杂正则表达式
class EmailValidator
LOCAL_PART = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+'
DOMAIN_PART = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'
DOMAIN = "#{DOMAIN_PART}(?:\\.#{DOMAIN_PART})*"
TLD = '[a-zA-Z]{2,}'
EMAIL_PATTERN = /\A#{LOCAL_PART}@#{DOMAIN}\.#{TLD}\z/
def self.valid?(email)
email.match?(EMAIL_PATTERN)
end
end
// 使用命名常量
PHONE_PATTERN = /\A1[3-9]\d{9}\z/
ID_CARD_PATTERN = /\A\d{17}[\dXx]\z/
def valid_phone?(phone)
phone.match?(PHONE_PATTERN)
end
def valid_id_card?(id_card)
id_card.match?(ID_CARD_PATTERN)
end2. 安全考虑
ruby
// 防止正则表达式拒绝服务攻击(ReDoS)
class SafeRegex
// 设置匹配超时(Ruby 2.6+)
def self.safe_match?(pattern, string, timeout: 1)
regex = Regexp.new(pattern)
// 在新线程中执行匹配
thread = Thread.new { string.match?(regex) }
result = thread.join(timeout)
if result
thread.value
else
thread.kill
raise "正则表达式匹配超时"
end
end
// 验证用户输入的正则表达式
def self.validate_user_regex(pattern)
begin
Regexp.new(pattern)
true
rescue RegexpError
false
end
end
// 清理用户输入
def self.sanitize_input(input)
// 移除潜在危险的字符
input.gsub(/[<>'"&]/, '')
end
end
// 使用安全正则表达式
begin
result = SafeRegex.safe_match?('\d+', '12345')
puts "匹配结果: #{result}"
rescue => e
puts "错误: #{e.message}"
end3. 测试正则表达式
ruby
// 正则表达式测试框架
class RegexTester
def initialize(pattern)
@pattern = pattern
@regex = Regexp.new(pattern)
end
def test_positive(*strings)
strings.each do |string|
unless string.match?(@regex)
puts "失败: '#{string}' 应该匹配模式 '#{@pattern}'"
return false
end
end
true
end
def test_negative(*strings)
strings.each do |string|
if string.match?(@regex)
puts "失败: '#{string}' 不应该匹配模式 '#{@pattern}'"
return false
end
end
true
end
def test_capture(string, expected_captures)
match = string.match(@regex)
if match
actual_captures = match.captures
if actual_captures == expected_captures
true
else
puts "捕获失败: 期望 #{expected_captures}, 实际 #{actual_captures}"
false
end
else
puts "匹配失败: '#{string}' 没有匹配"
false
end
end
end
// 使用测试框架
email_tester = RegexTester.new('[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+')
// 测试正例
email_tester.test_positive(
'user@example.com',
'test.email@domain.org',
'user123@test-domain.co.uk'
)
// 测试反例
email_tester.test_negative(
'invalid.email',
'@example.com',
'user@',
'user@.com'
)
// 测试捕获组
date_tester = RegexTester.new('(\d{4})-(\d{2})-(\d{2})')
date_tester.test_capture('2023-12-25', ['2023', '12', '25'])4. 实际应用场景
ruby
// 配置文件解析器
class ConfigParser
SECTION_PATTERN = /^\[(.+)\]$/
KEY_VALUE_PATTERN = /^([^=]+)=(.*)$/
COMMENT_PATTERN = /^\s*[#;]/
def self.parse(config_text)
config = {}
current_section = 'default'
config_text.each_line do |line|
line = line.strip
next if line.empty? || line.match?(COMMENT_PATTERN)
if section_match = line.match(SECTION_PATTERN)
current_section = section_match[1]
config[current_section] = {}
elsif kv_match = line.match(KEY_VALUE_PATTERN)
key = kv_match[1].strip
value = kv_match[2].strip
config[current_section] ||= {}
config[current_section][key] = value
end
end
config
end
end
// 使用配置解析器
config_text = <<~CONFIG
# 应用配置
[database]
host = localhost
port = 5432
username = admin
[logging]
level = info
file = app.log
CONFIG
config = ConfigParser.parse(config_text)
puts config.inspect
// CSV解析器
class CSVParser
CSV_LINE_PATTERN = /(?<=^|,)(?:"([^"]*)"|([^",]*))(?=,|$)/
def self.parse_line(line)
matches = line.scan(CSV_LINE_PATTERN)
matches.map { |match| match[0] || match[1] || '' }
end
def self.parse(csv_text)
csv_text.each_line.map { |line| parse_line(line.chomp) }
end
end
// 使用CSV解析器
csv_text = <<~CSV
"姓名","年龄","城市"
"张三","25","北京"
"李四","30","上海"
CSV
data = CSVParser.parse(csv_text)
data.each { |row| puts row.inspect }📚 下一步学习
掌握了Ruby正则表达式后,建议继续学习:
- Ruby 数据库访问 - 学习数据库操作
- Ruby 网络编程 - 了解Socket编程
- Ruby JSON - 处理JSON数据
- Ruby Web服务 - 学习Web服务开发
继续您的Ruby学习之旅吧!