标准类型

优质
小牛编辑
131浏览
2023-12-01
到现在我们已经实现了我们那点唱机的一部分代码,我们看到了数组,哈希,方法,但我们还没有涉及到Ruby中其他的数据类型:数字,字符串,范围(ranges),正则表达式。下面我们就要花些时间来看看这些类型。

数字型

Ruby 支持整型和浮点型两种数字类型。整型可以是任意长度(最大值由你机器的内存大小决定)。在一定范围内(通常是-230 to 230-1 or -262 to 262-1)在内部由二进制方式表示,内部类为Fixnum。大小超过这个范围的整数由Bignum表示,如果Fixnum计算之后结果超出范围,自动转换为Bignum。Ruby在两者之间自动转换,对用户来说是透明的。
num = 8
7.times do
  print num.type, " ", num, "\n"
  num *= num
end
produces:
Fixnum 8
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字符或者一个转意字符的数字值通过在它前面加一个问号。Control和Meta键的组合也可以用?\C-x, ?\M-x 和 ?\M-\C-x表达。字符value的加Control键的版本和"value & 0x9f"的值是一样的;字符value的加Meta键的版本和"value & 0x80"的值是一样的。最后,序列 ?\C-? 产生一个ASCII码的删除,0177。
?a                        # 字符的数字值
?\n                       # 换行符的值 (0x0a)
?\C-a                     # control a = ?A & 0x9f = 0x01
?\M-a                     # meta sets bit 7
?\M-\C-a                  # meta 和 control a
?\C-?                     # 删除字符
一个带小数点或者带指数的数字字符串会转换成一个Float对象,对应于本机操作系统构架的double数据类型。你必须在小数点后面加一个数字,因为像 1.e3会认为是调用了Fixnum类的e3这个方法。 所有数字都是对象,会响应一些消息(在290, 313, 315, 323和349页会完整的描述)。所以不像(比如说) C++,你会发现求一个数字的绝对值是这样写的 aNumber.abs,而不是abs(aNumber)。

整数也支持一些有用的迭代器(iterators)。我们已经看到过一个了--- 7.times在47页的代码例子中。还有其他的比如 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    #分割一行,把值存在vals里面
  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

字符串 Strings

Ruby的字符串是简单的8位字节(8-bit bytes)序列。它们通常保存可打印字符序列,但是这不是必须的;一个字符串也可以保存二进制数据。字符串是String类的对象。 字符串通常用字符常量建立---包括在分隔符里面的字符序列。因为二进制数据很难在程序代码里面表达,你可以在一个字符串常量里面使用各种转义字符。每个转义字符都会在程序编译的时候转换成相应的二进制数值。分隔符的种类表明了取代作用的程度。用单引号刮住的字符串里,两个连续的反斜杠会被一个反斜杠取代,一个反斜杠后面跟一个单引号变成一个单引号。'escape using "\\"' }}escape using "\"'That\'s right' }}That's right 用双引号刮住的字符串支持更多的转义字符。最常见的转义字符可能是"\n"了,代表一个换行符。第203页的表18.2列出了完整的转义字符。另外,你可以用 #{ expr } 来把任何的Ruby表达式的值插入到字符串中。如果那个表达式是全局变量,类变量或者实例变量,你可以省略大括号。"Seconds/day: #{24*60*60}" }}Seconds/day: 86400"#{'Ho! '*3}Merry Christmas" }}Ho! Ho! Ho! Merry Christmas"This is line #$." }}This is line 3 还有三种方法来构建字符串常量:%q, %Q和“here documents.”%q 和 %Q用来界定单引号和双引号的范围。%q/general single-quoted string/ }}general single-quoted string%Q!general double-quoted string! }}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
         :                  :           :                   :
观察一下数据,很明显在我们建立一些基于这个文件的Song对象之前,我们需要用String类的一些方法来展开和清理这些字段。至少,我们需要:
  • 把一行分成一个字段,
  • 把MM:SS格式的时间转换成秒, 然后
  • 把演唱者名字中额外的空格去掉.
我们的第一个任务是要把每行分成各个字段,String#split 方法最适合最这个。在这里,我们给 split 传递一个正则表达式,/\s*|\s*/,它会用小竖线和空格来分隔一行文本,把文本分成各个字段。还有,因为因为用文件里读取的这行文本后面跟了一个换行符,在我们用split函数之前,我们可以使用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)
不幸的是,那个创建原始文件的人在敲演唱者名字的时候是按列敲的,有些名字里面可能包含额外的空格。这样那些名字在我们极好的,高科技的,24小时运行的显示板上显示会很难看。我们必须去掉那些额外的空格才能继续我们的工作。有很多方法来做这个工作,但是最简单的可能是用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)。

 

(范围)Ranges

