学习ruby 的正则表达式

本文深入探讨了正则表达式的使用方法,包括基本语法、特殊字符、字符类、重复匹配等核心概念,并通过实例展示了如何在Ruby中进行字符串匹配与替换。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

正则表达式[ 作者: gong1]
之前我们根据一个档案的内容来建立歌曲列表的时候,我们用了一个正则表达式来匹配档案里的各个字段。我们说用了 line.split(//s*|/s*) line.split(//s*|/s*/) (一校) 会匹配一个「被非必须的空格所环绕的小竖线」。现在让我们来仔细研究一下正则表达式,证明我们之前所说的是正确的。
正则表达式是用来匹配字符串的样版。 正则表达式可用来对照文字而匹配样版。 (一校:据原文 "Regular expressions are used to match patterns against strings.") Ruby 内建了这种功能,让我们处理样版匹配和替换时很方便简练。在这小节中,我们会介绍主要的正规表示式功能。
正则表达式是 Regexp 这个类别的对象。它们可以用显式的构造函数来建立或者直接用 /pattern/ 和 %r/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) 或者用匹配运算子 =~(确定匹配)和 !~(否定匹配)。String 和 Regexp 对象都可以使用匹配运算子。如果匹配运算子的两个操作数都是字符串的话,右边那个会转成正规表示式。
a = "Fats Waller"
a =~ /a/ » 1
a =~ /z/ » nil
a =~ "ll" » 7
匹配运算会传回匹配成功的字符位置。同时它们还额外地设置了很多相关的 Ruby 变数。$& 会纪录匹配成功的字符串,$`(键盘 1 左边那个键)会纪录匹配处之前的字符串,$' 则会纪录匹配处之后的字符串。我们可以用这个来编写一个 method,showRE,它将会把被匹到的部份标记出来:(注:也就是说 $` + $& + $' = 原字符串)以下程序代码用到匹配运算写个 showRE 方法,以图示标明指定的样版匹配。 (一校:本句译文缺漏故补译。)
def showRE(a,re)
 if a =~ re
 "#{$`}<<#{$&}>>#{$'}"
 else
 "no match"
 end
