存疑,知行RUBY文档中心
提供RUBY中文教程、文章、翻译作品

 

返回 :: 返回目录
 
7、标准类型

翻译:夏克  发表时间:2005-6-16 4:10:39  点击次数:3221

   
很高兴我们已经实现了我们的点唱机的部分代码,但是还远远不够,我们使用了数组、哈希和proc,但是还没有介绍Ruby的其它基本类型:数字、字符串、区间、正则表达式。下面我们用些篇幅来用这些基本类型写些例程。


数字

Ruby支持整数和浮点数,整数可以是任意长度(长度仅局限于你的系统内存的容量),一个特定范围(通常是-2的30次方到2的30次方-1或者-2的62次方到2的62次方-1)内的整数以二进制形式存放,属于Fixnum类的对象,超出这个范围的整数则属于Bignum类的对象(Bignum类目前是以变长的短整数集的形式实现的),两种形式间的转换由Ruby自动完成。

num = 8
7.times do
  print num.type, " ", num, "\n"
  num *= num
end

 
结果:

Fixnum 8
Fixnum 64
Fixnum 4096
Fixnum 16777216
Bignum 281474976710656
Bignum 79228162514264337593543950336
Bignum 6277101735386680763835789423207666416102355444464034512896


整数的写法是可选的前缀+一串数字,数字串中的下划线会被忽略。(前缀包括:0表示八进制, 0x表示十六进制, 0b表示二进制)

123456                    # Fixnum
123_456                   # Fixnum (忽略了下划线)
-543                      # 负Fixnum
123_456_789_123_345_789   # Bignum
0xaabb                    # 十六进制
0377                      # 八进制
-0b101_010                # 二进制(负)


也可以通过在前面加上问号来得到ASCII码字符对应的整数值和转义序列的值。control和meta的组合产生出?\C-x、?\M-x和?\M-\C-x这些形式,整数用control修饰相当于该整数与0x9f;用meta修饰相当于或0x80;还有?\C-?产生一个删除键的ASCII码,即0177。

(译者注:可能有的朋友不知道这里的control和meta表示什么意思,参考emacs的文档,control修饰相当于CTRL键,而meta键相当于ALT键,在类UNIX系统领域,这种用法相当普遍。)

?a                        # 普通字符
?\n                       # 换行符 (0x0a)
?\C-a                     # CTRL+a (0x01)
?\M-a                     # ALT+a
?\M-\C-a                  # CTRL+ALT+a
?\C-?                     # 删除键


一个带小数点的数字字面值被转换成Float对象,类似于常规的double数据类型,小数点后必须直接跟数字,如果你写的是1.e3,那么就会执行Fixnum类中的方法e3了。

所有的数字都是对象,可以相应许多消息(在290、313、315、323和349页有详细列表),所以,不像C++,数字的绝对值是aNumber.abs而不是abs(aNumber)。

整数还支持几个有用的迭代器,我们在47页的例子中已经看到过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    # 分割line,存储在val中
  print vals[0] + vals[1], " "
end


文件中的内容:

3 4
5 6
7 8


结果你得到的输出是"34 56 78"(译者注:而不是7 11 15),怎么回事?

问题在于系统把输入当成字符串来读取,而不是数字,加号用于连接字符串,所以我们就看到了这样的输出。如果修改的话,应该用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 "\\"'      >>      转义为"\"  
'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


(译者注:字符串的内容就是输入的这些行,第一行的<<指定了结束符即END_OF_STRING,当读入到这个符号时,字符串就读完了,上面的aString的内容即:
    The body of the string
    is the input lines up to
    one ending with the same
    text that followed the '<<'


一个here document由源代码中的行组成,但不包括结束符,就是你在<<后面指定的那个。正常情况下,这个结束符应该在第一列出现,不过,如果在<<后面添一个减号,就可以缩进这个结束符(译者注:和该结束符所包含的内容)了。

print <<-STRING1, <<-STRING2
   Concat
   STRING1
      enate
      STRING2


结果:
   Concat
      enate


使用字符串 

字符串可能是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干这件事最好不过了,在这里,我们要传递给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)