范围无处不在:从一月到十二月,0到9,半熟到完全煮熟,从第50行到第67行等等。如果ruby想要帮助我们很好的根据现实世界来建模,那么它也应该支持这些范围。实际上正是如此,ruby支持的ranges有三种用途:序列,条件,和间隔(sequences, conditions, and intervals)。

作为序列

ruby的range最常用的用处是表示一个顺序的序列,序列有一个开始点,和一个结束点,和产生序列中下一个值的方法。ruby中,定义学列使用".."和"..."操作符。".."创建的序列包括两边的边界值,而"..."创建的序列将不包括最大的那个边界值。
1..10
'a'..'z'
0...anArray.length
ruby不像其他一些地早期perl那样,把序列保存在一个内部列表中,比如,1..100000在ruby中只是一个Range对象,包括两个指向Fixnum对象的引用。如果需要,你可以把一个Range用to_a转换成一个数组。(1..10).to_a }}[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]('bar'..'bat').to_a }}["bar", "bas", "bat"] Ranges实现了一些可以让你对其进行迭代,测试是否包含某个值的方法。digits = 0..9digits.include?(5) }}truedigits.min }}0digits.max }}9digits.reject {|i| i < 5 } }}[5, 6, 7, 8, 9]digits.each do |digit|dial(digit)end

到现在我们的ranges表示的都是数字和字符串型,作为一个面向对象的语言,ranges也可以用于我们自己创建的对象,但是这个对象必须实现一个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的range来测试一下:

medium = VU.new(4)..VU.new(7)medium.to_a }}[####, #####, ######, #######]medium.include?(VU.new(3)) }}false

条件范围( Ranges as Conditions )

除了表示一系列连续值之外,range还能作为条件表达式。比如,下面代码将接收标准输入,打印出来那些以start开头和以end结尾的代码行。

while gets
  print if /start/../end/
end

Ranges表示间隔

rang最后一个用处是测试一些值是否在这个间隔之内,这要用到操作符===(1..10) === 5 }}true(1..10) === 15 }}false(1..10) === 3.14159 }}true('a'..'j') === 'c' }}true('a'..'j') === 'z' }}false

正则表达式

回到第50页当我们从一个文件里创建歌曲列表的时候,我们用了一个正则表达式去匹配文件里的字段。我们声明了正则表达式 line.split(/\s*|\s*)来匹配一个小竖线被(不是必须的)空格环绕的情况。让我们来仔细研究一下正则表达式来证明为什么我们的声明是正确的。

正则表达式用来匹配字符串的模式。Ruby提供了对模式匹配和替换内建的支持,使我们使用时很方便简练。在这部分,我们会介绍大部分主要的正则表达式特征。有些细节我们不会涉及:你可以参考第205页取得更多信息。

正则表达式是类型Regexp的对象。它们可以用显式的构造函数建立或者直接用 /pattern/ 和 %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) 或者用匹配操作符 =~(确定匹配)和 !~(否定匹配)。字符串和Regexp对象都可以使用匹配操作符。如果匹配操作符的两个操作数都是字符串的话,右边那个会转化成正则表达式。a = "Fats Waller"a =~ /a/ }}1a =~ /z/ }}nila =~ "ll" }}7 匹配操作符返回模式匹配成功的字符位置。 它们还有一个设置所有Ruby变量的额外作用。$&接受模式匹配成功的那部分字符,$`(键盘1左边那个键)接受模式匹配成功前面那一部分字符,$'接受模式匹配成功后面那部分字符。我们可以用这个来编写一个函数,showRE,它阐明了一个特殊的模式匹配例子:def showRE(a,re)if a =~ re"#{$`}<<#{$&}>>#{$'}"else"no match"endendshowRE('very interesting', /t/) }}very in<<t>>erestingshowRE('Fats Waller', /ll/) }}Fats Wa<<ll>>er 这个匹配也设置了Ruby的全局线程(thread-global)变量 $~ 和 $1 到 $9。变量 $~ 是一个MatchData对象(在336页开始部分有描述),它保存了所有关于这个匹配的信息。$1和其他$*保存了这个匹配的部分,我们呆会儿还会讨论它们。如果有人看见这些像Perl语言的变量名感到害怕,不要着急,这章后面还有好消息。

模式(Patterns)

每个正则表达式都有一个模式,用来和字符串做匹配。

在一个模式中,除了., |, (, ), [, {, +, \, ^, $, *和 ? 之外的字符都是和它本身匹配。

showRE('kangaroo', /angar/) }}k<<angar>>ooshowRE('!@%&-_=+', /%&/) }}!@<<%&>>-_=+ 如果你想在字面上匹配一个上面的特殊字符,在它前面加一个'\'。这解释了我们用在分离那个歌曲列表文件时用的一个正则表达式的一部分,/\s*|\s*/。'\|'表示匹配一个'|',没有那个反斜杠,'|'代表交换(我们会在后面描述)。showRE('yes | no', /\|/) }}yes <<|>> noshowRE('yes (no)', /\(no\)/) }}yes <<(no)>>showRE('are you sure?', /e\?/) }}are you sur<<e?>> 反斜杠后跟一个数字字符用来引进一个特殊的匹配构造,我们会在后面介绍它。另外,一个正则表达式可以包含#{...}表达式取代。