end
showRE('very interesting', /t/) » very in< >eresting
showRE('Fats Waller', /ll/) » Fats Wa< >er
匹配运算的同时也设置了跨执行绪 全域变量 (thread-global variables) (一校:未校译文未指出 "thread-" 文意) $~ 和 $1 到 $9。变量 $~ 是一个 MatchData 匹配数据对象(在 336 页开始部分有描述),它保存了所有关于这个匹配的讯息。$1 和其它保存了这个匹配的部分 从 $1 之后保存了该匹配的各部份 (一校:原文 "$1 and so on hold the values of parts of the match." ),我们稍后还会讨论它们。如果有人看见这些像 Perl 语言的变量名称感到害怕,不用着急,这章后面还有好消息。
样版(Patterns)
每个正则表达式都有一个样版,用来和字符串做匹配。
在一个样版中,除了 ., |, (, ), [, {, +, /, ^, $, * 和 ? 之外的字符都是对字符本身作匹配。
showRE('kangaroo', /angar/) » k< >oo
showRE('!@%&-_=+', /%&/) » !@<<%&>>-_=+
如果你想在字面上匹配一个上面的特殊字符,在它前面加一个反斜线「/」。这解释了我们用在分离那个歌曲列表文件时用的一个正规表示式的一部分,//s*|/s*/。/| 表示要匹配一个「管线符号 |」,没有那个反斜线,"|" 代表或者 替换 (alternation) (一校)(我们会在后面描述)。
showRE('yes | no', //|/) » yes <<|>> no
showRE('yes (no)', //(no/)/) » yes <<(no)>>
showRE('are you sure?', /e/?/) » are you sur<>
反斜线后跟一个数字字符用来引进一个特殊的匹配构造,我们会在后面介绍它。另外,一个正规表示式可以包含 #{...} 语句取代。

锚点(Anchors)
一个正规表示式默认会找到字符串中第一个匹配的情况。要在字符串 "Mississippi" 中匹配 /iss/ ,它会找到那个靠近开始位置的哪个子字符串 "iss"。但是当你想自己指定从起始或者尾端开始匹配时要怎么设置呢?
样版 ^ 和 $ 分别匹配一行字符的开始和结束。它们经常用来指定一个样版匹配的方向 固定样版匹配的位置 (一校) :比如,/^option/ 和在一行文本 只出现在一段文字 (一校) 开始处出现 (一校) 的字符串 'option' 匹配。字符序列 /A 和一个字符串的起始 字符串开头 (一校) 匹配,/z 和 /Z 和一个字符串的尾端匹配。(事实上,/Z 和一个以 "/n" 结尾的字符串的结束匹配,这种情况下,它从 '/n' 前面开始匹配。 /Z 一般是匹配到字符串结尾,除非遇到有个字符串以 "/n" 符号结尾,才会在 "/n" 符号前面做匹配。 (一校) )
showRE("this is/nthe time", /^the/) » this is/n< > time
showRE("this is/nthe time", /is$/) » this < >/nthe time
showRE("this is/nthe time", //Athis/) » < > is/nthe time
showRE("this is/nthe time", //Athe/) » no match
相似的,样版 /b 和 /B 分别和 单词界限(word boundaries)和非单词界限(nonword boundaries) 英文单字分隔符与非英文单字分隔符 (一校)匹配。构成单字的是字母,数字和底线。
showRE("this is/nthe time", //bis/) » this < >/nthe time
showRE("this is/nthe time", //Bis/) » th< > is/nthe time

字符类别(Character Class)
一个字符类别是一系列 一组 (一校) 在方括号 [characters] 之间的字符,用来匹配方括号里面的单个字符 可匹配每个出现在方括号中的字符 (一校) 。比如,[aeiou] 会和元音字符匹配,[,.:;!?] 和标点符号匹配,等等。而那些有意义的特殊字符 .|()[{+^$*? 在方括号里面会失去匹配作用。 正规表示式所有特殊符号 .|()[{+^$*? 的意义在方括号里面会失效。 (一校) 但是普通的转义字符仍然起作用,所以,/b 代表退格键,/n 是换行符号(见 203 页表 18.2)。另外,你可以用 59 页的表 5.1 的缩写,所以 /s 表示空格符,不仅仅是一个字面上的空格。
showRE('It costs $12.', /[aeiou]/) » It c< >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 [< >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.

表5.1:字符类别缩写
字符序列 [ ... ] 意义
/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 < >ts $12.
 showRE(a, /./) » < >t costs $12.
 showRE(a, //./) » It costs $12<<.>>

重复(Repetition)
在我们讲述那个分隔歌曲文件的样版 //s*/|/s*/ 的时候, 我们说想匹配在一个 '|' 两边环绕任意的空格的情况。现在我们知道了 /s 匹配一个空格符,所以星号 '*' 代表任意数的大小。事实上,星号是允许你匹配模式多次出现的修饰字符中的一个。 星号是可任由匹配多次出现的许多修饰字符的其中一个。 (一校)
如果 r 代表一个样板里面的前导符 若 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*/ 样版可匹配任何由0个或多个 "a" 构成的字符串。 (一校)
这些样版被称为「贪婪地」,因为它们默认会匹配最多的字。你可以改变这种行为,让它们匹配最少的,只要加一个问号后缀就可以了。(注:以 "abbbbbc" 来说 /ab+/ 会匹配 abbbbb 而不是 ab、abb、abbb、abbbb 等)
a = "The moon is made of cheese"
showRE(a, //w+/) » < > 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< >n is made of cheese
showRE(a, /mo?o/) » The < >n is made of cheese

间隔 替换 (一校) (Alternation)
我们知道管线符号是特殊符号,因为我们的在 行分隔模式 分行式样版 (line splitting pattern) (一校) 中必须用一个反斜线使之转义 (escape) 。因为一个没有反斜线的管线符号 "|" 匹配正规表示式中它左右 样版 正规表示式 (一校) 的其中一个。
a = "red ball blue sky"
showRE(a, /d|e/) » r< >d ball blue sky
showRE(a, /al|lu/) » red b< >l blue sky
showRE(a, /red ball|angry sky/) » < > blue sky
要是我们一粗心,这里会有一个陷阱,因为 "|" 的优先层级很低。在上面最后一个例子中,我们的正规表示式匹配「red ball」或者「angry sky」,而不是「red ball sky」或「red angry sky」。为了匹配「red ball sky」或「red angry sky」,我们用 grouping 重载默认的优先层级。

分组(Grouping)
你可以在正则表达式中用圆括号来组合多个词。在这个组合 (group) 里面的每样事物是以正则表达式看待。
showRE('banana', /an*/) » b< >ana
showRE('banana', /(an)*/) » <<>>banana
showRE('banana', /(an)+/) » b< >a
a = 'red ball blue sky'
showRE(a, /blue|red/) » < > ball blue sky
showRE(a, /(blue|red) /w+/) » < > blue sky
showRE(a, /(red|blue) /w+/) » < > blue sky
showRE(a, /red|blue /w+/) » < > ball blue sky
showRE(a, /red (ball|angry) sky/) » no match
a = 'the red angry sky'
showRE(a, /red (ball|angry) sky/) » the < >
圆括号也用来收集样版匹配的结果。Ruby 对左括号记数,对每个左括号,它保存了已经匹配的部分结果和相应的右括号。你可以在剩下的样版中或者你的 Ruby 程序里使用这个匹配。在样版中,/1 代表第 1 个组,/2 代表第 2 个组,其它依此类推。在样版外面,特殊变量 $1、$2 其它 $* 和这个作用一样。 Ruby 为样版中每个左括号给个数字,并储存了每个左括号与其相对右括号之间的部份匹配结果。你可以在样版内其余区段及样版之外程序中使用这些部份匹配结果。在样版内,序列的 /1 参考到第一项组合 (group) 的匹配结果, /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< >o"
# 匹配重复的子串
showRE('Mississippi', /(/w+)/1/) » M< >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 五个字母 (一校) 的单词,你可以用样版 /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 区块 (一校) 。如果用了 block 区块 (一校) ,字符串会以 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 }
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"
如果你以 block 区块 (一校) 的方式来使用 gsub ,替换的字符串仅只会分析一次(在语法分析阶段),而结果是你想要的:
str = 'a/b/c' » "a/b/c"
str.gsub(////) { '////' } » "a//b//c"
最后,作为正规表示式和 block 程序区块 (一校) 结合起来的强大表现力的例子,我们来看看这段 Wakou Aoyama 所写 CGI 模块里的程序代码。程序接收一段包含 HTML 跳脱序列 (escape sequence) 的字符串,将其转化成普通的 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<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 类别的一个实例,在 336 页有详细描述。然后 MatchData 对象提供可以存取各种匹配结果的方法。所有能从 $ 系列变量里得到的好东东都可以在咱们手边这个小对象里得到。
re = /(/d+):(/d+)/ # 匹配一个时间 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+)/ # 匹配一个时间 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 对象)保存在执行绪的区域变量 (thread-local variables) 中(可由 $~ 存取)。所有其它的正规表示式变量都是从这个对象中衍生而来的。虽然我们不能真正这样使用以下程序代码,但是它证明了所有其它的和 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 对象。在日常应用中,我们只要用起来舒服就行。有时候我们不得不使自己变得更务实。 
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值