我们已经实现了很多点唱机的代码片段,但我们疏忽了一些问题。虽然我们已经了解过数组,散列表以及
procs
,但是还没有了解过 Ruby
的基础类型:数字,字符串,范围和正则表达式。让我们通过本章的内容学习这部分知识。
数字
Ruby
支持整型和浮点型数字。整型的长度不定(最大值由系统的可用存储决定)。在指定范围内创建的整型数是以二进制的形式保存(一般范围为 -230 到 230-1 或者 -262 到 262-1 之间),这个范围内的整型数都是 Fixnum
类的对象。超过这个长度范围的整型数就以 Bignum
类型存储(当前以一组变长的短整型数实现)。这个过程是透明的,Ruby 会自动管理类型的转换。
num = 8
7.times do
print num.type, " ", num, "\n"
num *= num
end
结果是
Fixnum 64
Fixnum 4096
Fixnum 16777216
Bignum 281474976710656
Bignum 79228162514264337593543950336
Bignum 6277101735386680763835789423207666416102355444464034512896
你可以使用一个开头符号书写整形数,开头符号起着标识作用(0 是八进制,0x
是十六进制,0b
是二进制),标识符号后跟一串数字。下划线在数字串中会被忽略。
123456 # Fixnum
123_456 # Fixnum (underscore ignored)
-543 # Negative Fixnum
123_456_789_123_345_789 # Bignum
0xaabb # Hexadecimal
0377 # Octal
-0b101_010 # Binary (negated)
可能你还接触过以 ASCII
码或者问号开头的转义序列表示的整型数。控制和元组合也可以用
?\C-x,?\M-x 和 \M-\C-x 生成。数值的控制版本与 value & 0x9f
相似。数值的元版本是 value | 0x80
。所以 ?\C-?
会生成「删除」的 ASCII 码——0177。
?a # character code
?\n # code for a newline (0x0a)
?\C-a # control a = ?A & 0x9f = 0x01
?\M-a # meta sets bit 7
?\M-\C-a # meta and control a
?\C-? # delete character
一个带有小数点或者以指数形式表示的数字都会被转换为 Float
对象,相当于自然体系中的 double
数据类型。你一定会在小数点后跟上数字,比如 1.e3
会尝试将 e3
调用
Fixnum
类中的方法。
所有的数字都是对象,并且表现各种各样的信息(会在 290,313,315,323 和
349 页列举)。因此和 C++ 不一样,需要编写为 aNumber.abs
获得绝对值,而不是写成 abs(aNumber)。
整型数也支持几种有用的迭代器。刚才我们已经见到过其中的一些用法,例如
7.times
。还有一些是 upto
和
downto
,分别表示在两个整型数之间向上或向下遍历,还有 step
也是一种使用方式,它的用法与 for
循环十分相似。
3.times { print "X " }
1.upto(5) { |i| print i, " " }
99.downto(95) { |i| print i, " " }
50.step(80, 5) { |i| print i, " " }
结果是
X X X 1 2 3 4 5 99 98 97 96 95 50 55 60 65 70 75 80
最后,需要提醒一下 Perl
语言用户。当使用表达式时,包含数字的字符串并不会被自动转换为数字。这会导致从文件中读取数字时通常是使用字节。下面的代码(可能)不会达到我们的期望。
DATA.each do |line|
vals = line.split # split line, storing tokens in val
print vals[0] + vals[1], " "
end
文件中内容如下
3 4
5 6
7 8
结果程序会输出 34 56 78
。为什么会这样?
因为读取文件的输入是以字符串而不是数字。所以加号是针对字符串的计算,于是我们就看到了如上所述的结果。如果要处理这个问题,可以通过
String#to_i
方法将字符串转换为整型数进行处理。
DATA.each do |line|
vals = line.split
print vals[0].to_i + vals[1].to_i, " "
end
结果是
7 11 15
字符串
Ruby 中的字符串就是单纯的 8
比特的字节序列。字符串通常保存可打印的字符,但这不是必要条件,因为字符串也可以保存二进制数据。字符串是
String
类的对象。
字符串通过用字符表达式表达,在引号之间的字符就是字符串。因为二进制数据在源码中较难表示,所以你可以在字符串中使用转义字符处理。每一个二进制数据在编译阶段都会被相应的二进制值替换。在不同引号的字符串中会有不同级别的替换执行等级。在单引号字符串中,两个连续反斜杠会被一个反斜杠替换,一个反斜杠加单引号会转换成一个单引号。
'escape using "\\"' »escape using "\"
'That\'s right' »That's right
双引号字符串支持一堆转义序列。最常用的应该是
\n
,可以用来换行。203 页的表 18.2
展示了完整列表。而且,你可以在字符串中使用 Ruby 表达式 #{ expr }
进行替换。如果表达式只是一个全局变量或者一个类变量,或者一个实例变量,这些情形都可以忽略花括号。
"Seconds/day: #{24*60*60}" »Seconds/day: 86400
"#{'Ho! '*3}Merry Christmas"Ho »Ho! Ho! Ho! Merry Christmas
"This is line #$." »This is line 3
还有更多的方式可以构造字符串,包括 %q,%Q 和 here document。
%q 和 %Q 分别表示单引号和双引号的字符串。
%q/general single-quoted string/ »general single-quoted string
%Q!general double-quoted Stringing! »general double-quoted string
%Q{Seconds/day: #{24*60*60}} »Seconds/day: 86400
字符 q
和 Q
后跟随的字符是一个分隔符。如果它是一个左花括号,左中括号,左小括号或者小于符号,一直到相应的闭符号内的代码都会被作为字符串读取。其他的情况就是直到相同的分隔符之间都会被认为是一个字符串。
接着,你可以通过 here document 创建字符串。
aString = <<END_OF_STRING
The body of the string
is the input lines up to
one ending with the same
text that followed the '<<'
END_OF_STRING
here document 由跟随在 <<
字符后的标记间的字符串组成,但不包含标记字符。一般情况下,第一行的起始就是标记符。如果你在
<<
后跟随减号,你就可以缩进标识符。
print <<-STRING1, <<-STRING2
Concat
STRING1
enate
STRING2
结果是
Concat
enate
使用字符串工作
String
应该是 Ruby 中最大的类了,总共有超过 75
个方法。我们无法在此节全部说到,不过在库中已经有完全的列表。现在我们要了解一下字符串的常用用法,也就是那些每天都会使用到的东西。
让我们回到点唱机的话题上。尽管它在设计时是打算连接网络的,但它也将一些常用歌曲拷贝到本地存储上。因为这样做可以在松鼠咬断我们的网线时点唱机还能继续工作。
由于一些历史原因,歌曲列表会一行一行地存储在文件中。每行都会包含歌曲文件名称,歌曲描述,作者以及歌曲标题,每一项内容都会被竖线分隔。文件的内容如下:
/jazz/j00132.mp3 | 3:45 | Fats Waller | Ain't Misbehavin'
/jazz/j00319.mp3 | 2:58 | Louis Armstrong | Wonderful World
/bgrass/bg0732.mp3| 4:09 | Strength in Numbers | Texas Red
: : : :
看到这些数据,我们会清晰地认识到,我们会使用 String
类的许多方法提取和格式化数据,之后再基于这些数据创建 Song
对象。基于最少步骤,我们需要按如下进行:
- 将每行分解为字段
- 将播放时间从
mm:ss
转换为秒 - 从作者名称中移除无用的空格
我们的第一个任务是将每行分解为字段,String#split
能够很好的完成这项工作。这个任务中,我们会通过正则表达式 /\s*\|\s*/
根据竖线分解数据为字段。但是因为从文件中读取的数据在结尾都会有换行符,所以在我们分解数据前要用 String#chomp
去除换行符。
songs = SongList.new
songFile.each do |line|
file, length, name, title = line.chomp.split(/\s*\|\s*/)
songs.append Song.new(title, name, length)
end
puts songs[1]
结果是
Song: Wonderful World--Louis Armstrong (2:58)
不幸的是,创建歌曲列表文件的人在输入作者名称时包含了许多多余的空格。这显示时会很丑,因此在我们使用这个值之前最好把多余的空格去除掉。有很多办法能够解决这个问题,不过最简便的是
String#squeeze
方法,它可以去除指定的重复字符。我们等下会使用 squeeze!
方法完成这个任务,它可以替换被修改的字符串。
songs = SongList.new
songFile.each do |line|
file, length, name, title = line.chomp.split(/\s*\|\s*/)
name.squeeze!(" ")
songs.append Song.new(title, name, length)
end
puts songs[1]
结果是:
Song: Wonderful World--Louis Armstrong (2:58)
最后还遗留一个小问题,播放时长在文件中在存储的是
2:58,但我们希望显示为秒数—— 178 秒。我们需要再次使用 split
方法,这次会根据冒号进行分隔。
mins, secs = length.split(/:/)
不过我们打算用另外一个方法。String#scan
和 split
方法是相似的,也是通过一个模式进行分割。但是与 split
不一样的是,scan
是获取与你填写的模式相匹配的内容。在这个任务中,我们想通过匹配一个或者多个数字为获取分钟和秒数的部分。一个或多个数字可以通过
/\d+/
进行匹配。
songs = SongList.new
songFile.each do |line|
file, length, name, title = line.chomp.split(/\s*\|\s*/)
name.squeeze!(" ")
mins, secs = length.scan(/\d+/)
songs.append Song.new(title, name, mins.to_i*60+secs.to_i)
end
puts songs[1]
结果是
Song: Wonderful World--Louis Armstrong (178)
点唱机还有一个关键的功能——搜索。通过歌曲标题或者作者名称中的一个单词,可以搜索到相关的歌曲列表。比如,通过
「fats」可以搜索到 Fats Domino,Fats Navarro 和 Fats Waller
相关歌曲。我们打算创建索引类来完成这个功能。通过存储一些字符串和对象,我们就可以索引到包含搜索单词的相关对象中。下面的例子我们会对使用到许多
String
方法进行说明。
class WordIndex
def initialize
@index = Hash.new(nil)
end
def index(anObject, *phrases)
phrases.each do |aPhrase|
aPhrase.scan /\w[-\w']+/ do |aWord| # extract each word
aWord.downcase!
@index[aWord] = [] if @index[aWord].nil?
@index[aWord].push(anObject)
end
end
end
def lookup(aWord)
@index[aWord.downcase]
end
end
String#scan
方法可以从字符串中提取与表达式匹配的元素。这个案例中 \w[-\w']+
可以匹配任意单词,这个单词后面可以跟连字符,其他的单词或者一个单引号。我们会在
56
页了解更多正则表达式的知识。为了让搜索功能大小写不敏感,我们查找单词和存储单词为键时都转换为小写。需要注意第一个 downcase!
是以感叹号结尾的。如同我们前面用过的 squeeze!
方法一样,这表示些方法会直接将调用者进行替换,也就是直接将字符串转换为小写。这个例子中有个小 bug,歌曲「Gone, Gone, Gone」会获取三次索引。可以修复这个问题吗?
我们要扩展 SongList
类,帮它对歌曲添加索引,并且增加一个方法可以通过单词搜索歌曲。
class SongList
def initialize
@songs = Array.new
@index = WordIndex.new
end
def append(aSong)
@songs.push(aSong)
@index.index(aSong, aSong.name, aSong.artist)
self
end
def lookup(aWord)
@index.lookup(aWord)
end
end
接下来让我们测试一下。
songs = SongList.new
songFile.each do |line|
file, length, name, title = line.chomp.split(/\s*\|\s*/)
name.squeeze!(" ")
mins, secs = length.scan(/\d+/)
songs.append Song.new(title, name, mins.to_i*60+secs.to_i)
end
puts songs.lookup("Fats")
puts songs.lookup("ain't")
puts songs.lookup("RED")
puts songs.lookup("WoRlD")
结果是:
Song: Ain't Misbehavin'--Fats Waller (225)
Song: Ain't Misbehavin'--Fats Waller (225)
Song: Texas Red--Strength in Numbers (249)
Song: Wonderful World--Louis Armstrong (178)
我们会在后面的 50 页中见识所有关于 String
的方法。不过我们还需要了解另一个基本类型「ranges(范围)」。
范围
范围在许多地方都会用到,1 月到 12 月,0 到
9,未完成到完工,50 行到 67 行等等。如果 Ruby
是为了帮助我们建造现实世界的模型的话,它就会自然而然地支持这些范围的使用方式。实际上,Ruby
在这个方面做得更好,它使用范围实现了三种特性,分别是序列,条件和间隔。
范围作为序列使用
使用范围表达一个序列应该是最先想到且自然而然的范围使用方式。序列都会有一个起点和一个终点,并且以指定的方式在序列中产生连续的值。在
Ruby
中序列通过「…」和「…」操作符创建。两个点创建的序列包含整个序列,而三个点不会包含最后的值。
1..10
'a'..'z'
0...anArray.length
和简易版的 Perl 语言不一样,Ruby
中的范围不会以列表的方式表现,例如序列 1..100000
会创建一个包含两个 Fixnum
对象的 Range
对象。如果你需要,也可以通过 to_a
方法将范围转换为列表。
(1..10).to_a »[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
('bar'..'10bat').to_a »["bar", "bas", "bat"]
Range
中实现了很多方法以方便对其进行遍历,同时也方便有各种方式检测其中的数据。
digits = 0..9
digits.include?(5) »true
digits.min »0
digits.max »9
digits.reject {|i| i < 5 } »[5, 6, 7, 8, 9]
digits.each do |digit|
dial(digit)
end
我们已经了解过数字和字符串的范围了。但是作为一门你期待的面向对象语言,Ruby
可以通过自定义对象的方式创建
range。不过自定义对象有个约束,自定义对象必须实现 succ
方法并返回序列中的下一个对象,而且也必须可以通过 <=>
完成对象间的比较操作。有时候 <=>
比较操作也叫宇宙飞船操作,它会返回 -1,0 或者
+1,分别对应第一个对象小于第二个对象,两个对象相等,第一个对象大于第二个对象。
我们定义一个类并用「#」号显示。我们可以用它来作为点唱机音量控制的基本显示符号。
class VU
include Comparable
attr :volume
def initialize(volume) # 0..9
@volume = volume
end
def inspect
'#' * @volume
end
# Support for ranges
def <=>(other)
self.volume <=> other.volume
end
def succ
raise(IndexError, "Volume too big") if @volume >= 9
VU.new(@volume.succ)
end
end
我们可以对创建 vu 对象序列进行测试。
medium = VU.new(4)..VU.new(7)
medium.to_a »[####, #####, ######, #######]
medium.include?(VU.new(3)) »false
范围作为条件使用
范围不仅可以作为序列表现,也可以用作条件表达式。例如,下面的代码块就会从输入中打印每一行,打印的内容需要以「start」为第一行,「end」为最后一行。
while gets
print if /start/../end/
end
在屏幕后面,range 保持对每个测试状态的记录。我们还会在 82
页在循环中使用的例子。
范围作为间隔使用
range
的最后一种使用方式是作为间隔测试以确定某些值是否出现在范围的间隔。需要使用
===
符号进行操作。
(1..10) === 5 »true
(1..10) === 15 »false
(1..10) === 3.14159 »true
('a'..'j') === 'c' »true
('a'..'j') === 'z' »false
在 81
页的例子中也展示了这种使用,那个例子是通过年份确定相应的 jazz 风格。
正则表达式
回到第 50
页,我们根据文件内容创建了相应的歌单,当时我们通过正则表达式将从文件中读取的内容分隔成相应的字段。我们解释过
line.split(/\s*\|\s*/)
会匹配竖线及周围的空格符。接下来我们会了解更多正则表达式的知识,这样我们就知道上面的表达式为何是我们解释的那样。
正则表达式是 Regexp
的对象。正则表达式即可以通过构造方法创建,也可以通过 /patern/
和
/%r\pattern\
创建。
a = Regexp.new('^\s*[a-z]') »/^\s*[a-z]/
b = /^\s*[a-z]/ »/^\s*[a-z]/
c = %r{^\s*[a-z]} »/^\s*[a-z]/
只要有拥有了正则表达式对象,你可以用 Regexp#match(aString)
方法,或者 =~
(正向匹配)和 !~
(反向匹配)匹配操作符与字符串进行匹配。匹配操作符在 String
和 Regexp
对象中都有定义。如果操作符的操作者是 String
对象,那么右侧就会转换为正则表达式。
a = "Fats Waller"
a =~ /a/ »1
a =~ /z/ »nil
a =~ "ll" »7
当匹配上相应的字符时,匹配操作会返回相应字符的位置。不过在设置 Ruby
整个变量加载方面存在一定副作用。「$ &」
获取的是字符串中被匹配的部分,「$ `」
获取匹配字符之前的部分,「$’」
获取匹配字符之后的部分。我们可以利用这个特性编写 showRE
方法,用以说明特别的模式匹配。
def showRE(a,re)
if a =~ re
"#{$`}<<#{$&}>>#{$'}"
else
"no match"
end
end
showRE('very interesting', /t/) »very in<<t>>eresting
showRE('Fats Waller', /ll/) »Fats Wa<<ll>>er
匹配结果也可以设置为全局线程变量 $~
和 $1
到 $9
。变量 $~
是 MatchData
对象(将在 336
页开始讲述),它会存储你可能希望了解的关于匹配的所有信息。$1
一类会存储匹配值的一部分。我们稍后再介绍。对于畏缩的人来说,当他们看到这些类似
Perl
语言变量的命名时总是希望调优。所以在这章的结尾有一些好消息。
模式
每个正则表达式都包含一个模式,模式可以用来将正则表达式与字符串进行匹配。
在模式中,除了 .,|,(,),[,{,+,\,^,$,*,?
外的字符都可以与自身匹配。
showRE('kangaroo', /angar/) »k<<angar>>oo
showRE('!@%&-_=+'oo, /%&/) »!@<<%&>>-_=+
如果你想要匹配以上列举的特殊字符只需要在字符前加反斜杠。接下来我们可以解释一下分隔歌曲数据的模式
/\s*\|\s*/
。「|」表示匹配竖线。没有反斜杠就表示或者(后面会讲到)。
showRE('yes | no', /\|/) »yes <<|>> no
showRE('yes (no)', /\(no\yes)/) »yes <<(no)>>
showRE('are you sure?', /e\?/) »are you sur<<e?>>
反斜杠后的字母数字都是特殊的匹配用法,我们稍后会介绍。而且,正则表达式也会包含
#{...}
替换表达式。
锚点
默认情况下,正则表达式会在字符串中查找第一个匹配的模式。如果在字符串「Mississippi」中匹配
/iss/
的话只会匹配第一个「iss」子串。但如果你想强行从头至尾地匹配字符串就需要怎么做呢?
^
和 $
分别可以匹配行的开头和结尾。它们常常用作模式匹配的锚点,例如
/^option/
只匹配出现在行开头的「option」字符。\A
可以匹配行开头,\z
和 \Z
可以匹配行结尾。(实际上 \Z
只匹配不是以 \n
结尾的行,被匹配的部分是换行之前的部分)。
showRE("this is\nthe time", /^the/) »this is\n<<the>> time
showRE("this is\nthe time"nthe, /is$/) »this <<is>>\nthe time
showRE("this is\nthe time", /\Athis/) »<<this>> is\nthe time
showRE("this is\nthe time", /\Athe/) »no match
类似地,\b
和 \B
分别匹配单词的边界和非单词的边界。单词字符包含字母,数字和下划线。
showRE("this is\nthe time", /\bis/) »this <<is>>\nthe time
showRE("this is\nthe time",nthe /\Bis/) »th<<is>> is\nthe time
字符类
字符类是指在中括号间的字符,比如
[characters]
,它可以匹配任何中括号间的单一字符。[aeiou]
会匹配元音字符,[,.:;!?]
会匹配标点符号等等。一些有特殊意义的字符也可以出现在中括号中,例如
.|()[{+^$*?
等。还有些简单的字符串替换也会出现,比如 \b
表示空格字符,\n
表示换行字符。而且也可以使用 59 页的表 5.1
中的缩写,还有 \s
可以匹配任意空白字符,不仅是空格。
showRE('It costs $12.', /[aeiou]/) »It c<<o>>sts $12.
showRE('It costs $12.', /[\s]/) »It<< >>costs $12.
在中括号中,如果使用 c1-c2
序列的话就表示匹配 c1 至 c2
间包含的所有字符。
如果你想要包含 ]
和 -
字符,它们必须出现在开头位置。
a = 'Gamma [Design Patterns-page 123]'
showRE(a, /[]]/) »Gamma [Design Patterns-page 123<<]>>
showRE(a, /[B-F]/) »Gamma [<<D>>esign Patterns-page 123]
showRE(a, /[-]/) »Gamma [Design Patterns<<->>page 123]
showRE(a, /[0-9]/) »Gamma [Design Patterns-page <<1>>23]
在中括号中的开头部分加 ^
符号表示反向的意思,例如 [^a-z]
会匹配不是小写字母的字符。
一些字符类经常使用 Ruby 提供的缩写。缩写都在 59 页的表格 5.1
中列出,它们在中括号中和模式体中都可以使用。
showRE('It costs $12.', /\s/)»It<< >>costs $12.
showRE('It costs $12.', /\d/)»It costs $<<1>>2.
Sequence | As [ … ] | Meaning |
---|---|---|
\d | [0-9] | Digit character |
\D | [^0-9] | Nondigit |
\s | [\s\t\r\n\f] | Whitespace character |
\S | [^\s\t\r\n\f] | Nonwhitespace character |
\w | [A-Za-z0-9_] | Word character |
\W | [^A-Za-z0-9_] | Nonword character |
最后,句点「.」通常使用在中括号外,它表示除了换行字符外的任意字符。
a = 'It costs $12.'
showRE(a, /c.s/) »It <<cos>>ts $12.
showRE(a,12 /./) »<<I>>t costs $12.
showRE(a, /\./) »It costs $12<<.>>
重复
之前我们已经说过我们是通过 /\s*\|\s*/
完成歌单数据的分割的,这个模式匹配的是竖线及周围任意多个的空格。我们已经知道
\s
是匹配一个空格字符,所以星号就表示任意多的数量级。事实上,星号是一个数量修饰符,它可以让你可以匹配一个模式出现的多个地方。
如果 r
出现在正则表达式的模式前,像下面这样:
r * matches zero or more occurrences of r.
r + matches one or more occurrences of r.
r ? matches zero or one occurrence of r.
r {m,n} matchches at least ``m'' and at most ``n'' occurrences of r.
r {m,} matches at least ``m'' occurrences of r.
这些重复结构优先级比较高,它们只会绑定符号之前的正则表达式。/ab+/
匹配「a」后面跟着一个或多个「b」,并还是「ab」的序列。你还需要注意「*」结构,/a*/
会匹配任意字符串,因为每个字符串都包含零个或多个 「a」。
这些模式称为贪婪模式,因为默认情况下它们会在字符串中匹配尽量多的字符。你也可以选择它们的行为,通过添加问号作为后缀的方式让它们匹配尽量少。
a = "The moon is made of cheese"
showRE(a, /\w+/) »<<The>> moon is made of cheese
showRE(a, /\s.*\s/) »The<< moon is made of >>cheese
showRE(a, /\cheeses.*?\s/) »The<< moon >>is made of cheese
showRE(a, /[aeiou]{2,99}/) »The m<<oo>>n is made of cheese
showRE(a, /mo?o/) »The <<moo>>n is made of cheese
交替
我们都知道竖线有特别的意义,因此我们分割歌单数据时的竖线使用反斜杠进行转义。因为未转义的竖线「|」表示在它之前或之后的正则表达式都会进行匹配。
a = "red ball blue sky"
showRE(a, /d|e/) »r<<e>>d ball blue sky
showRE(a, /al|lu/) »red b<<al>>l blue sky
showRE(a, /red ball|angry sky/) »<<red ball>> blue sky
这里如果粗心的话就会犯错,因为竖线的优先级非常低。所以最后一个例子中应该是匹配「red
ball」或者「angry sky」,而不是「red ball sky」或「red angry
sky」。如果要匹配「red ball sky」或「red angry
sky」,你需要通过分组的方式重新定义优先级。
分组
你通过在正则表达式内使用小括号进行分组。在分组内的每个东西都被视作单独的正则表达式。
showRE('banana', /an*/) »b<<an>>ana
showRE('banana', /(an)*/) »<<>>banana
showRE('banana', /(an)+/) »b<<anan>>an
a = 'red ball blue sky'
showRE(a, /blue|red/) »<<red>> ball blue sky
showRE(a, /(blue|red) \w+/) »<<red ball>> blue sky
showRE(a, /(red|blue) \w+/) »<<red ball>> blue sky
showRE(a, /red|blue \w+/) »<<red>> ball blue sky
showRE(a, /red (ball|angry) sky/) »no match
a = 'the red angry sky'
showRE(a, /red (ball|angry) sky/) »the <<red angry sky>>
小括号也用于模式匹配的结果收集。Ruby
会将括号间的结果分别存储进来。你可以在后面的 Ruby
代码中使用任意部分。在模式中,\1
表示匹配的第一组,\2
表示匹配的第二组等等。在模式外部,特殊变量 $1
,$2
等等也是同样的功能。
"12:50am" =~ /(\d\d):(\d\d)(..)/ »0
"Hour is #$1, minute #$2" »"Hour is 12, minute 50"
"12:50am" =~ /((\d\d):(\d\d))(..)/ »0
"Time is #$1" »"Time is 12:50"
"Hour is #$2, minute #$3" »"Hour is 12, minute 50"
"AM/PM is #$4" »"AM/PM is am"
通过这个功能还可以在当前的匹配中查询之前的匹配。
# match duplicated letter
showRE('He said "Hello"', /(\w)\1/)»He said "He<<ll>>o"
# match duplicated substrings
showRE('Mississippi', /(\w+)\1/)»M<<ississ>>ippi
你也可以将之前的结果作为引用分别匹配。
showRE('He said "Hello"', /(["']).*?\1/) »He said <<"Hello">>
showRE("He said 'Hello'", /(["']).*?\1/) »He said <<'Hello'>>
基于模式的替换
一般情况下能够在字符串中匹配模式已经足够使用。如果有朋友挑战你,需要你查找按顺序包含
a,b,c,d 和 e 的单词,这时你可以通过 /a.*b.*c.*d.*.e/
模式查找到「absconded」和「ambuscade」。这非常有价值。
但是,有时你会需要替换根据模式匹配的字符。让我们回到歌单文件。有人将歌曲作者的名字都以小写进行了记录。不过在点唱机屏幕上显示时还是大小写混合方式更好。我们可以将每个单词的首字母替换为大写吗?
String#sub
和 String#gsub
可以根据第一个参数匹配,然后将匹配的目标替换为第二个参数。String#sub
只执行一次替换,而 String#gsub
会对每个匹配结果的地方都进行替换。最后会返回一个包含了替换的 String
新拷贝。变化版本 String#sub!
和 String#gsub!
方法会改变原有的字符串。
a = "the quick brown fox"
a.sub(/[aeiou]/, '*') »"th* quick brown fox"
a.gsub(/[aeiou]/, '*') »"th* q**ck br*wn f*x"
a.sub(/\s\S+/, '') »"the brown fox"
a.gsub(/\s\S+/, '') »"the"
第二个参数可以是 String
也可以是代码块。如果使用代码块,代码块的值就会被替换至字符串中。
a = "the quick brown fox"
a.sub(/^./) { $&.upcase } »"The quick brown fox"
a.gsub(/[aeiou]/) { $&.upcase } »"thE qUIck brOwn fOx"
这个例子仿佛让我们找到了转换歌曲作者名字的方案。通过 \b\w
匹配每个单词的首字母。再结合 gsub
方法我们就可以修改作者名字。
def mixedCase(aName)
aName.gsub(/\b\w/) { $&.upcase }
end
mixedCase("fats waller") »"Fats Waller"
mixedCase("louis armstrong") »"Louis Armstrong"
mixedCase("strength in numbers") »"Strength In Numbers"
反斜杠序列在替换中的使用
我们已经觉察到 \1
,\2
等等模式中的序列是表示第 n
个匹配的分组。类似的序列也可以在 sub
和 gsub
的第二个参数中使用。
"fred:smith".sub(/(\w+):(\w+)/, '\2, \1') »"smith, fred"
"nercpyitno".gsub(/(.)(.)/, '\2\1') »"encryption"
还有一些反斜杠序列也可以作为替换字符串使用,例如「\&」(最后一个匹配值),「\+」(最后一组匹配值),「\ `](匹配值前的字符串),「\’」(匹配值后的字符串)和「\\」(反斜杠字符)。在替换的内容需要包含反斜杠时会让人很迷惑。不过可以像下面一样写:
str.gsub(/\\/, '\\\\')
这段代码清晰地表述了要替换 str
中的所有单个反斜杠替换为两个反斜杠。程序员会在替换文本中使用一倍的反斜杠,因为他们知道语法解析器会将「\\」转换。但是当替换动作发生时,正则引擎会将「\\」转换为「\」,替换的结果也就变成了将每个反斜杠替换为一个反斜杠的效果。所以你需要将代码写为 gsub(/\\/, '\\\\\\\\')
。
str = 'a\b\c' » "a\b\c"
str.gsub(/\\/, '\\\\\\\\') » "a\\b\\c"
不过也可以使用 \&
将已经匹配的字符串作为替换值,你可以编写如下代码:
str = 'a\b\c' » "a\b\c"
str.gsub(/\\/, '\&\&') » "a\\b\\c"
如果你在 gsub
方法使用代码块,字符串的替换只会进行一次分析多次使用。
str = 'a\b\c' » "a\b\c"
str.gsub(/\\/) {'\\\\'} » "a\\b\\c"
最后,CGI 库中的由 Wakou Aoyama
编写的代码是一个结合正则表达式和代码块使用非常好的例子。代码中包含了
HTML 的转义字符串,在正则表达式中还使用了 n
修饰符,这样可以关闭对宽字符的处理。并且它也阐明了 Ruby 中的 case
表达式,关于这个内容我们会在 81 页讨论。
def unescapeHTML(string)
str = string.dup
str.gsub!(/&(.*?);/n) {
match = $1.dup
case match
when /\Aamp\z/ni then '&'
when /\Aquot\z/ni then '"'
when /\Agt\z/ni then '>'
when /\Alt\z/ni then '<'
when /\A#(\d+)\z/n then Integer($1).chr
when /\A#x([0-9a-f]+)\z/ni then $1.hex.chr
end
}
str
end
puts unescapeHTML("1<2 && 4>3")
puts unescapeHTML(""A" = A = A")
结果是:
1<2 && 4>3
"A" = A = A
面向对象的正则表达式
我们必须承认正则的包装变量确认十分好用,但它们并不面向对象,只是显得神秘。我们还可以说
Ruby 中的每个事物都是对象吗?到底哪里出了问题呢?
完成没有问题。当 Matz 设计 Ruby
时,他在系统中创建了完全面向对象的正则表达式。他在变量前面使用 $
符号包装,这让 Perl
程序员觉得似曾相识。这些对象和类一直都在,它们只是潜藏着。所以让我们花点时间把它们挖掘出来。
我们已经了解过 Regexp
类了,只不过我们是通过字符创建的。
re = /cat/
re.type »Regexp
Regexp#match
方法可以将正则表达式与字符串匹配。如果失败了方法会返回
nil
。如果成功方法会返回 MatchData
类的实例,MatchData
的文档写在 336 页。并且 MatchData
会提供所有关于匹配的可用信息。不过所有有用的信息你也可以通过 $
开头的变量方便地获取。
re = /(\d+):(\d+)/ # match a time hh:mm
md = re.match("Time: 12:34am")
md.type »MatchData
md[0] # == $& »"12:34"
md[1] # == $1 »"12"
md[2] # == $2 »"34"
md.pre_match # == $` »"Time: "
md.post_match # == $' »"am'
因为匹配的数据是存储在它自己的对象中,所以你可以同时保存两个或更多模式的匹配,特别是在你不能使用
$ 变量时。在下面的例子中,我们要将两字符串与同一个 Regexp
对象匹配。每次匹配都会返回唯一的一个 MatchData
对象,并且我们可以通过检测的方式验证两个子模式字段。
re = /(\d+):(\d+)/ # match a time hh:mm
md1 = re.match("Time: 12:34am")
md2 = re.match("Time: 10:30pm")
md1[1, 2] »["12", "34"]
md2[1, 2] »["10", "30"]
因此这种情况我们要怎么使用 $ 变量?在每次模式匹配之后,Ruby
都会将结果(nil
或 MatchData
对象)的引用存储在本地线程变量中(通过 $~
访问)。所有的正则表达式变量都是由这些对象派生的。尽管我们不会真地思考下面代码的用途,不过它向我们解释了其他的
MatchData
引用和 $ 变量是如何使用 $~
的值的。
re = /(\d+):(\d+)/
md1 = re.match("Time: 12:34am")
md2 = re.match("Time: 10:30pm")
[ $1, $2 ] # last successful match »["10", "30"]
$~ = md1
[ $1, $2 ] # previous successful match »["12", "34"]
讲了那么多,我们还得承认。Andy 和 Dave 通常是使用 $ 变量而不是
MatchData
对象。在每天的工作中,我们还是以方便为主。所以有时我们在书里谈论的知识并没有那么实用主义。
本文翻译自《Programming Ruby》,主要目的是自己学习使用,文中翻译不到位之处烦请指正,如需转载请注明出处
本章原文为
Standard Types