不幸的是,创建原始文件的人在是按列输入作者名的,这就包含了过多的空格,这如果出现在我们的高科技超扭曲平板帝高荧光显示器上的话,该是多么的丑陋,所以最好在这之前把多余的空格删除掉。有很多种办法,可能最简单的是用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相似,把字符串按模式拆分成块,不过,区别于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!一样,这个感叹号表明这个方法会在适当的地方修改调用它的对象,本例中,会把字符串转换成小写。[这个例子代码中有一个小错误:歌曲"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类的所有方法,不过还是算了,抓紧时间来看一个简单的数据类型:区间。


区间

区间存在于任何地方:一月到十二月,0到9,半熟到十分熟,50米到67米等等。既然Ruby要帮我们模拟现实,那看起来支持区间应该是最自然不过的事情了。实际上,Ruby做得更好,它竟然用区间实现了三个不同的特性:序列,条件和间隔。

区间作为序列

区间的首选用法同时也可能是最自然的用法就是表述一个序列。序列有一个开始位置,结束位置和产生连续值的某种途径。在Ruby中,使用形如".."和"..."的区间操作符来创建序列。两个点号创建一个闭区间,而三个点号创建一个右开区间。(译者注:这里要说明一下,笔者查阅过网络上的资料,没能找到Ranges的标准翻法,把Ranges翻译作区间,取其义相近,由此在这里又联想出闭区间和右开区间的翻法,可能会贻笑大方。如果有的朋友不知道闭区间和右开区间的意思,我解释一下:闭区间指两个边界均取值,右开区间指右边界不取值。)

1..10
'a'..'z'
0...anArray.length


不像一些早期版本的Perl,在Ruby中,区间在内部不是表现为列表:1..100000的序列被储存为一个Range对象,它包含两个Fixnum对象的引用。如果你需要,你可以使用to_a方法把区间转换成列表。

(1..10).to_a      >>      [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
('bar'..'bat').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能够创建基于你自己定义的对象的区间。唯一的限制就是这个对象必须能够响应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


没有表现出来的是,区间把每次测试的状态都作了记录,我们会在82页的详细描述中给出一些例子。

区间作为间隔

万能的区间的最后一个用途是间隔测试:看一些值是否落在了区间所描述的间隔内。这要用到===,条件相等算符。

(1..10)    === 5      >>      true  
(1..10)    === 15      >>      false  
(1..10)    === 3.14159      >>      true  
('a'..'j') === 'c'      >>      true  
('a'..'j') === 'z'      >>      false  


81页的条件表达式的例子展示了这种测试的应用,给出一个年份决定一种爵士风格。


正则表达式

在第50页的时候,我们从一个文件中创建歌曲列表,我们使用了一个正则表达式来匹配从输入文件中分割出的字段。我们说表达式line.split(/\s*\|\s*/)匹配一个用任意多空格前后围绕的竖线。让我们来研究一下正则表达式的更多细节看看为什么这种说法正确。

正则表达式用来从字符串中匹配模式(字符串),Ruby提供了对模式匹配和简便替换的内置支持,在这一节,我们看一下正则表达式的主要特色,没有谈到的地方在205页有更多介绍。

正则表达式是Regexp类型的对象,可以使用构造器显式地创建一个正则表达式,也可以使用字面值形式/pattern/和%r\pattern\来创建。

有了一个正则表达式对象后,就可以用Regxp#match(aString)的形式或者匹配运算符=~(正匹配)和!~(负匹配)来匹配字符串了。匹配运算符在String和Regexp中都有定义,如果两个操作数都是字符串,则右边的那个要被转换成正则表达式。

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('!@%&-_=+', /%&/)      >>      !@<<%&>>-_=+  


如果你希望匹配这些特殊字符,就需要加上反斜线做前缀。我们曾经用过的/\s*\|\s*/用来分割歌曲行,我们来解释一下这个模式的一部分,\|的意思是匹配一个竖线,如果没有反斜线,那么"|"意味着alternation(后面会讲到)。

showRE('yes | no', /\|/)      >>      yes <<|>> no  
showRE('yes (no)', /\(no\)/)      >>      yes <<(no)>>  
showRE('are you sure?', /e\?/)      >>      are you sur<<e?>>  


一个反斜线后跟一个字母或数字字符用来表示一个特定的匹配结构,我们在后面谈到。另外,一个正则表达式可以包含形如 #{...} 的替代表达式。

锚点

一个正则表达式默认总是试图在字符串中找到模式的第一个匹配,在字符串"Mississippi,"中匹配/iss/会找到第一个位置的子字符串"iss",但是如果你希望强制一个模式仅返回第一个或者最后一个匹配时怎么办?

模式^和$分别用来匹配行首和行尾,它们经常用来定位模式匹配。例如,/^option/仅返回单词"option"中第一个出现的匹配。序列\A匹配字符串开始的位置,\z和\Z匹配字符串结尾的位置(事实上,如果该字符串是以"\n"结尾的话,\Z匹配"\n"之前的位置。)

showRE("this is\nthe time", /^the/)      >>      this is\n<<the>> time  
showRE("this is\nthe time", /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分别匹配字边界和非字边界,字字符是字母、数字和下划线。

字符类

(译者注:这里的字符类不是面向对象中的类,只表示这些字符属于一个特殊的种类)

字符类是用方括号扩起来的字符的集合:[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之间也包括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]                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, /./)      >>      <<I>>t costs $12.  
showRE(a, /\./)      >>      It costs $12<<.>>  


重复
 
我们在分割歌曲列表行的时候使用了模式 /\s*\|\s*/,我们提过我们想匹配一个被任意数量的空白符围绕的竖线。我们现在知道了\s序列匹配一个单空白符,看上去星号似乎是表示“任意数量”的意思,实际上,星号是许多重复修饰符中的一个,它可以匹配一个模式的多次出现。

如果用r来表示模式中的正则表达式前面的部分,那么:

r *  匹配0个或多个r的出现
r +  匹配一个或多个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 cheese  
showRE(a, /\s.*\s/)      >>      The<< moon is made of >>cheese  
showRE(a, /\s.*?\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",如果你要这样,你需要用分组来覆盖默认的优先权。

分组

你可以使用圆括号把正则表达式分组,组中的内容被当作一个单独的正则表达式。

showRE('banana', /an*/)      >>      b<<an>>ana  
showRE('banana', /(an)*/)      >>      <<>>banana  
showRE('banana', /(an)+/)      >>      b<<anan>>a  


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"  


在后面的匹配中使用当前匹配的部分的功能使你可以找到更多的重复的形式。

# 匹配重复的字母  
showRE('He said "Hello"', /(\w)\1/)      >>      He said "He<<ll>>o"  
# 匹配重复的子字符串
showRE('Mississippi', /(\w+)\1/)      >>      M<<ississ>>ippi  


你也可以使用后方参考来匹配定界符。

(后方参考是back reference的直译,在传统的正则表达式概念中,back reference的定义即我们这里说的\1、\2等,是用在模式的后面来取得模式前面部分所匹配的结果。)

showRE('He said "Hello"', /(["']).*?\1/)      >>      He said <<"Hello">> 
showRE("He said 'Hello'", /(["']).*?\1/)      >>      He said <<'Hello'>>  


基于模式的替换

有时候在字符串中找出一个模式已经非常不错了。比方说,如果你的朋友要向你挑战,让你找到包含a,b,c,d,e这5个字母并且这5个字母还是按着次序排列的单词,你可以用/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"  


两个方法的第二个参数既可以是字符串,也可以是代码块。如果是代码块,代码块的值被替换成字符串。

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转义序列的字符串,然后把它转换成正常的ASCII码。因为它是写给日本用户的,所以在正则表达式上使用了"n"修饰符,它跳过了Wide-character的处理过程,它也展示了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类(361页有详细文档)的实例。

re = /cat/  
re.type      >>      Regexp  


方法Regexp#match从字符串中匹配一个正则表达式,如果不成功,方法返回nil,如果成功,返回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对象,对于日常使用,它们确实很方便。有时我们对实务也帮不了什么忙。(译者注:这句话实际上是作者的一种调侃语气,因为本书是作者的Pragmatic Programmers系列中的一部,所以这里调侃说自己对实务帮不了什么忙,但我们都看得出来,作者的贡献是太大了,连ruby作者都感谢他们写出了完整的文档。)
版权声明:RUBY文档中心的所有文章标明[原创]的均为本站作品,版权属RUBY中文化计划,若转载请注明;标明[翻译]的其外文版权归原作者,译文版权属RUBY中文化计划;标明[转贴]的,若原作者感到侵犯了他的著作权,那么请及时跟主持人联系,我们会尽快更正。
 

 

 

版权所有(C) RUBY中文化计划