Anchors

一个正则表达式默认会找到字符串中第一个匹配的情况。要在字符串"Mississippi"中匹配 /iss/ ,它会找到那个靠近开始位置的哪个子串"iss"。但是当你想自己指定从头部或者末尾开始匹配时要怎么设置呢? 模式 ^ 和 $ 分别匹配一行字符的开始和结束。它们经常用来指定一个模式匹配的方向:比如,/^option/和在一行文本开始处出现的字符串'option'匹配。字符序列 \A 和一个字符串的开始匹配,\z 和 \Z 和一个字符串的结束匹配。(事实上,\Z 和一个以"\n"结尾的字符串的结束匹配,这种情况下,它从'\n'前面开始匹配)。showRE("this is\nthe time", /^the/) }}this is\n<<the>> timeshowRE("this is\nthe time", /is$/) }}this <<is>>\nthe timeshowRE("this is\nthe time", /\Athis/) }}<<this>> is\nthe timeshowRE("this is\nthe time", /\Athe/) }}no match 相似的,模式 \b 和 \B 分别和单词界限(word boundaries)和非单词界限(nonword boundaries)匹配。构成单词的字符有字母,数字和下划线。showRE("this is\nthe time", /\bis/) }}this <<is>>\nthe timeshowRE("this is\nthe time", /\Bis/) }}th<<is>> is\nthe time

字符类(character class)

一个字符类是一系列在方括号("[...]")之间的字符,用来匹配方括号里面的单个字符。比如,[aeiou]会和元音字符匹配,[,.:;!?]和标点符号匹配,等等。那些重要的特殊字符(.|()[{+^$*?)在方括号里面会失去匹配作用。但是普通的转义字符仍然起作用,所以,\b代表退格键,\n是换行符(见203页表18.2)。另外,你可以用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.字符类缩写字符序列 [ ... ]意思\d [0-9] 数字字符\D [^0-9] 非数字\s [\s\t\r\n\f] 空格字符\S [^\s\t\r\n\f] 非空格字符\w [A-Za-z0-9_] 单词符号\W [^A-Za-z0-9_] 非单词符号 最后,一个在放括号外面出现的句点"."表示除了换行符以外的任何字符(在多行模式下它也表示一个换行符)。a = 'It costs $12.'showRE(a, /c.s/) }}It <<cos>>ts $12.showRE(a, /./) }}<<I>>t costs $12.showRE(a, /\./) }}It costs $12<<.>>

重复(Repetition)

在我们讲述那个分隔歌曲文件的正则模式(/\s*\|\s*/)的时候, 我们说想匹配在一个'|'两边环绕任意的空格的情况。现在我们知道了 \s 匹配一个空白符,所以看来星号'*'代表任意数的大小。事实上,星号是允许你匹配模式多次出现的修饰符中的一个。 如果 r 代表一个模式里面的前置字符,那么:r* 匹配0个或多个 r.r+ 匹配1个或多个 r.r? 匹配0个或1个 r.r{m,n} 匹配最少m个,最多n个 r.r{m,} 匹配最少m个r. 这些重复修饰符有很高的优先权---在正则模式串里它们仅仅和它们的紧密前缀绑定。/ab+/匹配一个"a"后面跟一个或多个"b"而不是一个"ab"组成的序列。你也必须小心使用'*'修饰符---正则模式串/a*/会匹配任何字符串;任何有0个或者多个"a"的字符串。 这些模式串被称为“贪婪地”,因为它们默认会匹配尽量多的字符。你可以改变这种行为,让它们匹配最少的,只要加一个问号后缀就可以了。a = "The moon is made of cheese"showRE(a, /\w+/) }}<<The>> moon is made of cheeseshowRE(a, /\s.*\s/) }}The<< moon is made of >>cheeseshowRE(a, /\s.*?\s/) }}The<< moon >>is made of cheeseshowRE(a, /[aeiou]{2,99}/) }}The m<<oo>>n is made of cheeseshowRE(a, /mo?o/) }}The <<moo>>n is made of cheese

间隔(Alternation)

