Ruby 2.4参考手册
Regexp
Regexp
父类:对象
Regexp
拥有一个正则表达式,用于匹配字符串的模式。正则表达式是使用/.../
和%r{...}
文字以及Regexp::new
构造函数创建的。
正则表达式(regexp_s)是描述字符串内容的模式。它们用于测试字符串是否包含给定模式,或者提取匹配的部分。它们是用/
_pat/
和%r{
pat}
文字或Regexp.new
构造函数创建的。
正则表达式通常用正斜杠(/
)分隔。例如:
/hay/ =~ 'haystack' #=> 0
/y/.match('haystack') #=> #<MatchData "y">
如果一个字符串包含它被认为匹配的模式。一个文字串匹配它自己。
这里'haystack'不包含模式'针',所以它不匹配:
/needle/.match('haystack') #=> nil
这里'haystack'包含模式'hay',所以它匹配:
/hay/.match('haystack') #=> #<MatchData "hay">
具体来说,/st/
要求字符串包含字母s后跟字母t,所以它也与干草堆相匹配。
=~
and #match
模式匹配可以通过使用=~
运算符或#match方法来实现。
=~
operator
=~
是Ruby的基本模式匹配运算符。当一个操作数是一个正则表达式而另一个是一个字符串时,则正则表达式被用作模式来匹配字符串。(这个操作符等价地由Regexp和String定义,所以String和Regexp的顺序无关紧要,其他类可能有不同的实现=~
。)如果找到匹配,操作符返回字符串中第一个匹配的索引,否则返回nil
。
/hay/ =~ 'haystack' #=> 0
'haystack' =~ /hay/ #=> 0
/a/ =~ 'haystack' #=> 1
/u/ =~ 'haystack' #=> nil
使用=~
带有String和Regexp的运算符,$~
在成功匹配后设置全局变量。$~
拥有一个MatchData对象。:: last_match等价于$~
。
#match method
匹配方法返回一个MatchData对象:
/st/.match('haystack') #=> #<MatchData "st">
元字符和转义
以下是元字符 (
,)
,[
,]
,{
,}
,.
,?
,+
,*
。出现在模式中时,它们有特定的含义。要从字面上匹配它们,它们必须被反斜线转义。匹配反斜杠字面上的反斜杠 - 转义:\\\
。
/1 \+ 2 = 3\?/.match('Does 1 + 2 = 3?') #=> #<MatchData "1 + 2 = 3?">
模式表现得像双引号字符串,因此可以包含相同的反斜杠转义符。
/\s\u{6771 4eac 90fd}/.match("Go to 東京都")
#=> #<MatchData " 東京都">
任意的Ruby表达式可以嵌入到#{...}
构造的模式中。
place = "東京都"
/#{place}/.match("Go to 東京都")
#=> #<MatchData "東京都">
Character Classes
甲字符类被分隔用方括号([
,]
可能出现在匹配该点)和列表的字符。/[ab]/
意味着一个或b,相对于/ab/
这意味着一个随后b。
/W[aeiou]rd/.match("Word") #=> #<MatchData "Word">
在字符类中,连字符(-
)是一个表示包含字符范围的元字符。[abcd]
相当于[a-d]
。一个范围可以跟着另一个范围,所以[abcdwxyz]
相当于[a-dw-z]
。范围或单个字符在字符类中出现的顺序是不相关的。
/[0-9a-f]/.match('9f') #=> #<MatchData "9">
/[9f]/.match('9f') #=> #<MatchData "9">
如果字符类的第一个字符是caret(^
),则该类将被反转:它会匹配除名称之外的任何字符。
/[^a-eg-z]/.match('f') #=> #<MatchData "f">
一个字符类可能包含另一个字符类。本身并没有用,因为[a-z[0-9]]
描述了与之相同的集合[a-z0-9]
。但是,字符类还支持&&
对其参数执行集合交集的运算符。这两者可以结合如下:
/[a-w&&[^c-g]z]/ # ([a-w] AND ([^c-g] OR z))
这相当于:
/[abh-w]/
以下元字符的行为类似于字符类:
-
/./
- 除换行符外的任何字符。
-
/./m
- 任何字符(m
修饰符启用多行模式)
-
/\w/
- 一个字符([a-zA-Z0-9_]
)
-
/\W/
- 一个非单词字符([^a-zA-Z0-9_]
)。请看看错误#4044,如果使用/\W/
与/i
修改。
-
/\d/
- 一个数字字符([0-9]
)
-
/\D/
- 非数字字符([^0-9]
)
-
/\h/
- 一个十六进制字符([0-9a-fA-F]
)
-
/\H/
- 一个非十六进制字符([^0-9a-fA-F]
)
-
/\s/
- 空格字符:/[ \t\r\n\f\v]/
-
/\S/
- 非空白字符:/[^ \t\r\n\f\v]/
POSIX 括号表达式也与字符类相似。它们为上述提供了一种便携式替代方案,而且还包含非ASCII字符。例如,/\d/
只匹配ASCII十进制数字(0-9); 而/[[:digit:]]/
匹配Unicode Nd类别中的任何字符。
-
/[[:alnum:]]/
- 字母和数字字符
-
/[[:alpha:]]/
- 字母字符
-
/[[:blank:]]/
- 空间或标签
-
/[[:cntrl:]]/
- 控制字符
-
/[[:digit:]]/
- 数字
-
/[[:graph:]]/
- 非空白字符(不包括空格,控制字符和类似字符)
-
/[[:lower:]]/
- 小写字母字符
-
/[[:print:]]/
- 如:图:,但包含空格字符
-
/[[:punct:]]/
- 标点符号
-
/[[:space:]]/
- 空格字符([:blank:]
,换行符,回车等)
-
/[[:upper:]]/
- 大写字母
-
/[[:xdigit:]]/
- 数字允许以十六进制数字(即0-9a-fA-F)
Ruby还支持以下非POSIX字符类:
-
/[[:word:]]/
- 以下Unicode常规类别之一的字符Letter,Mark,Number,Connector_Punctuation
-
/[[:ascii:]]/
- ASCII字符集中的字符
Repetition
到目前为止描述的结构符合单个字符。它们可以跟随一个重复元字符来指定它们需要发生的次数。这种元字符称为量词。
-
*
- 零次或多次
-
+
- 一次或多次
-
?
- 零次或一次(可选)
-
{
n}
- 恰好n次
-
{
n,}
- n次或更多次
-
{,
m}
- m或更少
-
{
n,
m}
- 至少n次,最多m次
至少一个大写字符('H'),至少一个小写字符('e'),两个'l'字符,然后一个'o':
"Hello".match(/[[:upper:]]+[[:lower:]]+l{2}o/) #=> #<MatchData "Hello">
默认情况下,重复是贪婪的:尽可能多地匹配,同时仍然允许总体匹配成功。相比之下, 延迟匹配使得整体成功所需的最少量的匹配成为可能。一个贪婪的元字符可以通过跟随它而变得很懒?
。
这两个模式都与字符串匹配。第一个使用贪婪量词,所以'。+'匹配'<a> <b>'; 第二个使用一个懒惰的量词,所以'。+?' 匹配“<a>”:
/<.+>/.match("<a><b>") #=> #<MatchData "<a><b>">
/<.+?>/.match("<a><b>") #=> #<MatchData "<a>">
量词后跟+
匹配占有性:一旦匹配不走回头路。他们表现得像贪婪的量词,但是如果匹配,他们拒绝“放弃”他们的比赛,即使这会危及整体比赛。
捕获
圆括号可用于捕获。由第n组括号括起来的文本 随后可以用n来表示。在模式内使用 反向引用 \n
; 在模式外使用 MatchData[n]
。
'at'由第一组括号捕获,然后在后面提及\1
:
/[csh](..) [csh]\1 in/.match("The cat sat in the hat")
#=> #<MatchData "cat sat in" 1:"at">
#match返回一个MatchData对象,该对象使用[]方法使捕获的文本可用:
/[csh](..) [csh]\1 in/.match("The cat sat in the hat")[1] #=> 'at'
当使用(?<
名称>)
或(?'
名称')
结构进行定义时,捕获组可以按名称引用。
/\$(?<dollars>\d+)\.(?<cents>\d+)/.match("$3.67")
#=> #<MatchData "$3.67" dollars:"3" cents:"67">
/\$(?<dollars>\d+)\.(?<cents>\d+)/.match("$3.67")[:dollars] #=> "3"
命名组可以用\k<
名称反向引用>
,其中name是组名称。
/(?<vowel>[aeiou]).\k<vowel>.\k<vowel>/.match('ototomy')
#=> #<MatchData "ototo" vowel:"o">
注意:正则表达式不能同时使用命名的反向引用和编号的反向引用。
当命名的捕获组与表达式和=~
操作符左侧的文字正则表达式一起使用时,捕获的文本也会被分配给具有相应名称的局部变量。
/\$(?<dollars>\d+)\.(?<cents>\d+)/ =~ "$3.67" #=> 0
dollars #=> "3"
分组
还括号组它们包围的条款,允许它们被作为一个量化的原子整体。
下面的模式匹配一个元音后跟两个单词字符:
/ [aeiou] \ w {2} /。match(“Caenorhabditis elegans”)#=>#<MatchData“aen”>
而下面的模式匹配一个元音后跟一个单词字符,两次,即[aeiou]\w[aeiou]\w
:'enor'。
/([aeiou] \ w){2} /。match(“Caenorhabditis elegans”)
#=>#<MatchData“enor”1:“or”>
该(?:
... )
构造提供分组而不捕获。也就是说,它将它包含的术语组合到一个原子整体中,而不创建反向引用。这样可以降低可读性,有利于性能。
第一组圆括号包含'n'和第二个'ti'。后面提到的第二组是反向引用 \2
:
/ I(n)ves(ti)ga \ 2ons /。match(“Investigations”)
#=>#<MatchData“Investigations”1:“n”2:“ti”>
现在第一组圆括号用'?:'进行非捕获,所以它仍然匹配'n',但不创建反向引用。因此,反向引用\1
现在指的是'ti'。
/ I(?:n)ves(ti)ga \ 1ons /。match(“Investigations”)
#=>#<MatchData“Investigations”1:“ti”>
原子分组
分组可以用 pat拍成原子。这会导致子表达式pat独立于表达式的其余部分进行匹配,因此它匹配的内容会在匹配的其余部分变得固定,除非整个子表达式必须放弃并随后重新访问。通过这种方式,pat被视为一个不可分割的整体。原子分组通常用于优化模式,以防止正则表达式引擎不必要地回溯。(?>)
的"
图案中的下面相匹配的字符串的第一个字符,然后.*
匹配报价“。这会导致整个匹配失败,所以匹配的文本.*
会被一个位置回溯,从而使字符串的最后一个字符可以匹配"
/".*"/。match('“Quote”') #=>#<MatchData“\”Quote \“”>
如果.*
以原子方式分组,则拒绝回溯 引用“,尽管这意味着整体匹配失败
/I(?:n)ves(ti)ga\1ons/.match("Investigations")
#=> #<MatchData "Investigations" 1:"ti">
子表达式调用
该\g<
名>
语法命名前面的子表达式匹配的名称,它可以是一组名称或号码,再次。这不同于反向引用,因为它重新执行组而不是简单地重新匹配相同的文本。
此模式匹配(字符并将其分配给 paren
组,尝试再次调用该paren
子表达式但失败,然后匹配文字):
/\A(?<paren>\(\g<paren>*\))*\z/ =~ '()'
/\A(?<paren>\(\g<paren>*\))*\z/ =~ '(())' #=> 0
# ^1
# ^2
# ^3
# ^4
# ^5
# ^6
# ^7
# ^8
# ^9
# ^10
- 匹配字符串的开头,即在第一个字符之前。
2. 进入名为的已命名捕获组 paren
3. 匹配一个文字(,字符串中的第一个字符
4. paren
再次调用该组,即递归回到第二步
5. 重新进入该paren
组
6. 匹配一个文字(,字符串中的第二个字符
7. 尝试paren
第三次调用,但失败,因为这样做会阻止整体成功的匹配
8. 匹配文字),字符串中的第三个字符。标记第二次递归调用的结束
9. 匹配文字),即字符串中的第四个字符
10. 匹配string
Alternation
结尾垂直栏metacharacter(|
)将两个表达式组合成一个匹配任一表达式的表达式。每个表达式都是一种替代方式
/ \ w(和| or)\ w /。match(“Feliformia”)#=>#<MatchData“form”1:“or”>
/ \ w(and | or)\ w /。match(“furandi”) #=>#<MatchData“randi”1:“and”>
/ \ w(and | or)\ w /。match(“dissemblance”)#=> nil
字符属性¶ ↑
该\p{}
构造将字符与命名属性匹配,就像POSIX括号类一样。
-
/\p{Alnum}/
- 字母和数字字符
-
/\p{Alpha}/
- 字母字符
-
/\p{Blank}/
- 空间或标签
-
/\p{Cntrl}/
- 控制字符
-
/\p{Digit}/
- 数字
-
/\p{Graph}/
- 非空白字符(不包括空格,控制字符和类似字符)
-
/\p{Lower}/
- 小写字母字符
-
/\p{Print}/
- 如\p{Graph}
,但包括空格字符
-
/\p{Punct}/
- 标点符号
-
/\p{Space}/
- 空格字符([:blank:]
,换行符,回车等)
-
/\p{Upper}/
- 大写字母
-
/\p{XDigit}/
- 数字允许以十六进制数字(即0-9a-fA-F)
-
/\p{Word}/
- 以下Unicode常规类别之一的成员Letter,Mark,Number,Connector_Punctuation
-
/\p{ASCII}/
- ASCII字符集中的字符
-
/\p{Any}/
- 任何Unicode字符(包括未分配的字符)
-
/\p{Assigned}/
- 指定的字符Unicode字符的常规类别值也可以与\p{
Ab匹配,}
其中Ab是类别的缩写,如下所述:
-
/\p{L}/
- 'Letter'
-
/\p{Ll}/
- 'Letter: Lowercase'
-
/\p{Lm}/
- 'Letter: Mark'
-
/\p{Lo}/
- 'Letter: Other'
-
/\p{Lt}/
- 'Letter: Titlecase'
-
/\p{Lu}/
- 'Letter: Uppercase
-
/\p{Lo}/
- 'Letter: Other'
-
/\p{M}/
- 'Mark'
-
/\p{Mn}/
- 'Mark: Nonspacing'
-
/\p{Mc}/
- 'Mark: Spacing Combining'
-
/\p{Me}/
- 'Mark: Enclosing'
-
/\p{N}/
- 'Number'
-
/\p{Nd}/
- 'Number: Decimal Digit'
-
/\p{Nl}/
- 'Number: Letter'
-
/\p{No}/
- 'Number: Other'
-
/\p{P}/
- 'Punctuation'
-
/\p{Pc}/
- 'Punctuation: Connector'
-
/\p{Pd}/
- 'Punctuation: Dash'
-
/\p{Ps}/
- 'Punctuation: Open'
-
/\p{Pe}/
- 'Punctuation: Close'
-
/\p{Pi}/
- 'Punctuation: Initial Quote'
-
/\p{Pf}/
- 'Punctuation: Final Quote'
-
/\p{Po}/
- 'Punctuation: Other'
-
/\p{S}/
- 'Symbol'
-
/\p{Sm}/
- 'Symbol: Math'
-
/\p{Sc}/
- 'Symbol: Currency'
-
/\p{Sc}/
- 'Symbol: Currency'
-
/\p{Sk}/
- 'Symbol: Modifier'
-
/\p{So}/
- 'Symbol: Other'
-
/\p{Z}/
- 'Separator'
-
/\p{Zs}/
- 'Separator: Space'
-
/\p{Zl}/
- 'Separator: Line'
-
/\p{Zp}/
- 'Separator: Paragraph'
-
/\p{C}/
- 'Other'
-
/\p{Cc}/
- 'Other: Control'
-
/\p{Cf}/
- 'Other: Format'
-
/\p{Cn}/
- 'Other: Not Assigned'
-
/\p{Co}/
- 'Other: Private Use'
-
/\p{Cs}/
- 'Other: Surrogate'
最后,\p{}
匹配一个字符的Unicode 脚本。下面的脚本支持:阿拉伯语,亚美尼亚,巴厘,孟加拉语,汉语拼音,盲文,布吉文,布迪文,Canadian_Aboriginal,卡里亚,湛,切诺基,通用,科普特人,楔形文字,塞浦路斯,西里尔文,犹他州,梵文,埃塞俄比亚,格鲁吉亚,格拉哥里,哥特式,希腊语,古吉拉特语,旁遮普文,韩,朝鲜文,哈努诺文,希伯来文,平假名,继承,埃纳德语,片假名,KAYAH_LI,卡罗须提语,高棉,老挝,拉丁语,雷布查,林布,Linear_B,利西亚,吕底亚,马拉雅拉姆语,蒙古,缅甸,NEW_TAI_LUE,西非书面语言,欧甘文,OL_CHIKI,Old_Italic,Old_Persian,奥里雅语,奥斯曼亚语,Phags_Pa,腓尼基,拉让,符文,索拉什特拉,萧伯纳,僧伽罗人,巽,Syloti_Nagri,叙利亚,菲律宾语,塔格巴努亚文,Tai_Le,泰米尔语, Telugu, Thaana, Thai, Tibetan, Tifinagh, Ugaritic, Vai, and Yi.
Unicode代码点U + 06E9被命名为“ARABIC PLACE OF SAJDAH”并属于阿拉伯语脚本:
/\p{Arabic}/.match("\u06E9") #=> #<MatchData "\u06E9">
所有的字符属性都可以通过在名称前加上插入符(^
)来反转。
字母'A'不在Unicode Ll(Letter;小写字母)类别中,因此该匹配成功:
/\p{^Ll}/.match("A") #=> #<MatchData "A">
Anchors
锚点是元字符,匹配字符之间的零宽度位置,将匹配锚定到特定位置。
-
^
- 匹配行首
-
$
- 匹配行尾
-
\A
- 匹配字符串的开头。
-
\Z
- 匹配字符串的结尾。如果字符串以换行符结束,则它在换行符之前匹配
-
\z
- 匹配字符串的结尾
-
\G
- 匹配第一个匹配位置:
在像String#gsub
和的方法中String#scan
,它在每次迭代中都会改变。它最初匹配主题的开始,并且在每个后续迭代中它匹配最后匹配完成的地方。
" a b c".gsub(/ /, '_') #=> "____a_b_c" " a b c".gsub(/\G /, '_') #=> "____a b c"
在类似的方法Regexp#match
和String#match
称取(可选的)偏移量,它匹配于搜索的开始位置。
"hello, world".match(/,/, 3) #=> #<MatchData ","> "hello, world".match(/\G,/, 3) #=> nil
-
\b
- 在括号外匹配单词边界; 当括号内的退格(0x08)
-
\B
- 匹配非单词边界
-
(?=
pat)
- 积极的lookahead断言:确保以下字符匹配pat,但不包括匹配文本中的那些字符
-
(?!
pat)
- 负向前瞻断言:确保以下字符不匹配pat,但不包括匹配文本中的那些字符
-
(?<=
pat)
- 积极的lookbehind断言:确保前面的字符匹配pat,但不包括匹配文本中的那些字符
-
(?<!
pat)
- 否定后置断言:确保前面的字符不匹配pat,但不包括匹配文本中的那些字符
如果一个模式没有锚定,它可以从字符串中的任何一点开始:
/real/.match("surrealist") #=> #<MatchData "real">
将模式锚定到字符串的开头会强制匹配从那里开始。'真'不会出现在字符串的开头,所以现在匹配失败:
/\Areal/.match("surrealist") #=> nil
下面的匹配失败,因为尽管'请求'包含'和',模式不会出现在单词边界。
/\band/.match("Demand")
而在下面的例子中,'和'已经锚定到一个非单词边界,所以不是匹配第一个'和'而是匹配来自'demand'的第四个字母:
/\Band.+/.match("Supply and demand curve") #=> #<MatchData "and curve">
下面的模式使用积极lookahead和积极lookbehind匹配出现在标签中的文本,而不包含匹配中的标签:
/(?<=<b>)\w+(?=<\/b>)/.match("Fortune favours the <b>bold</b>")
#=> #<MatchData "bold">
Options
正则表达式的结束分隔符后面可以跟一个或多个单字母选项,用于控制模式如何匹配。
-
/pat/i
- 忽略大小写
-
/pat/m
- 将一个换行符视为一个匹配的字符.
-
/pat/x
- 忽略模式中的空格和注释
-
/pat/o
-#{}
只进行一次插值
i
,m
和x
也可以在与所述子表达式级别应用(?
上-
掉)
构建体,其使得选项上,并禁用选项关闭用于由括号包围的表达。
/a(?i:b)c/.match('aBc') #=> #<MatchData "aBc">
/a(?i:b)c/.match('abc') #=> #<MatchData "abc">
选项也可以用于Regexp.new
:
Regexp.new("abc", Regexp::IGNORECASE) #=> /abc/i
Regexp.new("abc", Regexp::MULTILINE) #=> /abc/m
Regexp.new("abc # Comment", Regexp::EXTENDED) #=> /abc # Comment/x
Regexp.new("abc", Regexp::IGNORECASE | Regexp::MULTILINE) #=> /abc/mi
Free-Spacing Mode and Comments
如上所述,该x
选项启用自由间隔模式。模式内的文字空白被忽略,并且octothorpe(#
)字符在该行的末尾引入注释。这允许模式的组件以更可读的方式进行组织。
与任意小数位数匹配的人为模式:
float_pat = /\A
[[:digit:]]+ # 1 or more digits before the decimal point
(\. # Decimal point
[[:digit:]]+ # 1 or more digits after the decimal point
)? # The decimal point and following digits are optional
\Z/x
float_pat.match('3.14') #=> #<MatchData "3.14" 1:".14">
有许多匹配空白的策略:
- 使用诸如
\s
或的模式\p{Space}
。
- 使用诸如空格之类的空格
\
,即以反斜杠开头的空格。
- 使用诸如
[ ]
.Comments之类的字符类可以通过注释构造包含在非x
模式中,其中注释是由正则表达式引擎忽略的任意文本。正则表达式文本中的注释不能包含未转义的终止符字符.EncodingRegular表达式假定使用源编码。这可以用以下修饰符之一来覆盖。(?#)
-
/
pat/u
- UTF-8
-
/
pat/e
- EUC-JP
-
/
pat/s
- Windows-31J
-
/
pat/n
- ASCII-8BITA
一个正则表达式可以与一个字符串匹配,当它们共享一个编码时,或者正则表达式的编码是US-ASCII并且该字符串的编码与ASCII兼容时。
如果试图在不兼容的编码之间进行匹配, Encoding::CompatibilityError
则会引发异常。
所述Regexp#fixed_encoding?
谓词表示正则表达式是否具有固定编码,即一个与ASCII不相容。正则表达式的编码可以通过提供Regexp::FIXEDENCODING
以下参数的第二个参数 来显式修复 Regexp.new
:
r = Regexp.new("a".force_encoding("iso-8859-1"),Regexp::FIXEDENCODING)
r =~ "a\u3042"
# raises Encoding::CompatibilityError: incompatible encoding regexp match
# (ISO-8859-1 regexp with UTF-8 string)
-
$~
相当于:: last_match;
-
$&
包含完整的匹配文本;
-
$`
包含匹配前的字符串;
-
$'
包含匹配后的字符串;
-
$1
,$2
等等包含与第一,第二等捕获组相匹配的文本;
-
$+
包含最后一个捕获组。
例:
m = /s(\w{2}).*(c)/.match('haystack') #=> #<MatchData "stac" 1:"ta" 2:"c">
$~ #=> #<MatchData "stac" 1:"ta" 2:"c">
Regexp.last_match #=> #<MatchData "stac" 1:"ta" 2:"c">
$& #=> "stac"
# same as m[0]
$` #=> "hay"
# same as m.pre_match
$' #=> "k"
# same as m.post_match
$1 #=> "ta"
# same as m[1]
$2 #=> "c"
# same as m[2]
$3 #=> nil
# no third group in pattern
$+ #=> "c"
# same as m[-1]
这些全局变量是线程局部变量和方法局部变量。
Performance
构建体的某些病态组合可能导致糟糕的表现。
考虑一个25个a_s,一个_d,4个a_s和一个_c的字符串。
s = 'a' * 25 + 'd' + 'a' * 4 + 'c'
#=> "aaaaaaaaaaaaaaaaaaaaaaaaadaaaac"
以下模式可以像您期望的那样立即匹配:
/(b|a)/ =~ s #=> 0
/(b|a+)/ =~ s #=> 0
/(b|a+)*/ =~ s #=> 0
但是,以下模式花费的时间明显较长:
/(b|a+)*c/ =~ s #=> 26
发生这种情况的原因是,正则表达式中的原子可以通过立即+
和封闭来进行量化*
,无需区分哪个字符受控于任何特定字符。结果产生的非确定性会产生超线性性能。(请参阅Mastering Regular Expressions(第三版),第222页,由Jeffery Friedl进行深入分析)。这种特殊情况可以通过使用原子分组来解决,这可以防止不必要的回溯:
(start = Time.now) && /(b|a+)*c/ =~ s && (Time.now - start)
#=> 24.702736882
(start = Time.now) && /(?>b|a+)*c/ =~ s && (Time.now - start)
#=> 0.000166571
下面的例子代表了一个类似的例子,这个例子大约需要60秒来执行:
匹配29个_a_s的字符串与29个可选的_a_s的模式,然后是29个必需的_a_s:
Regexp.new('a?' * 29 + 'a' * 29) =~ 'a' * 29
29个可选的_a_s匹配字符串,但是这阻止了匹配后面的29个必须的_a_s。Ruby必须重复回溯,以便满足尽可能多的可选匹配,同时仍然匹配必需的29.我们很清楚,没有任何可选匹配可以成功,但是这一事实很遗憾地让Ruby无法回避。
提高性能的最佳方法是显着减少所需的回溯数量。对于这种情况,不是单独匹配29个可选的a_s,而是可以使用_a {0,29}一次性匹配一系列可选的_a_s:
Regexp.new('a{0,29}' + 'a' * 29) =~ 'a' * 29
Constants
EXTENDED
see #options and ::new
FIXEDENCODING
see #options and ::new
IGNORECASE
see #options and ::new
MULTILINE
see #options and ::new
NOENCODING
see #options and ::new
Public Class Methods
compile(*args)
Alias for Regexp.new
escape(str) → string Show source
转义任何在正则表达式中具有特殊含义的字符。返回一个新的转义字符串,如果没有字符转义,则返回self。对于任何字符串,将是真实的。Regexp.new(Regexp.escape(str))=~str
Regexp.escape('\*?{}.') #=> \\\*\?\{\}\.
static VALUE
rb_reg_s_quote(VALUE c, VALUE str)
{
return rb_reg_quote(reg_operand(str, TRUE));
}
json_create(object) Show source
通过构建具有源代码s
(Regexp或String)和选项o
序列化的新Regexp对象来反序列化JSON字符串to_json
# File ext/json/lib/json/add/regexp.rb, line 11
def self.json_create(object)
new(object['s'], object['o'])
end
last_match → matchdata Show source
last_match(n) → str
第一种形式返回上次成功模式匹配生成的MatchData对象。相当于读取特殊的全局变量$~
(有关详细信息,请参阅Regexp中的特殊全局变量)。
第二种形式返回此MatchData对象中的第n个字段。_n可以是一个字符串或符号来引用一个命名的捕获。
请注意:: :: last_match对于模式匹配的方法的线程和方法范围是本地的。
/c(.)t/ =~ 'cat' #=> 0
Regexp.last_match #=> #<MatchData "cat" 1:"a">
Regexp.last_match(0) #=> "cat"
Regexp.last_match(1) #=> "a"
Regexp.last_match(2) #=> nil
/(?<lhs>\w+)\s*=\s*(?<rhs>\w+)/ =~ "var = val"
Regexp.last_match #=> #<MatchData "var = val" lhs:"var" rhs:"val">
Regexp.last_match(:lhs) #=> "var"
Regexp.last_match(:rhs) #=> "val"
static VALUE
rb_reg_s_last_match(int argc, VALUE *argv)
{
VALUE nth;
if (argc > 0 && rb_scan_args(argc, argv, "01", &nth) == 1) {
VALUE match = rb_backref_get();
int n;
if (NIL_P(match)) return Qnil;
n = match_backref_number(match, nth);
return rb_reg_nth_match(n, match);
}
return match_getter();
}
new(string, options) → regexp Show source
new(regexp) → regexp
compile(string, options) → regexp
compile(regexp) → regexp
构造一个新的正则表达式pattern
,它可以是一个String或一个Regexp(在这种情况下regexp的选项被传播),并且新的选项可能没有被指定(从Ruby 1.8开始的变化)。
如果options
是一个Integer,它应该是一个或多个常量Regexp :: EXTENDED,Regexp :: IGNORECASE和Regexp :: MULTILINE,或者 -ed在一起。否则,如果options
不是nil
或,则正false
则表达式将不区分大小写。
r1 = Regexp.new('^a-z+:\s+\w+') #=> /^a-z+:\s+\w+/
r2 = Regexp.new('cat', true) #=> /cat/i
r3 = Regexp.new(r2) #=> /cat/i
r4 = Regexp.new('dog', Regexp::EXTENDED | Regexp::IGNORECASE) #=> /dog/ix
static VALUE
rb_reg_initialize_m(int argc, VALUE *argv, VALUE self)
{
int flags = 0;
VALUE str;
rb_encoding *enc = 0;
rb_check_arity(argc, 1, 3);
if (RB_TYPE_P(argv[0], T_REGEXP)) {
VALUE re = argv[0];
if (argc > 1) {
rb_warn("flags ignored");
}
rb_reg_check(re);
flags = rb_reg_options(re);
str = RREGEXP_SRC(re);
}
else {
if (argc >= 2) {
if (FIXNUM_P(argv[1])) flags = FIX2INT(argv[1]);
else if (RTEST(argv[1])) flags = ONIG_OPTION_IGNORECASE;
}
if (argc == 3 && !NIL_P(argv[2])) {
char *kcode = StringValuePtr(argv[2]);
if (kcode[0] == 'n' || kcode[0] == 'N') {
enc = rb_ascii8bit_encoding();
flags |= ARG_ENCODING_NONE;
}
else {
rb_warn("encoding option is ignored - %s", kcode);
}
}
str = StringValue(argv[0]);
}
if (enc && rb_enc_get(str) != enc)
rb_reg_init_str_enc(self, str, enc, flags);
else
rb_reg_init_str(self, str, flags);
return self;
}
quote(str) → string Show source
转义任何在正则表达式中具有特殊含义的字符。返回一个新的转义字符串,如果没有字符转义,则返回self。对于任何字符串,将是真实的。Regexp.new(Regexp.escape(str))=~str
Regexp.escape('\*?{}.') #=> \\\*\?\{\}\.
static VALUE
rb_reg_s_quote(VALUE c, VALUE str)
{
return rb_reg_quote(reg_operand(str, TRUE));
}
try_convert(obj) → re or nil Show source
尝试使用to_regexp方法将obj转换为Regexp。如果因任何原因无法转换obj,则返回转换的正则表达式或零。
Regexp.try_convert(/re/) #=> /re/
Regexp.try_convert("re") #=> nil
o = Object.new
Regexp.try_convert(o) #=> nil
def o.to_regexp() /foo/ end
Regexp.try_convert(o) #=> /foo/
static VALUE
rb_reg_s_try_convert(VALUE dummy, VALUE re)
{
return rb_check_regexp_type(re);
}
union(pat1, pat2, ...) → new_regexp Show source
union(pats_ary) → new_regexp
返回Regexp
给定pattern_s/(?!)/
的并集的对象,即匹配其任何部分。_pattern_s可以是Regexp对象,在这种情况下,它们的选项将被保留,或者字符串。如果没有给出模式,则返回。如果任何给定的_pattern包含捕获,则行为是未指定的。
Regexp.union #=> /(?!)/
Regexp.union("penzance") #=> /penzance/
Regexp.union("a+b*c") #=> /a\+b\*c/
Regexp.union("skiing", "sledding") #=> /skiing|sledding/
Regexp.union(["skiing", "sledding"]) #=> /skiing|sledding/
Regexp.union(/dogs/, /cats/i) #=> /(?-mix:dogs)|(?i-mx:cats)/
注意::: union的参数将尝试通过to_regexp转换为正则表达式字面值。
static VALUE
rb_reg_s_union_m(VALUE self, VALUE args)
{
VALUE v;
if (RARRAY_LEN(args) == 1 &&
!NIL_P(v = rb_check_array_type(rb_ary_entry(args, 0)))) {
return rb_reg_s_union(self, v);
}
return rb_reg_s_union(self, args);
}
公共实例方法
rxp == other_rxp → true or false Show source
平等 - 如果它们的模式相同,则两个正则表达式相等,它们具有相同的字符集代码,并且它们的casefold?
值相同。
/abc/ == /abc/x #=> false
/abc/ == /abc/i #=> false
/abc/ == /abc/u #=> false
/abc/u == /abc/n #=> false
static VALUE
rb_reg_equal(VALUE re1, VALUE re2)
{
if (re1 == re2) return Qtrue;
if (!RB_TYPE_P(re2, T_REGEXP)) return Qfalse;
rb_reg_check(re1); rb_reg_check(re2);
if (FL_TEST(re1, KCODE_FIXED) != FL_TEST(re2, KCODE_FIXED)) return Qfalse;
if (RREGEXP_PTR(re1)->options != RREGEXP_PTR(re2)->options) return Qfalse;
if (RREGEXP_SRC_LEN(re1) != RREGEXP_SRC_LEN(re2)) return Qfalse;
if (ENCODING_GET(re1) != ENCODING_GET(re2)) return Qfalse;
if (memcmp(RREGEXP_SRC_PTR(re1), RREGEXP_SRC_PTR(re2), RREGEXP_SRC_LEN(re1)) == 0) {
return Qtrue;
}
return Qfalse;
}
rxp === str → true or false Show source
案例平等 - 用于案例陈述。
a = "HELLO"
case a
when /^[a-z]*$/; print "Lower case\n"
when /^[A-Z]*$/; print "Upper case\n"
else; print "Mixed case\n"
end
#=> "Upper case"
在使用===运算符的正则表达式之后,可以使用字符串进行比较。
/^[a-z]*$/ === "HELLO" #=> false
/^[A-Z]*$/ === "HELLO" #=> true
VALUE
rb_reg_eqq(VALUE re, VALUE str)
{
long start;
str = reg_operand(str, FALSE);
if (NIL_P(str)) {
rb_backref_set(Qnil);
return Qfalse;
}
start = rb_reg_search(re, str, 0, 0);
if (start < 0) {
return Qfalse;
}
return Qtrue;
}
rxp =~ str → integer or nil Show source
Match—Matches rxp against str.
/at/ =~ "input data" #=> 7
/ax/ =~ "input data" #=> nil
如果=~
与带有命名捕获的正则表达式文字一起使用,捕获的字符串(或无)将被分配给由捕获名称命名的局部变量。
/(?<lhs>\w+)\s*=\s*(?<rhs>\w+)/ =~ " x = y "
p lhs #=> "x"
p rhs #=> "y"
如果不匹配,则为变量分配nil。
/(?<lhs>\w+)\s*=\s*(?<rhs>\w+)/ =~ " x = "
p lhs #=> nil
p rhs #=> nil
这个任务是在Ruby解析器中实现的。解析器检测到赋值的'regexp-literal =〜expression'。正则表达式必须是没有插值的文字,并放置在左侧。
如果正则表达式不是字面值,则不会发生赋值。
re = /(?<lhs>\w+)\s*=\s*(?<rhs>\w+)/
re =~ " x = y "
p lhs # undefined local variable
p rhs # undefined local variable
正则表达式插值#{}
也会禁用分配。
rhs_pat = /(?<rhs>\w+)/
/(?<lhs>\w+)\s*=\s*#{rhs_pat}/ =~ "x = y"
p lhs # undefined local variable
如果正则表达式位于右侧,则不会发生分配。
" x = y " =~ /(?<lhs>\w+)\s*=\s*(?<rhs>\w+)/
p lhs, rhs # undefined local variable
VALUE
rb_reg_match(VALUE re, VALUE str)
{
long pos = reg_match_pos(re, &str, 0);
if (pos < 0) return Qnil;
pos = rb_str_sublen(str, pos);
return LONG2FIX(pos);
}
as_json(*) Show source
返回一个散列,它将变成一个JSON对象并表示这个对象。
# File ext/json/lib/json/add/regexp.rb, line 17
def as_json(*)
{
JSON.create_id => self.class.name,
'o' => options,
's' => source,
}
end
casefold? → true or false Show source
返回不区分大小写的标志的值。
/a/.casefold? #=> false
/a/i.casefold? #=> true
/(?i:a)/.casefold? #=> false
static VALUE
rb_reg_casefold_p(VALUE re)
{
rb_reg_check(re);
if (RREGEXP_PTR(re)->options & ONIG_OPTION_IGNORECASE) return Qtrue;
return Qfalse;
}
encoding → encoding Show source
返回表示obj编码的Encoding对象。
VALUE
rb_obj_encoding(VALUE obj)
{
int idx = rb_enc_get_index(obj);
if (idx < 0) {
rb_raise(rb_eTypeError, "unknown encoding");
}
return rb_enc_from_encoding_index(idx & ENC_INDEX_MASK);
}
eql?(other_rxp) → true or false Show source
平等 - 如果它们的模式相同,则两个正则表达式相等,它们具有相同的字符集代码,并且它们的casefold?
值相同。
/abc/ == /abc/x #=> false
/abc/ == /abc/i #=> false
/abc/ == /abc/u #=> false
/abc/u == /abc/n #=> false
static VALUE
rb_reg_equal(VALUE re1, VALUE re2)
{
if (re1 == re2) return Qtrue;
if (!RB_TYPE_P(re2, T_REGEXP)) return Qfalse;
rb_reg_check(re1); rb_reg_check(re2);
if (FL_TEST(re1, KCODE_FIXED) != FL_TEST(re2, KCODE_FIXED)) return Qfalse;
if (RREGEXP_PTR(re1)->options != RREGEXP_PTR(re2)->options) return Qfalse;
if (RREGEXP_SRC_LEN(re1) != RREGEXP_SRC_LEN(re2)) return Qfalse;
if (ENCODING_GET(re1) != ENCODING_GET(re2)) return Qfalse;
if (memcmp(RREGEXP_SRC_PTR(re1), RREGEXP_SRC_PTR(re2), RREGEXP_SRC_LEN(re1)) == 0) {
return Qtrue;
}
return Qfalse;
}
fixed_encoding? → true or false Show source
如果rxp适用于具有任何ASCII兼容编码的字符串,则返回false。否则返回true。
r = /a/
r.fixed_encoding? #=> false
r =~ "\u{6666} a" #=> 2
r =~ "\xa1\xa2 a".force_encoding("euc-jp") #=> 2
r =~ "abc".force_encoding("euc-jp") #=> 0
r = /a/u
r.fixed_encoding? #=> true
r.encoding #=> #<Encoding:UTF-8>
r =~ "\u{6666} a" #=> 2
r =~ "\xa1\xa2".force_encoding("euc-jp") #=> ArgumentError
r =~ "abc".force_encoding("euc-jp") #=> 0
r = /\u{6666}/
r.fixed_encoding? #=> true
r.encoding #=> #<Encoding:UTF-8>
r =~ "\u{6666} a" #=> 0
r =~ "\xa1\xa2".force_encoding("euc-jp") #=> ArgumentError
r =~ "abc".force_encoding("euc-jp") #=> nil
static VALUE
rb_reg_fixed_encoding_p(VALUE re)
{
if (FL_TEST(re, KCODE_FIXED))
return Qtrue;
else
return Qfalse;
}
hash → integer Show source
根据此正则表达式的文本和选项生成散列。
See also Object#hash.
static VALUE
rb_reg_hash(VALUE re)
{
st_index_t hashval = reg_hash(re);
return ST2FIX(hashval);
}
inspect → string Show source
生成一个很好格式化的字符串版本的rxp。也许令人惊讶的是,#inspect
实际上会产生比字符串更自然的版本#to_s
。
/ab+c/ix.inspect #=> "/ab+c/ix"
static VALUE
rb_reg_inspect(VALUE re)
{
if (!RREGEXP_PTR(re) || !RREGEXP_SRC(re) || !RREGEXP_SRC_PTR(re)) {
return rb_any_to_s(re);
}
return rb_reg_desc(RREGEXP_SRC_PTR(re), RREGEXP_SRC_LEN(re), re);
}
match(str) → matchdata or nil Show source
match(str,pos) → matchdata or nil
返回MatchData
描述匹配的对象,或者nil
不匹配。这相当于在$~
正常匹配之后检索特殊变量的值。如果第二个参数存在,它指定字符串中开始搜索的位置。
/(.)(.)(.)/.match("abc")[2] #=> "b"
/(.)(.)/.match("abc", 1)[2] #=> "c"
如果给出了块,如果匹配成功,则用MatchData调用块,以便写入
/M(.*)/.match("Matz") do |m|
puts m[0]
puts m[1]
end
代替
if m = /M(.*)/.match("Matz")
puts m[0]
puts m[1]
end
返回值是这种情况下块执行的值。
static VALUE
rb_reg_match_m(int argc, VALUE *argv, VALUE re)
{
VALUE result, str, initpos;
long pos;
if (rb_scan_args(argc, argv, "11", &str, &initpos) == 2) {
pos = NUM2LONG(initpos);
}
else {
pos = 0;
}
pos = reg_match_pos(re, &str, pos);
if (pos < 0) {
rb_backref_set(Qnil);
return Qnil;
}
result = rb_backref_get();
rb_match_busy(result);
if (!NIL_P(result) && rb_block_given_p()) {
return rb_yield(result);
}
return result;
}
match?(str) → true or false Show source
match?(str,pos) → true or false
返回a true
或者false
表示是否匹配正则表达式而不更新$〜和其他相关变量。如果第二个参数存在,它指定字符串中开始搜索的位置。
/R.../.match?("Ruby") #=> true
/R.../.match?("Ruby", 1) #=> false
/P.../.match?("Ruby") #=> false
$& #=> nil
static VALUE
rb_reg_match_m_p(int argc, VALUE *argv, VALUE re)
{
long pos = rb_check_arity(argc, 1, 2) > 1 ? NUM2LONG(argv[1]) : 0;
return rb_reg_match_p(re, argv[0], pos);
}
named_captures → hash Show source
返回表示关于rxp的命名捕获的信息的散列。
散列的一个关键是指定捕获的名称。哈希值是一个数组,它是相应命名捕获的索引列表。
/(?<foo>.)(?<bar>.)/.named_captures
#=> {"foo"=>[1], "bar"=>[2]}
/(?<foo>.)(?<foo>.)/.named_captures
#=> {"foo"=>[1, 2]}
如果没有命名捕获,则返回一个空的散列。
/(.)(.)/.named_captures
#=> {}
static VALUE
rb_reg_named_captures(VALUE re)
{
VALUE hash = rb_hash_new();
rb_reg_check(re);
onig_foreach_name(RREGEXP_PTR(re), reg_named_captures_iter, (void*)hash);
return hash;
}
names → name1, name2, ...()
以字符串数组的形式返回捕获的名称列表。
/(?<foo>.)(?<bar>.)(?<baz>.)/.names
#=> ["foo", "bar", "baz"]
/(?<foo>.)(?<foo>.)/.names
#=> ["foo"]
/(.)(.)/.names
#=> []
static VALUE
rb_reg_names(VALUE re)
{
VALUE ary;
rb_reg_check(re);
ary = rb_ary_new_capa(onig_number_of_names(RREGEXP_PTR(re)));
onig_foreach_name(RREGEXP_PTR(re), reg_names_iter, (void*)ary);
return ary;
}
options → integer Show source
返回与创建此Regexp::new
正则表达式时使用的选项对应的位集合(请参阅以了解详细信息。请注意,可在返回的选项中设置其他位:这些位在正则表达式代码内部使用。如果选项为传递给Regexp::new
。
Regexp::IGNORECASE #=> 1
Regexp::EXTENDED #=> 2
Regexp::MULTILINE #=> 4
/cat/.options #=> 0
/cat/ix.options #=> 3
Regexp.new('cat', true).options #=> 1
/\xa1\xa2/e.options #=> 16
r = /cat/ix
Regexp.new(r.source, r.options) #=> /cat/ix
static VALUE
rb_reg_options_m(VALUE re)
{
int options = rb_reg_options(re);
return INT2NUM(options);
}
source → str Show source
返回模式的原始字符串。
/ab+c/ix.source #=> "ab+c"
请注意,转义序列保留原样。
/\x20\+/.source #=> "\\x20\\+"
static VALUE
rb_reg_source(VALUE re)
{
VALUE str;
rb_reg_check(re);
str = rb_str_dup(RREGEXP_SRC(re));
if (OBJ_TAINTED(re)) OBJ_TAINT(str);
return str;
}
to_json(*) Show source
将类名(Regexp)与选项o
和源s
(Regexp或String)一起存储为JSON字符串
# File ext/json/lib/json/add/regexp.rb, line 27
def to_json(*)
as_json.to_json
end
to_s → str Show source
返回一个包含正则表达式及其选项的字符串(使用(?opts:source)
符号),该字符串可以被返回Regexp::new
到正则表达式,其语义与原始语言相同(但是,Regexp#==
在比较两者时,可能不会返回true,作为源的正则表达式本身可能会有所不同,如示例所示)Regexp#inspect
生成一个通常更易读的rxp版本。
r1 = /ab+c/ix #=> /ab+c/ix
s1 = r1.to_s #=> "(?ix-m:ab+c)"
r2 = Regexp.new(s1) #=> /(?ix-m:ab+c)/
r1 == r2 #=> false
r1.source #=> "ab+c"
r2.source #=> "(?ix-m:ab+c)"
static VALUE
rb_reg_to_s(VALUE re)
{
int options, opt;
const int embeddable = ONIG_OPTION_MULTILINE|ONIG_OPTION_IGNORECASE|ONIG_OPTION_EXTEND;
long len;
const UChar* ptr;
VALUE str = rb_str_buf_new2("(?");
char optbuf[5];
rb_encoding *enc = rb_enc_get(re);
rb_reg_check(re);
rb_enc_copy(str, re);
options = RREGEXP_PTR(re)->options;
ptr = (UChar*)RREGEXP_SRC_PTR(re);
len = RREGEXP_SRC_LEN(re);
again:
if (len >= 4 && ptr[0] == '(' && ptr[1] == '?') {
int err = 1;
ptr += 2;
if ((len -= 2) > 0) {
do {
opt = char_to_option((int )*ptr);
if (opt != 0) {
options |= opt;
}
else {
break;
}
++ptr;
} while (--len > 0);
}
if (len > 1 && *ptr == '-') {
++ptr;
--len;
do {
opt = char_to_option((int )*ptr);
if (opt != 0) {
options &= ~opt;
}
else {
break;
}
++ptr;
} while (--len > 0);
}
if (*ptr == ')') {
--len;
++ptr;
goto again;
}
if (*ptr == ':' && ptr[len-1] == ')') {
Regexp *rp;
VALUE verbose = ruby_verbose;
ruby_verbose = Qfalse;
++ptr;
len -= 2;
err = onig_new(&rp, ptr, ptr + len, ONIG_OPTION_DEFAULT,
enc, OnigDefaultSyntax, NULL);
onig_free(rp);
ruby_verbose = verbose;
}
if (err) {
options = RREGEXP_PTR(re)->options;
ptr = (UChar*)RREGEXP_SRC_PTR(re);
len = RREGEXP_SRC_LEN(re);
}
}
if (*option_to_str(optbuf, options)) rb_str_buf_cat2(str, optbuf);
if ((options & embeddable) != embeddable) {
optbuf[0] = '-';
option_to_str(optbuf + 1, ~options);
rb_str_buf_cat2(str, optbuf);
}
rb_str_buf_cat2(str, ":");
if (rb_enc_asciicompat(enc)) {
rb_reg_expr_str(str, (char*)ptr, len, enc, NULL);
rb_str_buf_cat2(str, ")");
}
else {
const char *s, *e;
char *paren;
ptrdiff_t n;
rb_str_buf_cat2(str, ")");
rb_enc_associate(str, rb_usascii_encoding());
str = rb_str_encode(str, rb_enc_from_encoding(enc), 0, Qnil);
/* backup encoded ")" to paren */
s = RSTRING_PTR(str);
e = RSTRING_END(str);
s = rb_enc_left_char_head(s, e-1, e, enc);
n = e - s;
paren = ALLOCA_N(char, n);
memcpy(paren, s, n);
rb_str_resize(str, RSTRING_LEN(str) - n);
rb_reg_expr_str(str, (char*)ptr, len, enc, NULL);
rb_str_buf_cat(str, paren, n);
}
rb_enc_copy(str, re);
OBJ_INFECT(str, re);
return str;
}
~ rxp → integer or nil Show source
匹配rxp与内容匹配$_
。相当于。rxp
=~ $_
$_ = "input data"
~ /at/ #=> 7
VALUE
rb_reg_match2(VALUE re)
{
long start;
VALUE line = rb_lastline_get();
if (!RB_TYPE_P(line, T_STRING)) {
rb_backref_set(Qnil);
return Qnil;
}
start = rb_reg_search(re, line, 0, 0);
if (start < 0) {
return Qnil;
}
start = rb_str_sublen(line, start);
return LONG2FIX(start);
}
Regexp |
---|
Ruby 是一种面向对象、命令式、函数式、动态的通用编程语言,是世界上最优美而巧妙的语言。
主页 | https://www.ruby-lang.org/ |
源码 | https://github.com/ruby/ruby |
版本 | 2.4 |
发布版本 | 2.4.1 |