参考:
aardio 模式串是描述字符与字符串特征的文本,用于字符串查找、替换等操作中执行特定语义的模式匹配。
模式串由主要由表示字面值的字符(literal characters)、表示特定文本集合的子模式(Subpattern)、表示特定匹配规则的模式运算符(operators)、以及分组匹配结果捕获组(capturing groups)与非捕获组(Non-capturing group)组成。
模式匹配的语法与正则表达式类似,但比正则表达式的语法更为简洁,运行速度也快很多。可以有效地避免在使用正则表达式时复杂度无限上升,性能降低等问题。
另外正则表达式通常用于匹配文本,而 aardio 模式表达式天然适合处理 UTF-8 文本与任意二进制字符串。
aardio 模式匹配与正则表达式一个较大的区别是 aardio 严格区分描述字符集合的子模式( Subpattern )与描述匹配规则的运算符(operators),并且"模式运算符"只能用于"子模式"(包含非捕获组)不能用于捕获组。而正则运算符没有这些限制,可以用于捕获组。
子模式描述字符或字符串的特征并表示特定匹配目标,用于在目标文本中匹配特定字符、字符串序列、锚点位置。
子模式是 aardio 模式匹配的最小匹配单位。子模式的一个主要特征是具有原子性(atomic),子模式是匹配链中的原子节点,也是回溯的原子节点。
子模式:
普通单字节字符表示的字面值。
多字节字符(例如中文)不是最小模式匹配单位(子模式),除非将多字节字符放入中括号(自定义字符类)。或者用元序列也可以将多个字节转换为最小模式匹配单位(子模式)。
特殊字符( special characters )
有特殊模式语义的特殊符号可以用 转义符 转换为字面值。
注意:在 aardio 模式匹配中非捕获组是子模式,而捕获组不是子模式,不能对捕获组使用任何模式运算符。这是与正则表达式的最大区别。
在 aardio 中运算符是指用于描述或改变匹配规则的特殊符号。
运算符默认只能用于子模式。
以限定子模式匹配一次或多次量词运算符 +
为例:
a+
表示匹配字符 a 一次或多次,用法正确。[a-z]+
表示匹配小写字母一次或多次,用法正确。(abc)+
错误, "捕获组"不是"子模式",不能使用运算符。<abc>+
正确, "元序列"虽然匹配多个字符,但属于"子模式",可以使用运算符。不能对捕获组或使用模式运算符,这是"aardio 模式匹配"与"正则表达式"最大的一个区别。
例如正则表达式 (hello|world)
,在模式匹配中等价的写法为 (<hello>|<world>)
。这是因为多个字符只有放入尖括号中创建元序列(非捕获分组)才会变成一个子模式,aardio 模式运算符只对子模式有效( 在模式匹配里捕获组不是子模式 )。
在 aardio 模式匹配中区分子模式与运算符是非常重要的。例如匹配首尾锚点的 ^
与 $
是子模式,而用于边界断言的 !
却是运算符。虽然都是匹配边界但性质不同,^
与 $
只是直接匹配特定的目标,而 !
是作用于其他子模式以改变匹配规则。
请参考:运算符列表
在模式串中可以使用一对圆括号 ()
将模式子串包含在内以创建捕获组。
捕获组记录匹配的内容,在模式串后面可以使用 \1
到 \9
反向引用之前捕获组的匹配结果。
对于string.match string.gmatch 等模式匹配函数,每个捕获组都会增加一个返回的字符串值。如果没有任何捕获组,则第一个返回值为匹配的全部字符串,否则第一个返回值为第一个捕获组。
在替换函数的回调函数中,每个捕获组都会增加一个字符串参数。如果没有任何捕获组,则第一个回调参数为匹配的全部字符串,否则第一个回调参数为第一个捕获组。
可使用不包含模式子串的空捕获组 ()
记录并返回该分组所在位置(以字节计数)。
捕获组不具有原子性,内部允许回溯。对捕获组也不能使用任何模式运算符(这是与正则表达式最大的一个区别)。
捕获组可以包含子模式与运算符,而子模式不能包含捕获组。
捕获组可以嵌套。
捕获组引用 #
在模式串中也可以使用
\1
到\9
向前引用捕获组,在\
后面用单个数字指定捕获组序号 - 也就是在整个模式串中创建捕获组的左括号(
出现的前后顺序。例如(a(bc))\2
匹配 "abcbc",从前向后数(
出现的位置 ,\2
反向引用的是第 2 个(
创建的捕获组为(bc)
。在查找模式串中可以引用的捕获组序号为\1
到\9
,而在替换字符串除了可以使用\1
到\9
引用捕获组,还中可以用\0
表示匹配到的整个字符串。默认不能对捕获组引用使用任何运算符,但一个例外是可以在元序列内对捕获组引用使用运算符。例如
(abc)<\1+>
这里面的+
是对捕获组引用\1
正确使用模式运算符,但(abc)\1+
这里面的+
并不会解析为模式运算符(仅匹配+
的字面值)。
使用尖括号包含模式子串以创建非捕获组,格式为 <subpattern>
,这在 aardio 中称为元序列( Metasequence ) 。
要特别注意:
在 aardio 模式匹配中非捕获组有重要的意义。这是因为除了元序列与对称匹配以外其他子模式与模式运算符都只能匹配单个字符,而元序列则能够组合一组子模式以匹配连续的字符,并且对这一组匹配结果可以使用其他的模式运算符,这极大地扩展了模式匹配的功能。
在 aardio 模式匹配中『模式转义符』为反斜杆 \
。
模式串中的 \
用于将表示字面值的普通字符转换为特殊的模式符号。对于特定的模式符号,在前面加上转义符 \
,可用于表示普通的字面值字符。
转义符的作用:
在特殊符号不具有特殊模式语义时可以不用转义,例如:
+*?!{}<>()%&|
等在 []
内部不具有特殊语义的运算符表示字面值。{}()!%&
等在 <>
内部不具有特殊语义的运算符表示字面值!
后面只能是子模式,不能构建子模式的特殊符号不用转义,包括 !
自身也不用转义。%
后面只能是子模式,不能构建子模式的特殊符号不用转义,包括 %
自身也不用转义。不需要转义的字符前面加 \
转义会被忽略(不会报错)。
要特别注意区分"模式转义符"与"字符串转义符"。
\
转义符,例如'\n'
表示换行符。如果将模式串写在单引号中,模式转义符 \
要写为 \\
,例如 '\\d+'
。\
可以直接写在原始字符串中,例如 "\d+"
。这与其他 C 系语言不同,要注意区别。 "字符串转义符"是在 aardio 代码编译过程中处理的。而 "模式转义符"是在程序运行时处理的。对于 aardio 代码,写为 '\\d+'
或者写为 "\d+"
在编译后得到的都是同一个字符串。
参考:
圆点 .
表示任意单字节。
冒号 :
表示 UTF-8 编码文本中的任意多字节字符(例如中文字符) 。
:
在任何时候都表示子模式,除非用转义符取消转义( \:
)才表示原始冒号的字面值。
.
在中括号包含的自定义字符类里表示字面值,因为 [.]
这样表示任意字符是无意义的。
使用 \ + 特定字母字符
表示预定义的字符类,
\a
字母\d
数字\i
ASCII 字符( 字节码 < 0x80 )\l
小写字母\u
大写字母\p
标点字符\s
空白符\u
大写字母\w
字母和数字、以及下划线\x
十六进制数字\n
换行符\r
回车符\t
匹配一个制表符 '\x09'
\v
匹配一个垂直制表符 '\x0b'
\f
换页符 '\x0c'
\z
表示'\0'
,也就是字节码为 0 的文本终止符注意模式串本身必须是纯文本,不能包含字节码为 0 文本终止符。但匹配目标字符串支持二进制,可以用 \z
匹配字节码为 0 的字符。
上面字符类的大写形式表示取反,代表小写字符类所代表的集合的补集。
\A
不是字母\C
不是控制字符\D
不是数字\I
不是 ASCII 字符( 字节码 >= 0x80
)\L
不是小写字母\U
不是大写字母\P
不是标点 \S
不是空白符\U
不是大写字母\W
不是字母和数字、并且不是下划线 \X
不是十六进制数字\N
不是换行\R
不是回车符\T
不是制表符 \V
不是垂直制表符\F
不是换页符\Z
不是'\0'
的字符以上所有预定义字节类都只匹配单个字节,遇到多字节字符(例如中文)也只能匹配单个字节。
在双引号或反引号包含的 原始字符串 中可以直接写这些预定义的字符类,例如 "\d"
。
在单引号包含的 转义字符串 中预定义字符类要注意反斜杆要写两次,例如,例如 '\d'
。
中括号用于创建自定义的字符类,用于匹配与其中任意一个条件匹配的字符。
自定义字符类可以包含以下内容:
[\d\w]
。[a-z]
表示所有小写字母,[0-7]
则等同于 [01234567]
。:
,需要用 \:
表示原始冒号字面值。.
在自定义字符类中表示字面值而不是任意字节。+*?!{}<>()%&|
等在 []
内部不具有特殊语义的运算符表示字面值。示例:"[abc0-9\n]"
在自定义字符类内部开始处可以用 ^
表示取反义的补集。
例如 [^a-z]
匹配任何不是小写字母的单字节字符。
注意自定义字符类补集与自定义字符类的其他规则一样,.
仍然表示字面值。
自定义字符类补集中:
仍然表示多字节字符。但自定义字符补集中的:
或多字节字符只要遇到的不是多字节字符的第一个字节就总是能匹配成功,因为模式匹配本身是基于二进制的。这个看起来是局限的特性实能实现一各巧妙的预测多字节字符边界(中文字符边界)的效果,例如模式串 [^”]+”
它就可以一直向前推进直到遇到中文双引号的首字节,而 <“[^”]+”>
可以匹配双引号内的字符串。
元序列用于在模式串中创建一个非捕获分组( Non-capturing group )。
在元序列内部可以使用其他子模式,并且这些模式被重新组合为一个新的子模式( Subpattern )。
元序列同时也是具有原子性质的分组(Atomic Group),在元序列内部不会记录之前的匹配位置也不支持回溯。
语法示例:
<abc0-9\n\d+:+>
尖括号创建的元序列匹配一组有序的字符串而不是单个字符。例如<hello>
匹配 "hello" 单词。而不是匹配 "hello" 其中的一个字符。
元序列可以包含的基本子模式与自定义字符类相似:
<\d\w>
。<a-z>
表示所有小写字母。:
,需要用 \:
表示原始冒号字面值。.
在元序列内表示任意单字节字符,而在自定义字符内表示字面值。这一点是不同的,请注意区别。{}()!%&
等在 <>
内部不具有特殊语义的运算符仅表示字面值(不需要转义)。元序列内部还支持以下模式语法:
<hello<world>>
。<[a-z0-9]>
。|
,以及 +
*
?
等限定匹配次数的运算符(贪婪匹配,且不会回溯),但不允许在元序列内使用表示惰性匹配的 +?
*?
运算符。?=
,?!
。!
%
, 也支持附加 ?
后缀的最近对称匹配。+
*
?
|
运算符。例如 (abc)<\1+>
。注意在元序列外部不能对捕获组引用使用任何模式运算符。注意不能在元序列内部创建捕获组,但是可以引用在之前创建的其他捕获组。
要特别注意在元序列内部执行贪婪而激进的匹配规则,只要能匹配到内容就会尽力向后走并且不会回溯(这在需要局部禁止回溯时非常有用)。例如我们试图用 <“.+”>
匹配中文双引号包含的字符串总是会失败,因为在元序列内部 .+
会一直向前推进,直到消费掉所有符合它自己要求的字节,失败也不会回溯。解决的方法是改为有边界限制的匹配,例如 <“[^”]+”>
就可以匹配成功,自定义字符补集[^”]+
只会消费不是中文双引号的字符。
元序列自身作为一个子模式可以支持回溯与惰性匹配,例如 <abc>+?
。
在正则表达式中与元序列具有类似特性的非捕获组与原子分组都是常见的性能优化分组,但元序列有更严格的限制,运行速度也更快。aardio 通过『元序列』限制并避免了在使用模式匹配时复杂度无限上升、并以此换取最大化的匹配速度(模式匹配比正则表达式快数十倍)与更精简更可控的匹配语法。
元序列的原子性(不回溯)与激进性(无惰性)不但能提供更好的性能,有时候可以解决一些复杂的匹配问题。
举个例子: #
import console;
var p = "<endif>|<end if>|<end><\s+else>?!"
console.log( string.match("if ... end",p) )
console.log( string.match("if ... endif",p) )
console.log( string.match("if ... end else",p) )
console.log( string.match("if ... end if else",p) )
console.pause();
在上面的示例中,我们期望匹配 "endif","end if","end" 三个关键词后面都没有出现 "else" 的字符串。
测试上面的代码,我们意外地发现 "end if else" 总是能匹配成功。这是因为模式 <endif>|<end if>|<end>
遇到字符串 "if ... end if else" 以后,首先匹配 <endif>
成功,然后发现后面有不应该出现的 else
,于是回溯到原来的位置匹配 <end>
成功,这时候出现在它后面是if
而不是 else
于是机巧地利用回溯陷阱
返回了我们不需要的结果。
解决方法是将『逻辑或匹配』放到另外一个元序列内部,将上面的 (endif|end if|end)
改为 <<endif>|<end if>|<end>>
就可以得到正确的结果。这利用了元序列的原子特性,在元序列内部匹配成功就不会因为后续的匹配失败而回溯重试
, 这不但提供了更好的性能,也能帮助我们排除不需要的结果。
示例语法:
<^a-zA-Z\d*>
反义元序列可以包含的内容与元序列相同,但是在元序列内部最前面要加上 ^
表示取反匹配,用于匹配等长就不匹配设定内容的字符串。
例如 <^hello>
匹配所有不是 "hello" 并且等长的字符串。
示例语法:
<@任意内容@>
@
放在尖括号内部首尾两端,表示禁用模式语法以匹配一个原始子串。
这是 aardio 模式语法中非常有用的一个特殊功能,它可以在一个查找串中对部分字符串暂时局部禁用模式语法。
例如 <@a-zbc@>
匹配"a-zbc",而不能匹配"abc"。
举个具体的例子。
如果我们需要查找 A,B,C 三段文本组成的块,其中 A 为开始段,而 C 为结束段, B 是任意字符。我们用 .+
的模式串表示中间的B段,而 A 和 C 包含大量的标点符号,我们不希望在这两处使用模式语法,只是希望直接查找开始文本与结束文本。
这在批量处理文本时经常遇到,要么细心地编写查找模式,一个字符地处理转义,或者编写大量的代码来实现该逻辑,先查出开始段,再找出 ………… 总之是很麻烦的一件事。
在模式匹配中使用临时禁用模式语法的原始元序列 <@任意内容@>
能彻底解决这一难题,使模式语法的使用更为简单。因为我们可以按需禁用模式语法,查找速度也会显著提升。
如果元序列以两个 @@
字符开始写为 <@@任意内容@>
表示忽略大小写匹配一个原始子串。如果是使用本就大小写不敏感的 string.cmpMatch 函数就没必要这样写了。
子模式 | 说明 |
---|---|
^ |
文本开始锚点 |
$ |
文本尾部锚点 |
示例:
import win;
var str = "1234";
if( string.find(str, "^\d") ) {
win.msgbox(str + " 字符串以数字开始")
}
if( string.find(str, "^[+-]?\d+$") ) {
win.msgbox(str + " 字符串是一个整数")
}
模式匹配中的运算符是指用于描述或改变匹配规则的特殊符号。运算符默认只能用于子模式( Subpattern )。
量词运算符用于指定一个子模式的匹配次数。
下面的表格中用 p 表示一个 子模式( Subpattern )。
贪婪匹配量词 | 说明 |
---|---|
p{min,max} |
最长匹配子模式 p 最少 min 次,最多 max 次, min,max 都可以是多个数字表示的数值。 可以省略其中一个参数,或者仅用一个参数限定匹配长度。 例如: p{min,} p{,max} p{len} |
p+ | 最长匹配子模式 1次或多次,等价于{1,} |
p* | 最长匹配子模式 0 次或多次,类似{0,} |
p? | 最长匹配子模式 1 次或 0 次,类似{0,1} |
用于指定匹配次数的运算符默认为**贪婪匹配**模式,**贪婪匹配**又称为最长匹配,默认会在指定语义下尽可能获得最长的匹配结果。
如果在 {min,max}
,+
,*
这些运算符后加一个 ?
号可以生成三个新的惰性匹配运算符 {min,max}?
,+?
,*?
。惰性匹配运算符保持原来的语义,但会在指定语义下做尽可能短的匹配。
惰性匹配量词 | 说明 |
---|---|
p{min,max}? |
最短匹配子模式 p 最少 min 次,最多 max 次, min,max 都可以是多个数字表示的数值。 可以省略其中一个参数,或者仅用一个参数限定匹配长度。 例如: p{min,} p{,max} p{len} |
p+? | 最短匹配子模式 1次或多次,等价于{1,} |
p*? | 最短匹配子模式0次或多次,类似{0,} |
惰性匹配又称为最短匹配、非贪婪匹配。
惰性匹配的速度更快。
例如,对于字符串 "aaaaaa",模式串 a+?
将匹配单个 "a",而模式串 a+
将匹配所有 'a'。
import console;
var str = "a1234z"
//以?号结束的运算符表示最短匹配
str2 = string.match(str, "a\d*?")
console.log(str,"a\d*?",str2);
//最长匹配
str2 = string.match(str, "a\d*")
console.log(str,"a\d*",str2);
//最长匹配
str2 = string.match(str, "a\d+\d")
console.log(str,"a\d+\d",str2);
//以 ? 号结束的运算符表示最短匹配
str2 = string.match(str, "a\d+?\d")
console.log(str,"a\d+?\d",str2);/*显示 a12*/
//最长匹配
str2 = string.match(str, "a\d{2,3}")
console.log(str,"a\d{2,3}", str2); //显示 a123
//以 ? 号结束的运算符表示最短匹配
str2 = string.match(str, "a\d{2,3}?")
console.log(str,"a\d{2,3}?",str2); //显示 a12
console.pause()
断言都是零宽断言,不消费任何字符宽度。
零宽断言主要分为两种:
模式匹配支持的预测断言语法:
运算符 | 说明 |
---|---|
p?= | 预测断言,测试子模式 p 是否可匹配1次,不消费任何字符宽度 |
p?! | 反预测断言 ,测试模式 p 是否不匹配至少 1 次,不消费任何字符宽度 |
断言与量词运算符的作用是类似的,断言虽然不会实际消费字符宽度,但仍然是用于判定匹配结果的数量。
预测断言可以认为是量词运算符 ?
的变体:
p?
允许匹配子模式 p 一次或零次。p?=
则要求匹配子模式 p 至少一次,且不会消费字符宽度。p?!
则要求不匹配子模式 p 至少一次,且不会消费字符宽度。这个 ?
在这里的寓意就是“询问有没有”某个子模式,而 =
寓意为条件相符,!
寓意为条件取反。
在正则表达式这两个零宽断言是用于捕获分组的,语法为:
(?=p)
预测断言
(?!p)
反预测断言
aardio 里断言运算符只能用于子模式,可以将断言用于元序列内部的子模式或元序列本身,,但不能对捕获组使用预测断言运算符。
语法: !p
边界断言用一个感叹号加一个子模式组成。
感叹号后面必须是子模式,如果不是可以构建子模式的特殊符号则表示字面值可以不用转义。例如 !(\(
或 !{\{
用于匹配单个括号的字面值,甚至 !!\!
这样写也是可以的。
字面上理解,与编程一样这里的 !
是逻辑取反的意思。
!p
的检测规则如下:
!p
首先是一个回顾断言,它向字符串开始的方向回顾匹配程序刚刚经过的字符串是否不符合 p 子模式指定的条件。!p
同时也是一个预测断言,用于判断当前位置是从不满足该匹配条件(边界左侧)切换到满足该条件(边界右侧)的字符串分界。!p
在字符串首尾两端只检测有字符的内侧边界:
!p
的回顾断言总是成功,只做预测断言。!p
的预测断言总是成功,只做回顾断言。边界断言是一种左右双向的零宽断言(Zero-width Assertions)语法,匹配时只是做断言检测,但并不消费任何字符宽度,也就是说下一次匹配仍然是从当前位置开始。
!p
类似正则表达式的 Lookaround(Lookahead and Lookbehind) 特性,可以用正则表达式 (?<!p)(?=p)
描述类似的语义 可以理解为一个向左回顾断言(逻辑取反) + 向右预测断言。
例如 !\w\a\w*)
表示单词边界。该边界左侧不能是字母数字,右侧必须是字母数字,并且必须以字母开头,下面看一个完整的正则与模式匹配对比的示例:
import console;
//测试字符串
var str = "abc 3ddeadsfasd dfa123 qerqwe"
import preg;
//正则表达式回顾断言、预测断言
var regex = preg("(?<!\w)(?=\w)([a-zA-Z]\w*)");
for word in regex.gmatch( str ) {
console.log("正则表达式:", word )
}
regex.free();
//模式匹配边界断言
for word in string.gmatch( str,"!\w([a-zA-Z]\w*)") {
console.log("模式匹配:", word )
}
console.pause();
可以看到正则表达式、模式匹配的匹配结果是一样的,
如果我们希望匹配一个特定单词的首尾锚点,可以在首尾写两个相反的边界断言。
示例:
import console;
//测试字符串
var str = "hello hello123 123hello hello "
//模式匹配边界断言
for word in string.gmatch( str,"!\whello!\W") {
console.log("模式匹配:", word )
}
console.pause();
边界断言可以用于子模式,也可以用于元序列内部的子模式或元序列本身,但不能对捕获组使用边界断言运算符。
逻辑或运算符 #
在两个或多个子模式中间添加一个|
字符以表示匹配其中任意一个子模式。
注意: |
两侧不能都是表示字面值的普通字符,A|B
必须改为'[AB]'。
逻辑或运算符在元序列外支持回溯,但在元序列内不支持回溯并总是返回最早匹配到的结果。
例如
string.match("https://","<<http>|<https>>\://")
会因为逻辑或在元序列内总是返回 http 而不是 https 而无法成功。而将更长的<https>
移到前面写为string.match("https://","<<https>|<http>>\://")
, 或者将逻辑或从元序列内部移出来写为string.match("https://","<http>|<https>\://")
,都能正确匹配 "https://" 或 "http://" 。
逻辑与匹配 #
在一个或多个子模式中间添加一个&
字符以表示目标字符或字符串必须匹配所有子模式,匹配结果将是其中最长的匹配子串。
示例:
import console
//匹配结尾不能为英文标点或中文字符,返回值应当为null
var abc = string.match( "123ABC.",".+\P&\i$")
//匹配结尾不能为英文标点或中文字符,返回值应当为"456cde"
var cde = string.match( "456cde",".+\P&\i$")
console.log(abc,cde);
console.pause();
%
用来匹配两个对称的子模式及其位置区间内包含的字符串。
例如:
%{}
匹配以 {
开始, 以 }
结束的字符串。%()
匹配以 (
开始, 以 )
结束的字符串。%""
匹配以引号开始, 以引号结束的字符串。%
后面的符号如果不是构建子模式的符号可以不用转义。
{
、}
、(
、)
不是构建子模式的符号,所以可以直接写。
甚至可以 %%%
这样指定以%
的字面值做对称匹配。
%
后面可以写两个子模式,不限于单个符号,例如:
%<begin><end>
匹配以 begin
开始, 以 end
结束的字符串。%[a-z][0-9]
匹配以字母开始, 以数字结束的字符串。%\a\d
同样匹配以字母开始, 以数字结束的字符串。示例代码:
import console;
var str = 'a = (a(b)cd) '
var str2 = string.match(str, '%()')
//显示 (a(b)cd)
console.log(str2);
console.pause();
对称匹配也可以在后面附加问号 ?
以表示『最近对象匹配』,也就是在多层嵌套时尽可能取最里面一层的对称匹配。
例如:
import console;
var str = 'a = (a(b)cd) '
var str2 = string.match(str, '%()?')
//显示 (b)
console.log(str2);
console.pause();
『最近对称匹配』虽然返回最短的匹配结果。
只能对子模式使用对称匹配运算符。可在元序列内部使用对称匹配,也可以对元序列本身使用对称匹配,但不能将对称匹配运算符施加于捕获组。
局部禁用模式语法
以<@
开始,并以@>
结束的"原始元序列"用于在模式匹配中进行原始的字符串比较,并在匹配目标子串时暂时局部禁用模式匹配语法。
始元序列以<@@
开始,并以@>
结束则表示忽略大小写进行原始的字符串比较。
示例:
var str = string.match("a\d", "[a-z]<@\d@>")
参考: 元序列
全局禁用模式语法
如果在模式串的最开始处加上 @
字符,则表示模式串完全禁用模式匹配语法,而使用更快速的原始字符串查找替换功能。这时候在替换函数中,替换字符串里只能是普通字符串,替换字符串里不解析捕获组引用语法,也不能使用替换函数、替换表等基于模式语法的替换对象。
如果在模式串的最开始处加上 @@
字符,同样禁用所有模式语法,并且在进行原始的字符串查找替换操作时忽略大小写。
实际上这种用法 aardio 在内部将其转换为了单个的原始元序列。
例如 @\dabc
会被转换为 <@\dabc@>
。
而 @@\\dabc
会被自动转换为 <@@\dabc@>
。
所以这个功能只是一个语法糖。
例如:
var str = string.match("a\d", "@a\d");
上面的 \d
并不匹配数字,仅仅是匹配原始的 \d
字面值,等价于:
var str = string.match("a\d", "a\\d");
'\0'
为文本终止符。但模式匹配的目标字符串可以是二进制的,在模式串中可以用 "\z"
表示 '\0'
。|
两侧不能都是表示字面值的普通字符,例如A|B
必须改为'[AB]'。:
是一个子模式。aardio 模式匹配基本的语法尽可能沿用了正则表达式常用的一些基本语法,但模式匹配比正则表达式更简单、运行速度也更快。
测试数据: string.regex 耗时 455 毫秒的查询,preg 库正则表达式需要 49 毫秒,而模式匹配仅需要 1.4 毫秒。
模式匹配与 aardio 语言完全融为一体,与字符串有关每函数大多默认就支持模式匹配语法。
正则表达式一个强大的功能就是可以对圆括号指定的捕获组指定匹配次数,而模式匹配并不支持此功能。模式匹配只能对表达式中的最小单位“子模式”使用运算符设定匹配次数,而捕获组在模式匹配里不属于子模式(捕获组的唯一作用仅仅就是捕获记录并返回匹配子串)。
为了可以对一个子字符串设定匹配次数 - 模式匹配提供了元序列语法以创建非捕获分组。元序列可将多个子模式组合为新的子模式并且于匹配一个长度不限的字符序列。在元序列内部可以支持大多数其他模式语法(不能在元序列内创建捕获组,但可以引用捕获组)。元序列可以嵌套元序列,可以对元序列应用在他模式运算符。
元序列是非捕获分组,也是原子分组,这两者在正则表达式中都属于性能优化分组,而元序列的速度更快。aardio 通过『元序列』限制并避免了在使用模式匹配时复杂度无限上升、并以此换取最大化的匹配速度以及更精简更可控的匹配语法。
模式匹配在元序列可以使用@
字符局部串禁用模式语法,或指定局部串忽略大小写,正则表达式无此功能。
模式匹配运算符 %
用于匹配成对出现的符号及包含的字符串,正则表达式没有类似的语法。
正则表达式使用 \b
表示单词边界,模式匹配提供的边界断言运算符可以作用于子模式实现更灵活的双向零宽断言以及边界测试。
aardio 提供了预测断言 (p?=
与反预测断言 p?!
运算符。 类似正则的预测断言 (?=p)
与反预测断言 (?!p)
。aardio 没有提供单纯的单纯的回顾断言,边界断言则需要同时做双向的回顾断言与预测断言。
正则表达式中 \u
表示 Unicode 编码,而模式匹配中 \u
表示大写字符, \l
表示小写字符 。 aardio 的 转义字符串 支持 \u
表示 Unicode 编码,但转义字符串是在编译时处理转义,这与模式匹配无关。
正则表达式可以表示 Unicode 字符范围,在 aardio 模式匹配中不允许用多字节字符表示宽字节码范围。
模式匹配与正则的最大匹别是正则表达式强大且复杂,模式匹配更简单、小、轻、快,并易于掌握和使用。很多时候我们要避免“正则表达式甜蜜陷阱”,避免执着于用正则表达式解决复杂问题,把正则表达式实现的过于复杂。把所有的需求试图用一个正则表达式去解决,这是一种非常原始的“写命令行”的思维,并非解决问题的最佳方案。
很多时候更好的解决方案是去编写代码替换复杂的模式串,在 aardio 中文本分析是非常方便的,例如标准库中 string.xml 的源码大家可以看看,里面虽然用到了模式匹配,但并非用一两个模式匹配就能搞定这种复杂的文本分析。 其它的可以看看 bencoding.decoder,string.csv 等等支持库的源代码,这些里面基本没有使用或很少使用模式匹配。
我们经常看到新手在询问我这个文本分析的想法怎么用一个模式匹配解决?解决了以后我现在有新的更复杂的想法了,我该怎么把模式串改的更复杂以解决新的问题? 实际上当你把模式匹配越写越复杂的时候,你就要提醒自己可能掉入“正则表达式甜蜜陷阱”了。
当然 aardio 同样也支持正则表达式,提供了 string.regex,preg 等正则表达式支持库。
使用惰性匹配避免不必要的回溯。
多数情况下,在匹配次数运算符后面加上?
会更快,这一点非常重要。贪婪匹配会首先尝试最大长度的匹配,匹配失败会向后回溯重试,类似 (.*)test
这样过多且不必要的回溯是非常低效的。
限制严格的模式串比一个限制宽松的模式更快。
例如模式串 (.*)test
用来获取 test 以前的全部字符,这个条件太过于宽松,查找时会从目标串的第一个字符开始匹配直到字符串结束,如果没有找到,则从目标串的第二个字符开始再次查找。这样的查找效率是很低的。
解决办法是在模式串最前面加上 ^
限定只在字符串开始锚点匹配,例如 ^(.*)test
。改用惰性匹配 ^(.*?)test
找到第一个 test 就停会更快。
严格限制子模式序列的开始字符可以显著提升效率,道理同上。
不要滥用可能匹配空字符串的模式串,例如\a*
。