参考:
aardio 模式串是描述字符与字符串特征的文本,用于字符串查找、替换等操作中执行特定语义的模式匹配。
模式串由主要由表示字面值的字符(literal characters)、表示特定文本集合的子模式(Subpattern)、表示特定匹配规则的模式运算符(operators)、以及分组匹配结果捕获组(capturing groups)与非捕获组(Non-capturing group)组成。
模式匹配的语法与正则表达式类似,但比正则表达式的语法更为简洁,运行速度也快很多。可以有效地避免在使用正则表达式时复杂度无限上升,性能降低等问题。
另外正则表达式通常用于匹配文本,而 aardio 模式表达式天然适合处理 UTF-8 文本与任意二进制字符串。
aardio 模式匹配与正则表达式一个较大的区别是 aardio 严格区分描述字符集合的子模式( Subpattern )与描述匹配规则的运算符(operators),并且"模式运算符"只能用于"子模式"(包含非捕获组)不能用于捕获组。而正则运算符没有这些限制,可以用于捕获组。
子模式描述字符或字符串的特征并表示特定匹配目标,用于在目标文本中匹配特定字符、字符串序列、锚点位置。
子模式是 aardio 模式匹配的最小匹配单位。子模式的一个主要特征是具有原子性(atomic),子模式是匹配链中的原子节点,也是回溯的原子节点。
子模式:
<>内部构成非捕获组,这些都是子模式(并且能包含中文等多字节字符)。有特殊模式语义的特殊符号可以用 模式转义符 转换为字面值。
注意: aardio 模式匹配中 <> 包围的非捕获组是子模式(可以对其使用量词等运算符),但 () 包围的捕获组不是子模式(不能对其使用量词等运算符),这是 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+这里面的+并不会解析为量词运算符(仅匹配+的字面值)。
使用尖括号包含模式子串以创建非捕获组( Non-capturing group ),格式为 <subpattern>。
要特别注意:
在 aardio 模式匹配中非捕获组有重要的意义。这是因为除了非捕获组与对称匹配以外其他子模式与模式运算符都只能匹配单个字符,而非捕获组则能够聚合一组子模式以匹配连续的字符,并且对这一组匹配结果可以使用其他的模式运算符,这极大地扩展了模式匹配的功能。
在 aardio 模式匹配中『模式转义符』为反斜杠 \。
模式串中的 \ 用于将表示原始字面值的普通字符转换为特殊的模式符号,或将特殊的模式符号转换为仅匹配原始字面值的普通字符。
转义符的作用:
\ + 特定字母字符表示特定的字符类,例如用 \d 匹配数字。\ + 标点符号表示标点符号字面值本身(即取消原来的模式语义),例如 \\ 匹配原始反斜杆, \< 、 \> 匹配原始尖括号。\ + 数字表示向前引用捕获组。 在特殊符号不具有特殊模式语义时可以不用转义,例如:
+*?!{}<>()%&|等在 [] 内部不具有特殊语义的运算符表示字面值。{}()!%&等在 <> 内部不具有特殊语义的运算符表示字面值! 后面只能是子模式,不能构建子模式的特殊符号不用转义,包括 !自身也不用转义。% 后面只能是子模式,不能构建子模式的特殊符号不用转义,包括 %自身也不用转义。不具有特殊模式语义的字符前面加或不加 \ 是等价的,例如模式串 \, 等价于 , 。
要特别注意区分模式串( Patterns ) 的"模式转义符"与转义字符串( escaped string literals )的"编译转义符"。建议总是将模式匹配写在双引号或反引号包围的原样字符串( raw string literals )内部,在单引号包围的转义字符串内则必须用双反斜杆将编译转义符转换为模式转义符,例如模式串 '\\<title\\>.*?\\</title\\>' 与 "\<title\>.*?\</title\>" 是等价的。
参考:
圆点 . 表示任意单字节。
冒号 : 表示 UTF-8 编码文本中的任意多字节字符(例如中文字符) 。
: 在任何时候都表示子模式,除非用转义符取消转义( \: )才表示原始冒号的字面值。
. 在中括号包含的自定义字符类里表示字面值,因为 [.] 这样表示任意字符是无意义的。
使用 \ + 特定字母字符 表示预定义的字符类,
\a 字母\d 数字\i ASCII 字符( 字节码 < 0x80 )\l 小写字母\u 大写字母\p 标点字符\s 空白符 \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 不是空白符\W 不是字母和数字、并且不是下划线 \X 不是十六进制数字\N 不是换行\R 不是回车符\T 不是制表符 \V 不是垂直制表符\F 不是换页符\Z 不是'\0'的字符以上所有预定义字节类都只匹配单个字节,遇到多字节字符(例如中文)也只能匹配单个字节。
在双引号或反引号包含的 原样字符串 中可以直接写这些预定义的字符类,例如 "\d" 。
在单引号包含的 编译时转义字符串 中预定义字符类要注意反斜杠要写两次,例如,例如 '\\d'。这是因为单引号包含的字符串在编译阶段就会将反斜杆识别为转义符, '\\d'经过编译就等价于双引号包含的原样字符串 "\d"。
中括号用于创建自定义的字符类,用于匹配与其中任意一个条件匹配的字符。
自定义字符类可以包含以下内容:
[\d\w] 。[a-z] 表示所有小写字母,[0-7] 则等同于 [01234567]。:,需要用 \: 表示原始冒号字面值。. 在自定义字符类中表示字面值而不是任意字节。+*?!{}<>()%&|等在 [] 内部不具有特殊语义的运算符表示字面值。示例:"[abc0-9\n]"
在自定义字符类内部开始处可以用 ^ 表示取反义的补集。
例如 [^a-z] 匹配任何不是小写字母的单字节字符。
注意自定义字符类补集与自定义字符类的其他规则一样,.仍然表示字面值。
自定义字符类补集中:仍然表示多字节字符。但自定义字符补集中的:或多字节字符只要遇到的不是多字节字符的第一个字节就总是能匹配成功,因为模式匹配本身是基于二进制的。这个看起来是局限的特性实能实现一各巧妙的预测多字节字符边界(中文字符边界)的效果,例如模式串 [^”]+” 它就可以一直向前推进直到遇到中文双引号的首字节,而 <“[^”]+”> 可以匹配双引号内的字符串。
非捕获分组用于将局部模式串重新聚合为一个新的子模式( Subpattern ),用于匹配连续的串而非单个目标。
非捕获组同时也是具有原子性质的分组(Atomic Group),在非捕获组内部不会记录之前的匹配位置也不支持回溯。
语法示例:
<abc0-9\n\d+:+>
尖括号创建的非捕获组匹配一组有序的字符串而不是单个字符。例如<hello> 匹配 "hello" 单词。而不是匹配 "hello" 其中的一个字符。
非捕获组可以包含的基本子模式与自定义字符类相似:
<\d\w> 。<a-z> 表示所有小写字母,<>内-用\转义或者在放在最后面时表示-自身。- 连接的多字节字符表示一个编码范围)。:,需要用 \: 表示原始冒号字面值。非捕获组内部还支持以下模式语法:
<hello<world>> 。<[a-z0-9]> 。|,但不能使用 & 运算符。+ * ? 等限定匹配次数的运算符(贪婪匹配,且不会回溯),但不允许在非捕获组内使用表示惰性匹配的 +? *? 运算符,也不支持 {n,m} 或 {n} 表示匹配次数。 ?=,?!。!%, 也支持附加 ?后缀的最近对称匹配。+ * ? | 运算符。例如 (abc)<\1+> 。注意在非捕获组外部不能对捕获组引用使用任何模式运算符。注意:
. 在非捕获组内仅表示任意单字节字符。在自定义字符内 . 表示字面值,请注意区别。<> 内可以嵌套 <> 以表示嵌套的非捕获组,但其他括号{}() 在 <> 内部仅表示字面值(不具有模式语义,不需要转义)。例如 string.match("a{}()>","<a{}()>") 可以匹配成功。<> 内不能用 () 创建捕获组,()仅表示字面意义(参考上一条)。要特别注意在非捕获组内部执行贪婪而激进的匹配规则,只要能匹配到内容就会尽力向后走并且不会回溯(这在需要局部禁止回溯时非常有用)。例如我们试图用 <“.+”> 匹配中文双引号包含的字符串总是会失败,因为在非捕获组内部 .+ 会一直向前推进,直到消费掉所有符合它自己要求的字节,失败也不会回溯。解决的方法是改为有边界限制的匹配,例如 <“[^”]+”> 就可以匹配成功,自定义字符补集[^”]+只会消费不是中文双引号的字符。
非捕获组自身作为一个子模式可以支持回溯与惰性匹配,例如 <abc>+? 。
在正则表达式中具有类似特性的非捕获组与原子分组都是常见的性能优化分组,但 aardio 模式匹配中的非捕获组有更严格的限制,运行速度也更快。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 + " 字符串是一个整数")
}
另外,在模式串尾部或者非捕获组尾部还可以使用单个 ! 表示 尾部边界断言,其作用与 $ 相同,但是 ! 还可以放在 非捕获组 尾部。
我们还可以用 !p 表示从不匹配子模式 p 到匹配子模式 p 的边界。利用这个特性,我们可以用 !. 匹配字符串开始位置。!. 与锚点 ^ 的作用相同,但 !. 可以用于非捕获组。
下面是一个示例,用模式匹配实现 string.trim 函数的效果,利用边界断言与逻辑运算符去除字符串首尾指定的字符,示例:
var str = "0012345600";
str = string.replace(str, "<!.0+>|<0+!>", "");
print(str);
另外,用反预测断言 .?! 也可以匹配尾部边界。
var str = "0012345600";
str = string.replace(str, "<!.0+>|<0+.?!>", "");
print(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) 反预测断言
正则表达式需要将 ?= 或 ?! 放在模式 p 前面,并且用括号 () 包围断言。
而 aardio 模式匹配将断言视为运算符,运算符只能用于独立的子模式。可以将断言运算符用于非捕获组内部包含的子模式或非捕获组本身,但不能对捕获组使用预测断言运算符。
零宽预测断言可以在同一位置连续使用,示例:
var str = "123z";
//123 后面必须是字母,但不能是 test,也不能是 hello 。
str = string.match(str, "123<\a+>?=<test>?!<hello>?!", "") ;
print(str||"not found");
语法: !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();
边界断言可以用于子模式,也可以用于非捕获组内部的子模式或者非捕获组本身,但不能对捕获组使用边界断言运算符。
如果边界断言没有遇到字符串尾部,在向后预测断言成功以后,会继续从边界向前逐个字符进行回顾性匹配测试。回顾匹配会受到最大估算长度的限制,最大回顾长度由边界右侧匹配成功的长度、以及子模式串本身的长度(对于!p来说就是 p本身的长度)之间取最大值。
在正则表达式里回顾断言只能检测固定长度字符串,而 aardio 模式匹配里
!p虽然可以使用量词,但仍然存在谨慎的估算长度限制。
逻辑或运算符 #
在两个或多个子模式中间添加一个|字符以表示匹配其中任意一个子模式。
注意: |两侧不能都是表示字面值的普通字符,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_subpattern><end_subpattern>
% 后面必须紧跟两个子模式以匹配首尾标记成对出现的字符串( balanced strings )。
如果匹配结果嵌套包含首尾标记,则这些内部的首尾匹配也必须是成对出现的。
<begin_subpattern> 与 <end_subpattern> 可以使用任何子模式,例如可以用 %() 匹配首尾成对的括号。% 后面的符号如果不是构建子模式的符号可以不用转义。 {、}、( 、)不是构建子模式的符号,所以可以用模式 %()、 %{} 匹配成对出现的括号与大括号(这几个括号不能用于包围子模式),但必须用模式 %\[\]、 %\<\> 表示成对出现的方括号与尖括号(方括号和尖括号都用于包围子模式,必须使用转义符避免歧义)。 示例:
%{}匹配以 { 开始, 以 } 结束的字符串。%()匹配以 ( 开始, 以 ) 结束的字符串。%"" 匹配以引号开始, 以引号结束的字符串。%<begin><end>匹配以 begin 开始, 以 end 结束的字符串。%[a-z][0-9] 匹配以字母开始, 以数字结束的字符串。%\a\d 同样匹配以字母开始, 以数字结束的字符串。示例代码:
var str = `a = (a(b)cd) `
var substr = string.match(str, `%()`)
//显示 (a(b)cd)
print(substr);
首尾匹配的子串如果嵌套多层,则 %<begin_subpattern><end_subpattern> 默认会匹配最外层的首尾标记(以及包围的内容)。
如果对称匹配模式后面附加问号 ? ,例如 %() , 就表示『最近对称匹配』,匹在多层嵌套时匹配最内层靠得最近的首尾标记(以及包围的内容)。
例如:
var str = `a = (a(b)cd) `
var substr = string.match(str, `%()?`)
//显示 (b)
print(substr);
只能对子模式使用对称匹配运算符 %。可在非捕获组内部使用对称匹配,也可以对非捕获组本身使用对称匹配,但不能将对用 () 包围的捕获组本身使用对称匹配运算符。
如果希望将对称匹配到的结果进一步应用其他模式进行匹配,可参考:
连续匹配替换函数 string.reduceReplace 。
同样可以执行连续匹配,并替换最终匹配的字符串。
示例:
var str = string.reduceReplace("print(a,(1+2))",
"\w+(%())",//用 %() 对称匹配外层括号包围的部分
"\((.+)\)",//获取括号中间的部分
"\1" //替换为捕获组1
);
print(str)
局部禁用模式语法
以<@开始,并以@>结束的非捕获组用于在模式匹配中进行原始的字符串比较,并在匹配目标子串时暂时局部禁用模式匹配语法。
<@@开始,并以@>结束的非捕获组则表示忽略大小写进行原始的字符串比较。
示例:
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'。<> 以表示嵌套的非捕获分组,也可以包含 [] 以表示自定义字符类,但在 <> 内 {} 或 () 仅具有字面意义(不具有模式语义) ,不能在尖括号包含的非捕获组内使用圆括号 () 创建捕获组,也不能在非捕获组内使用 {n,m} 表示量词运算符 。% 创建的对称匹配除外 ),非捕获组内的匹配规则不但是贪婪而且是特别贪婪,只要能匹配到内容就会尽力向后走并且不会回溯。但非捕获组本身作为子模式且使用量词运算符时可以支持回溯。| 两侧不能都是表示字面值的普通字符,例如A|B必须改为'[AB]'。: 是一个子模式。使用惰性匹配避免不必要的回溯。
多数情况下,在匹配次数运算符后面加上? 会更快,这一点非常重要。贪婪匹配会首先尝试最大长度的匹配,匹配失败会向后回溯重试,类似 (.*)test 这样过多且不必要的回溯是非常低效的。
限制严格的模式串比一个限制宽松的模式更快。
例如模式串 (.*)test 用来获取 test 以前的全部字符,这个条件太过于宽松,查找时会从目标串的第一个字符开始匹配直到字符串结束,如果没有找到,则从目标串的第二个字符开始再次查找。这样的查找效率是很低的。
解决办法是在模式串最前面加上 ^ 限定只在字符串开始锚点匹配,例如 ^(.*)test。改用惰性匹配 ^(.*?)test 找到第一个 test 就停会更快。
严格限制子模式序列的开始字符可以显著提升效率,道理同上。
不要滥用可能匹配空字符串的模式串,例如\a*。
aardio 模式匹配基本的语法尽可能沿用了正则表达式常用的一些基本语法,但模式匹配比正则表达式更简单、运行速度也更快。模式匹配与 aardio 语言完全融为一体,与字符串有关每函数大多默认就支持模式匹配语法。
在测试中 string.regex 耗时 455 毫秒的查询,preg 库正则表达式需要 49 毫秒,而模式匹配仅需要 1.4 毫秒。
aardio 模式匹配与传统正则表达式对比:
| 功能特性 | aardio 模式匹配 | 传统正则表达式 |
|---|---|---|
| 复杂性 | 语法更简洁,限制更多,易于掌握和使用。 | 语法更复杂,功能更强大,但容易陷入“甜蜜陷阱”,导致过于复杂的表达式。 |
| 运行速度 | 模式匹配比正则表达式快数十倍到数百倍 。 | 比模式匹配慢很多。 |
| 捕获组与运算符 | 不能对捕获组 ( ) 使用任何模式运算符(例如量词),捕获组仅用于记录和返回匹配内容。 |
可以对捕获组 ( ) 使用运算符(如量词 +, * 等),功能更灵活。 |
| 非捕获组 | 使用尖括号 < > 定义非捕获组(Non-capturing group),非捕获组是子模式(可以对其使用量词等运算符),也是具有原子性的非捕获分组,内部贪婪匹配且不回溯。非捕获组可嵌套,但不能包含捕获组。例如 <hello>+ 匹配 hello 重复一次或多次。 |
使用 (?:subpattern) 定义非捕获组,使用 (?>subpattern)定义原子分组,可以使用对其运算符。 |
| 子模式与运算符 | aardio 严格区分子模式(subpattern)和运算符(operator)。运算符只能用于子模式(包括非捕获组),不适用于捕获组。 | 无此限制,运算符可用于捕获组和非捕获组,灵活性更高。 |
| 局部禁用模式语法 | 支持使用 <@内容@> 局部禁用模式语法,或 <@@内容@> 忽略大小写匹配。 |
无此功能,无法局部禁用正则语法。 |
| 全局禁用模式语法 | 模式串开头加 @ 或 @@ 可全局禁用模式语法,转换为原样字符串匹配(更快)。 |
无此功能,正则表达式始终解析语法。 |
| 对称匹配运算符 | 使用 % 运算符匹配成对符号及其包含内容,如 %() 匹配成对括号。可添加问号表示最近配对,例如 %()?。 |
无类似语法,需通过其他方式实现对称匹配。 |
| 边界断言 | 使用 !p 实现双向零宽边界断言,同时进行回顾和预测断言,功能更灵活。 |
使用 \b 表示单词边界,或 (?<!p) 和 (?=p) 分别实现回顾和预测断言。 |
| 预测断言 | 支持 p?=(预测断言)和 p?!(反预测断言),但不能用于捕获组。 |
支持 (?=p)(预测断言)和 (?!p)(反预测断言),可用于捕获组。 |
| 回顾断言 | 不支持单独的回顾断言,边界断言 !p 同时包含回顾和预测功能。 |
支持 (?<!p)(反回顾断言),功能更全面。 |
| 字符类定义 | \u 表示大写字母,\l 表示小写字母,不支持 Unicode 范围定义。 |
\u 常用于表示 Unicode 编码,支持 Unicode 字符范围。 |
| 多字节字符处理 | 多字节字符(如中文)不是最小匹配单位,只有放入 [] 或 <> 中才能作为子模式使用;可用 : 表示任意多字节字符。 |
多字节字符处理更灵活,支持 Unicode 范围和属性匹配。 |
| 惰性匹配 | 支持惰性匹配(如 +?, *?),但非捕获组内部不支持惰性匹配,总是贪婪匹配且不回溯。 |
支持惰性匹配(如 +?, *?),非捕获组无类似限制。 |
| 捕获组引用 | 使用 \1 到 \9 引用捕获组,替换字符串中还可用 \0 表示整个匹配;不能对捕获组引用使用运算符,除非是在非捕获组内。 |
使用 $1 到 $9 引用捕获组,可对引用使用运算符。 |
| 适用场景 | 更适合快速、简单的文本和二进制字符串处理,避免复杂度无限上升。 | 适合复杂文本处理,但可能因复杂语法导致性能下降。 |
| 与语言集成 | 与 aardio 语言深度集成,字符串相关函数默认支持模式匹配。 | 需额外导入 preg 或 string.regex 等独立库以支持正则表达式。 |
aardio 用单引号包围的 转义字符串 内支持以
\u前缀引导的 Unicode 编码,但在转义字符内必须用又反斜杠表示模式转义符,例如'\\d'与"\d"表示相同的模式串。
模式匹配与正则的最大区别是正则表达式强大且复杂,模式匹配小、轻、快并易于掌握和使用。很多时候我们要避免“正则表达式甜蜜陷阱”,避免执着于用正则表达式解决复杂问题,把正则表达式实现的过于复杂。把所有的需求试图用一个正则表达式去解决,这是一种非常原始的“写命令行”的思维,并非解决问题的最佳方案。
很多时候更好的解决方案是去编写代码替换复杂的模式串,在 aardio 中文本分析是非常方便的,例如标准库中 string.xml 的源码大家可以看看,里面虽然用到了模式匹配,但并非用一两个模式匹配就能搞定这种复杂的文本分析。 其它的可以看看 bencoding.decoder,string.csv 等等支持库的源代码,这些里面基本没有使用或很少使用模式匹配。