我们知道‘|’是特殊的,因为我们的在行分隔模式中必须用一个反斜杠使之转义。因为一个没有反斜杠的‘|’匹配正则表达式中它左右两边模式中的一个。a = "red ball blue sky"showRE(a, /d|e/) }}r<<e>>d ball blue skyshowRE(a, /al|lu/) }}red b<<al>>l blue skyshowRE(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”,我们用grouping重载默认的优先级。

成组技术(Grouping)

你可以在正则式中用圆括号来成组字符集。在这个组里面的所有字符会被认为是一个正则表达式。showRE('banana', /an*/) }}b<<an>>anashowRE('banana', /(an)*/) }}<<>>bananashowRE('banana', /(an)+/) }}b<<anan>>aa = 'red ball blue sky'showRE(a, /blue|red/) }}<<red>> ball blue skyshowRE(a, /(blue|red) \w+/) }}<<red ball>> blue skyshowRE(a, /(red|blue) \w+/) }}<<red ball>> blue skyshowRE(a, /red|blue \w+/) }}<<red>> ball blue skyshowRE(a, /red (ball|angry) sky/) }}no matcha = 'the red angry sky'showRE(a, /red (ball|angry) sky/) }}the <<red angry sky>> 圆括号也用来收集模式匹配的结果。Ruby对左括号记数,对每个左括号,它保存了已经匹配的部分结果和相应的右括号。你可以在剩下的模式串中或者你的Ruby程序里使用这个匹配。在模式匹配中,\1代表第1个组,\2代表第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" 能够利用目前的部分匹配允许你寻找各种形式的循环。# 匹配重复的字母showRE('He said "Hello"', /(\w)\1/) }}He said "He<<ll>>o"# 匹配重复的子串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#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" 第二个参数可以是一个字符串或者一个程序块(block)。如果用了程序块,那个程序块的值被替换进了字符串。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 }endmixedCase("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文本的字符串然后把它转化成普通的ASCII文本。因为这是为日本的用户编写的,它在正则式用了"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&lt;2 &amp;&amp; 4&gt;3")       
puts unescapeHTML("&quot;A&quot; = A = A")
产生结果:
1<2 && 4>3
"A" = A = A

面向对象的正则表达式

我们必须承认虽然这些关怪的表达式很好用,但是它们不是面向对象的,而且相当晦涩难懂。你们不是说过Ruby里面任何东西都是对象吗?为什么这里是这样的呢? 这无关紧要,真的。因为Matz在设计Ruby的时候,他构建了一个完全面向对的正则表达式处理系统。但是为了让Perl程序员感到熟悉一些,他把这些 $* 包装在这系统之上。这些对象和类还在那里,在外观里面,现在让我们花点时间把它们挖出来。 我们已经遇到过这样一个类了:字面正则表达式的产生类 Regexp(在361页有具体描述)。re = /cat/re.type }}Regexp 方法Regexp#match 把一个正则表达式和一个字符串进行匹配。如果不成功,方法返回 nil。在成功的情况下,它返回类 Matchdata 的一个实例,在336页有详细描述。然后那个 MatchData 对象给你访问这个匹配的各种信息的方法。所有能从 $-*变量里得到的好东东都可以在咱们手边这个小对象里得到。re = /(\d+):(\d+)/ # 匹配一个时间 hh:mmmd = re.match("Time: 12:34am")md.type }}MatchDatamd[0] # == $& }}"12:34"md[1] # == $1 }}"12"md[2] # == $2 }}"34"md.pre_match # == $` }}"Time: "md.post_match # == $' }}"am" 因为匹配信息是保存在它自己对象里的,你可以在同一时间保存两个或者多个匹配的结果,这用$-*你可能不能实现。在下面一个例子里,我们用同一个 Regexp 对象去匹配两个不同的字符串。每个匹配都返回一个唯一的 MatchData 对象,我们通过它们的两个子模式字段来区别它们。re = /(\d+):(\d+)/ # 匹配一个时间 hh:mmmd1 = re.match("Time: 12:34am")md2 = re.match("Time: 10:30pm")md1[1, 2] }}["12", "34"]md2[1, 2] }}["10", "30"] 但是那些 $-*是怎么包装起来的呢?每个模式匹配结束以后,Ruby在一个局部线程(thread-local)变量中保存了一个指向结果的引用(nil 或者 是一个 MatchData 对象)。所有其他的正则表达式变量都是从这个对象中继承而来的。虽然我们不能真正这样使用以下代码,但是它证明了所有其他的和MatchData有关的 $-*变量都是 $~里面的值。re = /(\d+):(\d+)/md1 = re.match("Time: 12:34am")md2 = re.match("Time: 10:30pm")[ $1, $2 ] # 最后匹配成功的情况 }}["10", "30"]$~ = md1[ $1, $2 ] # 上一个匹配成功情况 }}["12", "34"] 说完了所有这些,我们必须说点实话:) Andy和Dave平常都只是使用$-*变量而不去担心 MatchData对象。在日常应用中,我们只要用起来舒服就行。有时候我们情不自禁地使自己变得更务实。