---
url: https://www.aardio.com/zh-cn/doc/library-guide/builtin/string/patterns.html.md
---

# 模式匹配语法

参考: 

- [模式匹配函数](matching.html)
- [模式匹配快速入门](../../../guide/language/pattern-matching.html)

## 模式串( Patterns )

aardio 模式串是描述字符与字符串特征的文本，用于字符串查找、替换等操作中执行特定语义的模式匹配。

模式串由主要由表示字面值的字符(literal characters)、表示特定文本集合的子模式(Subpattern)、表示特定匹配规则的模式运算符（operators）、以及分组匹配结果捕获组（capturing groups）与非捕获组（Non-capturing group）组成。

模式匹配的语法与正则表达式类似，但比正则表达式的语法更为简洁，运行速度也快很多。可以有效地避免在使用正则表达式时复杂度无限上升，性能降低等问题。

另外正则表达式通常用于匹配文本，而 aardio 模式表达式天然适合处理 UTF-8 文本与任意二进制字符串。

aardio 模式匹配与正则表达式一个较大的区别是 aardio 严格区分描述字符集合的子模式( Subpattern )与描述匹配规则的运算符（operators），并且"模式运算符"只能用于"子模式"（包含非捕获组）不能用于捕获组。而正则运算符没有这些限制，可以用于捕获组。

### 1. 子模式( Subpattern )

子模式描述字符或字符串的特征并表示特定匹配目标，用于在目标文本中匹配特定字符、字符串序列、锚点位置。

子模式是 aardio 模式匹配的最小匹配单位。子模式的一个主要特征是具有原子性（atomic），子模式是匹配链中的原子节点，也是回溯的原子节点。

子模式：

- 普通单字节字符表示的字面值属于子模式。  
  多字节字符（例如中文）不是最小模式匹配单位（子模式）。但是我们可以将多字节字符放入中括号（自定义字符类），或者将多字节字符放到`<>`内部构成非捕获组，这些都是子模式（并且能包含中文等多字节字符）。
- 特殊字符（ special characters ）构建的子模式
  * [字符类](#classes)，例如 `\w` 用于表示所有字母数字。
  * [自定义字符集合](#set)，例如`[a-z]` 表示 a 到 z 的所有小写字母 
  * [非捕获组](#non-cap)，例如 `<abc>` 匹配字符串 "abc"。非捕获组内可以组合内部的多个子模式为单个子模式，可用于匹配任意长度的字符串。
  * [任意字符](#any-character) 表示任意单字节字符的 `.` 以及表示任意多字节字符的  `:` 。
  * [首尾锚点](#anchors) 匹配文本开始位置的 `^` 与匹配文本结束位置的 `$` 是一种零宽匹配的特殊子模式。

有特殊模式语义的特殊符号可以用 [模式转义符](#escape-character) 转换为字面值。

**注意: aardio 模式匹配中 `<>` 包围的非捕获组是子模式（可以对其使用量词等运算符），但 `()` 包围的捕获组不是子模式（不能对其使用量词等运算符），这是 aardio 模式匹配与正则表达式最大的区别**

### 2. 模式运算符（ operators ） <a id="operators" href="#operators">&#x23;</a>

在 aardio 中运算符是指用于描述或改变匹配规则的特殊符号。

运算符默认只能用于子模式。

以限定子模式匹配一次或多次量词运算符 `+` 为例：

* `a+` 表示匹配字符 a 一次或多次，用法正确。
* `[a-z]+` 表示匹配小写字母一次或多次，用法正确。
* `(abc)+` 错误， "捕获组"不是"子模式"，不能使用运算符。
* `<abc>+` 正确， "非捕获组"虽然匹配多个字符，但已被重新聚合为单个"子模式"，可以对捕获组使用其他模式运算符。

不能对捕获组或使用模式运算符，这是"aardio 模式匹配"与"正则表达式"最大的一个区别。

例如正则表达式  `(hello|world)` ，在模式匹配中等价的写法为  `(<hello>|<world>)`。这是因为多个字符只有放入尖括号中创建 **非捕获分组** 才会变成一个子模式，aardio 模式运算符只对子模式有效（ 在模式匹配里捕获组不是子模式 ）。

在 aardio 模式匹配中区分子模式与运算符是非常重要的。例如匹配首尾锚点的  `^` 与 `$` 是子模式，而用于[边界断言](#boundary)的 `!` 却是运算符。虽然都是匹配边界但性质不同，`^` 与 `$` 只是直接匹配特定的目标，而 `!` 是作用于其他子模式以改变匹配规则。

请参考：[运算符列表](#operators)

## 分组  <a id="groups" href="#groups">&#x23;</a>

### 1. 捕获组（ capturing groups ） <a id="capturing-groups" href="#capturing-groups">&#x23;</a>

在模式串中可以使用一对圆括号 `()` 将模式子串包含在内以创建捕获组。

捕获组记录匹配的内容，在模式串后面可以使用 `\1` 到  `\9` 反向引用之前捕获组的匹配结果。

对于[string.match](matching.html#match) [string.gmatch](matching.html#gmatch) 等模式匹配函数，每个捕获组都会增加一个返回的字符串值。如果没有任何捕获组，则第一个返回值为匹配的全部字符串，否则第一个返回值为第一个捕获组。

在替换函数的回调函数中，每个捕获组都会增加一个字符串参数。如果没有任何捕获组，则第一个回调参数为匹配的全部字符串，否则第一个回调参数为第一个捕获组。

可使用不包含模式子串的空捕获组 `()` 记录并返回该分组所在位置（以字节计数）。

捕获组不具有原子性，内部允许回溯。对捕获组也不能使用任何模式运算符（这是与正则表达式最大的一个区别）。

捕获组可以包含子模式与运算符，而子模式不能包含捕获组。

捕获组可以嵌套。

> 捕获组引用 <a id="backreferences" href="#backreferences">&#x23;</a>
> 
> 在模式串中也可以使用 `\1` 到 `\9` 向前引用捕获组，在  `\` 后面用单个数字指定捕获组序号 - 也就是在整个模式串中创建捕获组的左括号 `(` 出现的前后顺序。例如 `(a(bc))\2` 匹配 "abcbc"，从前向后数`(` 出现的位置 ，`\2` 反向引用的是第 2 个 `(` 创建的捕获组为 `(bc)`。在查找模式串中可以引用的捕获组序号为 `\1` 到 `\9`，而在替换字符串除了可以使用 `\1` 到 `\9` 引用捕获组，还中可以用 `\0` 表示匹配到的整个字符串。
> 
> 默认不能对捕获组引用使用任何运算符，但一个例外是可以在非捕获组内对捕获组引用使用运算符。例如 `(abc)<\1+>` 这里面的  `+` 是施加于 `\1` 的量词运算符（用法正确），但在非捕获组外部不能这样用，例如 `(abc)\1+` 这里面的 `+` 并不会解析为量词运算符（仅匹配 `+` 的字面值）。

### 2. 非捕获组（Non-capturing group） 

使用尖括号包含模式子串以创建非捕获组（ [Non-capturing group](#non-cap) ），格式为 `<subpattern>`。

- 非捕获组允许嵌套。
- 可以对非捕获组本身使用其他模式运算符。
- 非捕获组同时也是具有原子性质的分组（Atomic Group），是一旦匹配成功就会被“锁定”的原子节点，绝不会再回溯到  `<>` 内部
- 非捕获组内部中只存在贪婪激进的匹配规则，不支持惰性匹配。
- 在非捕获组内可以使用其他模式（限制惰性运算符等语法），非捕获组将包含一个或多个子模式的 **模式子串** 组合为一个独立的子模式。
- 非捕获组不会捕获匹配的内容，反向引用会忽略所有非捕获组。

要特别注意：

- 可以对非捕获组使用模式运算符。
- 不可以对捕获组使用模式运算符。

在 aardio 模式匹配中非捕获组有重要的意义。这是因为除了非捕获组与对称匹配以外其他子模式与模式运算符都只能匹配单个字符，而非捕获组则能够聚合一组子模式以匹配连续的字符，并且对这一组匹配结果可以使用其他的模式运算符，这极大地扩展了模式匹配的功能。

## 模式转义符 (escape character) <a id="escape-character" href="#escape-character">&#x23;</a>

在 aardio 模式匹配中『模式转义符』为反斜杠 `\`。

模式串中的 `\` 用于将表示原始字面值的普通字符转换为特殊的模式符号，或将特殊的模式符号转换为仅匹配原始字面值的普通字符。

转义符的作用:

1.  使用`\ + 特定字母字符`表示特定的[字符类](#class)，例如用 `\d` 匹配数字。
2.  使用`\ + 标点符号`表示标点符号字面值本身（即取消原来的模式语义），例如 `\\` 匹配原始反斜杆， `\<` 、 `\>` 匹配原始尖括号。
3.  使用`\ + 数字`表示[向前引用捕获组](#backreferences)。 

在特殊符号不具有特殊模式语义时可以不用转义，例如：

- `+*?!{}<>()%&|`等在 `[]` 内部不具有特殊语义的运算符表示字面值。
- `{}()!%&`等在 `<>` 内部不具有特殊语义的运算符表示字面值
- 在边界断言运算符 `!` 后面只能是子模式，不能构建子模式的特殊符号不用转义，包括 `!`自身也不用转义。
- 在对称匹配运算符 `%` 后面只能是子模式，不能构建子模式的特殊符号不用转义，包括 `%`自身也不用转义。

不具有特殊模式语义的字符前面加或不加 `\` 是等价的，例如模式串 `\,` 等价于 `,` 。

要特别注意区分模式串( Patterns ) 的"模式转义符"与转义字符串（ escaped string literals ）的"编译转义符"。建议总是将模式匹配写在双引号或反引号包围的原样字符串（ raw string literals ）内部，在单引号包围的转义字符串内则必须用双反斜杆将编译转义符转换为模式转义符，例如模式串 `'\\<title\\>.*?\\</title\\>'` 与  `"\<title\>.*?\</title\>"` 是等价的。

参考：

- [原样字符串](../../../language-reference/datatype/string.html#raw) 
- [编译时转义字符串](../../../language-reference/datatype/string.html#backslash-escaped) 


## 子模式

### 任意字符 <a id="any-character" href="#any-character">&#x23;</a>


圆点 `.` 表示任意单字节。
冒号 `:` 表示 UTF-8 编码文本中的任意多字节字符(例如中文字符) 。

`:` 在任何时候都表示子模式，除非用转义符取消转义（ `\:` ）才表示原始冒号的字面值。

`.` 在中括号包含的自定义字符类里表示字面值，因为 `[.]` 这样表示任意字符是无意义的。 

### 预定义字符类 ( predefined character classes ) <a id="classes" href="#classes">&#x23;</a>


使用 `\ + 特定字母字符` 表示预定义的字符类，

- `\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'`的字符  

以上所有预定义字节类都只匹配单个字节，遇到多字节字符（例如中文）也只能匹配单个字节。

在双引号或反引号包含的 [原样字符串](../../../language-reference/datatype/string.html#raw)  中可以直接写这些预定义的字符类，例如 `"\d"` 。

在单引号包含的 [编译时转义字符串](../../../language-reference/datatype/string.html#backslash-escaped) 中预定义字符类要注意反斜杠要写两次，例如，例如 `'\\d'`。这是因为单引号包含的字符串在编译阶段就会将反斜杆识别为转义符， `'\\d'`经过编译就等价于双引号包含的原样字符串 `"\d"`。

### 自定义字符集合（ character set ） <a id="set" href="#set">&#x23;</a>


1. 自定义字符集合  

中括号用于创建自定义的字符类，用于匹配与其中任意一个条件匹配的字符。

自定义字符类可以包含以下内容：

- 其他反斜杠引导的预定义字符类，例如 `[\d\w]` 。
- 单字节字符字面值，特殊模式符号需要转义。
- 两个单字节字符之间范围内的字符集合，首尾两个字符用连字符分开。例如 `[a-z]` 表示所有小写字母，`[0-7]` 则等同于 `[01234567]`。
- UTF-8 编码的多字节字符（不能指定编码范围）。
- 表示任意中文的 `:`，需要用 `\:` 表示原始冒号字面值。
- `.` 在自定义字符类中表示字面值而不是任意字节。
- 其他 `+*?!{}<>()%&|`等在 `[]` 内部不具有特殊语义的运算符表示字面值。

示例:`"[abc0-9\n]"` 

### 2.  自定义字符补集（negated character set）

在自定义字符类内部开始处可以用 `^` 表示取反义的补集。

例如 `[^a-z]` 匹配任何不是小写字母的单字节字符。

注意自定义字符类补集与自定义字符类的其他规则一样，`.`仍然表示字面值。

自定义字符类补集中`:`仍然表示多字节字符。但自定义字符补集中的`:`或多字节字符只要遇到的不是多字节字符的第一个字节就总是能匹配成功，因为模式匹配本身是基于二进制的。这个看起来是局限的特性实能实现一各巧妙的预测多字节字符边界（中文字符边界）的效果，例如模式串 `[^”]+”` 它就可以一直向前推进直到遇到中文双引号的首字节，而 `<“[^”]+”>` 可以匹配双引号内的字符串。


### 非捕获组（ Non-capturing group ） <a id="non-cap" href="#non-cap">&#x23;</a>

#### 1. 非捕获组

非捕获分组用于将局部模式串重新聚合为一个新的子模式( Subpattern )，用于匹配连续的串而非单个目标。 

非捕获组同时也是具有原子性质的分组（Atomic Group），在非捕获组内部不会记录之前的匹配位置也不支持回溯。

语法示例:

`<abc0-9\n\d+:+>`  
  
尖括号创建的非捕获组匹配一组有序的字符串而不是单个字符。例如`<hello>` 匹配 "hello" 单词。而不是匹配 "hello" 其中的一个字符。  

非捕获组可以包含的基本子模式与自定义字符类相似：

- 其他反斜杠引导的预定义字符类，例如 `<\d\w>` 。
- 单字节字符字面值，特殊模式符号需要转义。
- 用连字符分开 2 个单字节字符表示自定义的字符集合。例如 `<a-z>` 表示所有小写字母，`<>`内`-`用`\`转义或者在放在最后面时表示`-`自身。
- UTF-8 编码的多字节字符（不能使用 `-` 连接的多字节字符表示一个编码范围）。
- 表示任意中文的 `:`，需要用 `\:` 表示原始冒号字面值。

非捕获组内部还支持以下模式语法：

- 非捕获组内部可以嵌套其他非捕获组，例如  `<hello<world>>` 
- 可以在非捕获组中插入用中括号包含的自定义字符类，例如 `<[a-z0-9]>`  
- 非捕获组内部可以使用逻辑运算符 `|`， `&` 
- 非捕获组内部可以 `+` ，`*`， `?` ，`{n,m}` 或 `{n}`等限定匹配次数的量词运算符（贪婪匹配，且不会回溯），但不允许在非捕获组内使用表示惰性匹配的 `+?` `*?` 惰性量词
- 非捕获组内部支持预测断言运算符 `?=`,`?!`
- 非捕获组内部可以使用边界断言运算符 `!`
- 非捕获组内部可以使用对称匹配运算符 `%`, 也支持附加 `?` 后缀的最近对称匹配。
- 可以在非捕获组内部向前引用捕获组，并支持对捕获组引用使用其他模式运算符。例如 `(abc)<\1+>` 。注意在非捕获组外部不能对捕获组引用使用任何模式运算符。

注意：
- `.` 在非捕获组内仅表示任意单字节字符。在自定义字符内  `.` 表示字面值，请注意区别
- `<>` 内不能用 `()` 创建捕获组，`()`仅表示字面意义（参考上一条）。

非捕获组内部的任何匹配都是原子性的（每个模式都只顾自己，所以速度就快），并且执行贪婪而激进的匹配规则（非捕获组内不支持惰性量词），只要能匹配到内容就会尽力向后走并且不会回溯（这在需要局部禁止回溯时非常有用）。在例如我们试图用 `<".+">` 匹配双引号包含的字符串总是会失败，因为在非捕获组内部 `.+` 会一直向前推进，直到消费掉所有符合它自己要求的字节，失败也不会回溯。替代方案：

1. 将 `<.+>` 改为更严格的条件，例如 `<"[^"]+">` 内部的 `[^"]+` 只会消费双引号 `"` 以外的字符
2. 改用对称匹配，例如 `string.match("begin任意end","<%<begin><end>?>")` 可以匹配成功
3. 移到非捕获组之外，例如 `string.match("begin任意end","begin.*?end")` 可以匹配成功

非捕获组自身作为一个子模式则可以支持回溯与惰性匹配，例如 `<abc>+?` 。

在正则表达式中具有类似特性的非捕获组与原子分组都是常见的性能优化分组，但 aardio 模式匹配中的非捕获组有更严格的限制，运行速度也更快。aardio 通过**非捕获组**限制并避免了在使用模式匹配时复杂度无限上升、并以此换取最大化的匹配速度（模式匹配比正则表达式快数十倍）与更精简更可控的匹配语法。

非捕获组的原子性（不回溯）与激进性（无惰性）不但能提供更好的性能，善用之可以高效地解决一些复杂的匹配问题。

举个例子: <a id="atomic-group-example" href="atomic-group-example">&#x23;</a>


```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>>`就可以得到正确的结果。这利用了非捕获组的原子特性，在非捕获组内部匹配成功就不会因为后续的匹配失败而`回溯重试`， 这不但提供了更好的性能，也能帮助我们排除不需要的结果。

参考：[打开 preg 扩展库文档查看相同的正则表达式示例](../../ext/preg/_.html#atomic-group-example)

#### 2.  非捕获组 - 反匹配

示例语法:

`<^a-zA-Z\d*>`

在尖括号内部最前面要加上 `^` 表示取反匹配，用于匹配等长并且不匹配指定模式的字符串。

例如 `<^hello>` 匹配所有不是 "hello" 并且等长的字符串。
  
#### 3. 非捕获组 - 原始匹配

示例语法:

`<@任意内容@>`

`@`放在尖括号内部首尾两端，表示禁用模式语法以匹配一个原始子串。

这是 aardio 模式语法中非常有用的一个特殊功能，它可以在一个查找串中对部分字符串暂时局部禁用模式语法。

例如 `<@a-zbc@>` 匹配"a-zbc"，而不能匹配"abc"。

举个具体的例子。

如果我们需要查找 A,B,C 三段文本组成的块，其中 A 为开始段,而 C 为结束段, B 是任意字符。我们用 `.+` 的模式串表示中间的B段，而 A 和 C 包含大量的标点符号，我们不希望在这两处使用模式语法，只是希望直接查找开始文本与结束文本。

这在批量处理文本时经常遇到，要么细心地编写查找模式，一个字符地处理转义，或者编写大量的代码来实现该逻辑,先查出开始段，再找出 ………… 总之是很麻烦的一件事。

在模式匹配中使用临时禁用模式语法的非捕获组`<@任意内容@>` 能彻底解决这一难题，使模式语法的使用更为简单。因为我们可以按需禁用模式语法，查找速度也会显著提升。

如果非捕获组以两个 `@@`字符开始写为 `<@@任意内容@>` 表示忽略大小写匹配一个原始子串。如果是使用本就大小写不敏感的 string.cmpMatch 函数就没必要这样写了。

### 首尾锚点（ anchors ）<a id="anchors" href="#anchors">&#x23;</a>


| 子模式 | 说明 |
| --- | --- |
| `^` | 文本开始锚点 |
| `$` | 文本尾部锚点 |

`^` 作为模式串的首字符才表示目标字符串头部锚点， 
`$` 作为模式串的最后一个字符才表示目标字符串尾部锚点。

示例:  
  
```aardio
import win;
var str = "1234";

if( string.find(str, "^\d") ) { 
	win.msgbox(str + " 字符串以数字开始")
}

if( string.find(str, "^[+-]?\d+$") ) {
	win.msgbox(str + " 字符串是一个整数")
}
```

另外，在模式串尾部或者非捕获组尾部还可以使用单个 `!` 表示 [尾部边界断言](#end-boundary)，其作用与 `$` 相同，但是 `!` 还可以放在 [非捕获组](#non-cap) 尾部。

我们还可以用 `!p` 表示从不匹配子模式 p 到匹配子模式 p 的[边界](#boundary)。利用这个特性，我们可以用 `!.` 匹配字符串开始位置。`!.` 与锚点 `^` 的作用相同，但  `!.` 可以用于非捕获组。

下面是一个示例，用模式匹配实现 string.trim 函数的效果，利用边界断言与逻辑运算符去除字符串首尾指定的字符，示例：

```aardio
var str = "0012345600";
str = string.replace(str, "<!.0+>|<0+!>", "");
print(str);

```

另外，用反预测断言 `.?!` 也可以匹配尾部边界。

```aardio
var str = "0012345600";
str = string.replace(str, "<!.0+>|<0+.?!>", "");
print(str);
```

## 运算符 <a id="operators" href="#operators">&#x23;</a>


模式匹配中的运算符是指用于描述或改变匹配规则的特殊符号。运算符默认只能用于子模式( Subpattern )。

### 量词（ quantifiers ）<a id="quantifiers" href="#quantifiers">&#x23;</a>

量词运算符用于指定一个子模式的匹配次数。

下面的表格中用 p 表示一个 子模式( Subpattern )。

| 贪婪匹配量词 | 说明 |
| --- | --- |
| p{min,max}<br> | 最长匹配子模式 p 最少 min 次，最多 max 次，<br>min,max 都可以是多个数字表示的数值。<br><br> 可以省略其中一个参数，或者仅用一个参数限定匹配长度。<br><br>例如:<br><br> p{min,} <br> p{,max} <br> p{len} |
| p+ | 最长匹配子模式 1次或多次,等价于{1,} |
| p* | 最长匹配子模式 0 次或多次,类似{0,} |
| p？ | 最长匹配子模式 1 次或 0 次,类似{0,1} |

用于指定匹配次数的运算符默认为**贪婪匹配**模式，**贪婪匹配**又称为最长匹配，默认会在指定语义下尽可能获得最长的匹配结果。

如果在 `{min,max}`,`+`,`*` 这些运算符后加一个 `?` 号可以生成三个新的惰性匹配运算符 `{min,max}?`,`+?`,`*?` 。惰性匹配运算符保持原来的语义，但会在指定语义下做尽可能短的匹配。

| 惰性匹配量词 | 说明 |
| --- | --- |
| p{min,max}?<br> | 最短匹配子模式 p 最少 min 次，最多 max 次，<br>min,max 都可以是多个数字表示的数值。<br><br> 可以省略其中一个参数，或者仅用一个参数限定匹配长度。<br><br>例如:<br><br> p{min,} <br> p{,max} <br> p{len} |
| p+? | 最短匹配子模式 1次或多次,等价于{1,} |
| p*? | 最短匹配子模式0次或多次,类似{0,} | 

惰性匹配又称为最短匹配、非贪婪匹配。

惰性匹配的速度更快。

例如，对于字符串 "aaaaaa"，模式串 `a+?` 将匹配单个 "a"，而模式串 `a+` 将匹配所有 'a'。  
  
  
```aardio
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()
```  

### 预测断言（ lookahead assertion ） <a id="lookahead" href="#lookahead">&#x23;</a>

断言都是零宽断言，不消费任何字符宽度。

零宽断言主要分为两种：

- 回顾断言：向字符串开始的方向回顾匹配程序刚刚经过的字符串是否符合条件。
- 预测断言：向字符串尾部的方向预测匹配程序即将消费的字符或字符串是否符合条件。

模式匹配支持的预测断言语法：

| 运算符 | 说明 |
| --- | --- | 
| p?= | 正预测断言，测试子模式 p 是否可匹配1次，不消费任何字符宽度 |
| p?! | 反预测断言 ，测试模式 p 是否不匹配至少 1 次，不消费任何字符宽度 |

断言与量词运算符的作用是类似的，断言虽然不会实际消费字符宽度，但仍然是用于判定匹配结果的数量。

预测断言可以认为是量词运算符 `?` 的变体：

- `p?` 允许匹配子模式 p 一次或零次。
- `p?=` 则要求匹配子模式 p 至少一次，且不会消费字符宽度。
- `p?!` 则要求不匹配子模式 p 至少一次，且不会消费字符宽度。

这个 `?` 在这里的寓意就是“询问有没有”某个子模式，而 `=` 寓意为条件相符，`!` 寓意为条件取反。

在[正则表达式里类似的断言语法](../../ext/preg/_.html#assertion)如下：

`(?=p)` 正预测断言
`(?!p)` 反预测断言

正则表达式需要将 `?=` 或 `?!` 放在模式 p 前面，并且用括号 `()` 包围断言。

而 aardio 模式匹配将断言视为运算符，运算符只能用于独立的子模式。可以将断言运算符用于非捕获组内部包含的子模式或非捕获组本身，但不能对捕获组使用预测断言运算符。

零宽预测断言可以在同一位置连续使用，示例：

```aardio
var str = "123z";

//123 后面必须是字母，但不能是 test，也不能是 hello 。
str = string.match(str, "123<\a+>?=<test>?!<hello>?!", "") ;
print(str||"not found");
```

### 边界断言（ boundary ） <a id="boundary" href="#boundary">&#x23;</a>

语法: `!p`

普通的边界断言用一个感叹号加一个子模式组成。字面上理解，与编程一样这里的 `!` 是逻辑取反的意思。

感叹号后面只能放一个子模式，如果不是可以构建子模式的特殊符号则表示字面值可以不用转义。例如 `!(` 或 `!{` ，甚至 `!!` 都可以。

> 边界断言可以用于子模式，也可以用于 `<非捕获组>`内部的子模式或者非捕获组本身，但不能对`捕获组`应用此类模式操作符。

边界断言是一种左右双向的零宽断言（Zero-width Assertions）语法，匹配时只是做断言检测，但并不消费任何字符宽度，也就是说下一次匹配仍然是从当前位置开始。`!p` 类似正则表达式的 [Lookaround(Lookahead and Lookbehind)](../../ext/preg/_.html#assertion) 特性，可以用正则表达式 `(?<!p)(?=p)`  描述类似的语义 。可以理解为一个`反回顾断言` + `正预测断言`。

**`!p` 的检测规则如下：**

- `!p` 首先是一个反回顾断言。如果边界断言没有遇到字符串尾部，在向后预测断言成功以后，会从边界向字符串开始的方向逐个字符回溯并执行模式匹配校验，检测程序刚刚经过的字符串是否不符合 p 子模式指定的条件。如果回溯时模式匹配成功，匹配结果的**开始位置自边界之前开始，并且达到或者跨越边界**则本次反回顾断言失败（边界断言也会失败）。

  边界断言回溯的最大长度为估算值（在边界右侧匹配成功的长度、以及子模式串本身的长度（对于`!p`来说就是 `p`本身的长度）之间取最大值）。在正则表达式里回顾断言只能检测固定长度字符串，而 aardio 模式匹配里 `!p` 并不限制固定长度（内部可以使用长度不确定的量词），但 aardio 仍然会设置一个谨慎的估算长度上限（过度回溯是不必要的）。

  重复要点：**如果回溯检测时匹配成功并且匹配的字符串自边界之前开始并且达到边界或跨越边界之后则断言失败**

- `!p` 同时也是一个预测断言，用于判断当前位置是从不满足该匹配条件（边界左侧）切换到满足该条件（边界右侧）的字符串分界（匹配的字符串开始位置必须恰好是当前边界，不能自边界之前开始并跨越边界）。

**使用 `!p` 匹配字符串首尾边界：**

`!p` 在字符串首尾两端只检测有字符的内侧边界：

- 非空字符串的开始位置 `!p` 的反回顾断言总是成功，只做预测断言。
- 非空字符串的结束位置 `!p` 的预测断言总是成功，只做回顾断言。
- 边界断言不匹配空字符串（长度为 0）。

在整个模式串的尾部或者非捕获组的尾部可以用单个 `!` 匹配字符串的尾部边界，这时候 `!` 等价于  `!<^.>`,相当于尾部边界前面可以是任意字符。单个 `!` 放在模式串的尾部等价于 `$`，但 `$` 在模式串的其他位置只有字面意义没有模式语义，而单个  `!` 则可放`<非捕获组!>`的尾部用于匹配目标字符串尾端边界。 <a id="end-boundary " href="#end-boundary">&#x23;</a>

**边界断言有两种常用格式：**

1. 单模式断言

	格式：
	
	- `!<断言模式串><消费模式串>` 
	- `<消费模式串>!<断言模式串>`
	
	边界之前不匹配该模式串，边界之后匹配该模式串则断言成功。
	
	示例： `!\whello!W` 
	
	单词前面的 `!\w` 从不匹配的 `\w` 到匹配 `\w` 的边界。
	单词后面的 `!\W` 从匹配的 `\w` 到匹配 `\w` 的边界（大写`\W`与小写 `\w` 匹配的字符集相反）。

2. 双模式断言

	格式：
	
	- `!<<反回顾断言>|<预测断言>><消费模式串>` 
	- `<消费模式串>!<<反回顾断言>|<预测断言>>` 
	
	这种方式更为灵活。

	示例： `!<<\[^\>\]>|\>>\>` 
 
	这里的消费模式串是 `\>` ，其目的是匹配 `>` 符号。
	但要求 `>` 不能出现在 `[^>]` 内部。
	
	这是一个 `!<<反回顾断言>|<预测断言>><消费模式串>` 格式。
	<预测断言> 与 <消费模式串> 都是 `\>`。
	
	而 <反回顾断言> 则是 `<\[^\>\]>`,这是一个非捕获组，
	其作用是从边界开始逐字回溯查找 `[^>]` 是否出现在边界上（匹配结果达到或**跨越边界**）。
	如果遇到  `[^>]` 内部的 `>` 显然反回顾断言就会失败。
	
	最后实现的效果就是只会找到不出现在 `[^>]` 内部的 `>` 

  > aardio 模式匹配的边界断言不仅仅是匹配边界之前固定长度的字符串，也会**跨越边界检测以判定边界是否出现或者不出现在指定模式的内部**。

**示例：匹配单词边界**
  
```aardio
import console; 

//测试字符串
var str = "abc 3ddeadsfasd dfa123 qerqwe"

//模式匹配边界断言
for word in string.gmatch( str,"!\w([a-zA-Z]\w*)") { 
	console.log("模式匹配：", word )
}

import preg; 

//正则表达式反回顾断言 + 预测断言模拟边界断言效果
var regex = preg("(?<!\w)(?=\w)([a-zA-Z]\w*)");  
for word in regex.gmatch( str  ) { 
	console.log("正则表达式：", word )
}
regex.free();

console.pause();
```  

可以看到正则表达式、模式匹配的匹配结果是一样的， 

如果我们希望匹配一个特定单词的首尾锚点，可以在首尾写两个相反的边界断言，例如 `\whello!\W`。


### 逻辑运算符 <a id="logical-operators" href="#logical-operators">&#x23;</a>


1. 逻辑或运算符 <a id="or" href="#or">&#x23;</a>


    在两个或多个子模式中间添加一个`|`字符以表示匹配其中任意一个子模式。

    注意： `|`两侧不能都是表示字面值的普通字符，`A|B`必须改为'[AB]'。

    逻辑或运算符在非捕获组外支持回溯，但在非捕获组内不支持回溯并总是返回最早匹配到的结果。

    > 例如 `string.match("https://","<<http>|<https>>\://")` 会因为逻辑或在非捕获组内总是返回 http 而不是 https 而无法成功。而将更长的 `<https>` 移到前面写为 `string.match("https://","<<https>|<http>>\://")`, 或者将逻辑或从非捕获组内部移出来写为 `string.match("https://","<http>|<https>\://")`  ，都能正确匹配 "https://" 或 "http://" 。
  

2. 逻辑与匹配 <a id="end" href="#end">&#x23;</a>

  
    在一个或多个子模式中间添加一个`&`字符以表示目标字符或字符串必须匹配所有子模式，匹配结果将是其中最长的匹配子串。

    示例：

    ```aardio
    import console

    //匹配结尾不能为英文标点或中文字符，返回值应当为null
    var abc =  string.match( "123ABC.",".+\P&\i$")

    //匹配结尾不能为英文标点或中文字符，返回值应当为"456cde"
    var cde =  string.match( "456cde",".+\P&\i$")

    console.log(abc,cde);
    console.pause();
    ```  

### 对称匹配运算符  <a id="balanced-strings" href="#balanced-strings">&#x23;</a>

语法：`%<begin_subpattern><end_subpattern>`

`%` 后面必须紧跟两个`子模式`以匹配首尾标记成对出现的字符串（ balanced strings ）。 
如果匹配结果嵌套包含首尾标记，则这些内部的首尾匹配也必须是成对出现的。

- 上面的 `<begin_subpattern>` 与  `<end_subpattern>` 可以使用任何子模式，例如可以用 `%()` 匹配首尾成对的括号。
- `%` 后面的符号如果不是构建子模式的符号可以不用转义。 `{`、`}`、`(` 、`)`不是构建子模式的符号，所以可以用模式 `%()`、 `%{}` 匹配成对出现的括号与大括号(这几个括号不能用于包围子模式)，但必须用模式 `%\[\]`、 `%\<\>` 表示成对出现的方括号与尖括号（方括号和尖括号都用于包围子模式，必须使用转义符避免歧义）。 

示例：

- `%{}`匹配以  `{` 开始， 以 `}` 结束的字符串。
- `%()`匹配以  `(` 开始， 以 `)` 结束的字符串。
- `%""` 匹配以引号开始， 以引号结束的字符串。  
- `%<begin><end>`匹配以  `begin` 开始， 以 `end` 结束的字符串。
- `%[a-z][0-9]` 匹配以字母开始， 以数字结束的字符串。
- `%\a\d` 同样匹配以字母开始， 以数字结束的字符串。

示例代码：
  
```aardio
var str = `a = (a(b)cd)  `

var substr = string.match(str, `%()`) 

//显示 (a(b)cd)
print(substr); 
```  

首尾匹配的子串如果嵌套多层，则 `%<begin_subpattern><end_subpattern>` 默认会匹配最外层的首尾标记（以及包围的内容）。  
如果对称匹配模式后面附加问号 `?` ，例如 `%()` , 就表示『最近对称匹配』，匹在多层嵌套时匹配最内层靠得最近的首尾标记（以及包围的内容）。

例如:

```aardio

var str = `a = (a(b)cd)  `

var substr = string.match(str, `%()?`) 

//显示 (b)
print(substr); 
```   

只能对子模式使用对称匹配运算符 `%`。可在非捕获组内部使用对称匹配，也可以对非捕获组本身使用对称匹配，但不能将对用 `()` 包围的捕获组本身使用对称匹配运算符。

如果希望将对称匹配到的结果进一步应用其他模式进行匹配，可参考：
- 连续匹配函数 [string.reduce](matching.html#reduce)   
  可指定多个模式串参数，对上一次模式匹配的结果应用下一个模式串参数进行匹配，通过连续地模式匹配逐步缩短并简化需要用匹配的字符串。
- 连续匹配替换函数 [string.reduceReplace](matching.html#reduceReplace) 。  
  同样可以执行连续匹配，并替换最终匹配的字符串。

  示例：

  ```aardio
  var str = string.reduceReplace("print(a,(1+2))",
    "\w+(%())",//用 %() 对称匹配外层括号包围的部分
    "\((.+)\)",//获取括号中间的部分
    "\1" //替换为捕获组1
  );

  print(str)
  ```
 

## 禁用模式语法

1. 局部禁用模式语法

    以`<@`开始,并以`@>`结束的非捕获组用于在模式匹配中进行原始的字符串比较，并在匹配目标子串时暂时局部禁用模式匹配语法。

    `<@@`开始,并以`@>`结束的非捕获组则表示忽略大小写进行原始的字符串比较。

    示例：

    ```aardio
    var str = string.match("a\d", "[a-z]<@\d@>")
    ```

    参考: [非捕获组](#non-cap)
 
2. 全局禁用模式语法

    如果在模式串的最开始处加上 `@` 字符，则表示模式串完全禁用模式匹配语法，而使用更快速的原样字符串查找替换功能。这时候在替换函数中，替换字符串里只能是普通字符串，替换字符串里不解析捕获组引用语法，也不能使用替换函数、替换表等基于模式语法的替换对象。

    如果在模式串的最开始处加上 `@@` 字符，同样禁用所有模式语法，并且在进行原始的字符串查找替换操作时忽略大小写。

    实际上这种用法 aardio 在内部将其转换为了禁用模式语法的非捕获分组。

    例如 `@\dabc` 会被转换为 `<@\dabc@>` 。

    而 `@@\\dabc` 会被自动转换为 `<@@\dabc@>` 。

    所以这个功能只是一个语法糖。

      
    例如：

    ```aardio
    var str = string.match("a\d", "@a\d");
    ```

    上面的 `\d` 并不匹配数字，仅仅是匹配原始的  `\d` 字面值，等价于:  

    ```aardio
    var str = string.match("a\d", "a\\d");
    ```

## 限制 <a id="limitations" href="#limitations">&#x23;</a>

- 模式串本身应当是纯文本，`'\0'`为文本终止符。但模式匹配的目标字符串可以是二进制的，在模式串中可以用 `"\z"` 表示 `'\0'`。
- 不能对一个用圆括号指定的捕获组使用模式运算符来指定匹配行为、匹配次数等。 
- 不能对模式串中的捕获组引用使用运算符，除非这个捕获组引用被放在非捕获组内。
- 非捕获组内可以嵌套包含 `<>` 以表示嵌套的非捕获分组，也可以包含  `[]` 以表示自定义字符类，但在  `<>` 内 `{}` 或 `()` 仅具有字面意义（不具有模式语义） ，不能在尖括号包含的非捕获组内使用圆括号 `()` 创建捕获组，也不能在非捕获组内使用 `{n,m}` 表示量词运算符 。  
- 要特别注意尖括号包围的非捕获组内部不支持惰性匹配（ `%` 创建的对称匹配除外 ），非捕获组内的匹配规则不但是贪婪而且是特别贪婪，只要能匹配到内容就会尽力向后走并且不会回溯。但非捕获组本身作为子模式且使用量词运算符时可以支持回溯。
- 在中括号指定的自定义字符类内圆点表示字面值(无模式语义)。  
- 逻辑或运算符 `|` 两侧不能都是表示字面值的普通字符，例如`A|B`必须改为'[AB]'。
- UTF-8 编码的多字节字符（例如中文）不被认为是最小模式匹配单位（子模式），除非将多字节字符放入中括号（自定义字符类）或尖括号（非捕获组）。 
- 匹配任意多字节字符（例如中文）的 `:` 是一个子模式。

## 模式串-效率优化

1. 使用惰性匹配避免不必要的回溯。
  
  多数情况下，在匹配次数运算符后面加上`?` 会更快，这一点非常重要。贪婪匹配会首先尝试最大长度的匹配，匹配失败会向后回溯重试，类似 `(.*)test` 这样过多且不必要的回溯是非常低效的。

2. 限制严格的模式串比一个限制宽松的模式更快。 

例如模式串 `(.*)test` 用来获取 test 以前的全部字符，这个条件太过于宽松，查找时会从目标串的第一个字符开始匹配直到字符串结束，如果没有找到，则从目标串的第二个字符开始再次查找。这样的查找效率是很低的。

解决办法是在模式串最前面加上 `^` 限定只在字符串开始锚点匹配，例如 `^(.*)test`。改用惰性匹配 `^(.*?)test` 找到第一个 test 就停会更快。

3. 严格限制子模式序列的开始字符可以显著提升效率，道理同上。 

4. 不要滥用可能匹配空字符串的模式串，例如`\a*`。 

## 模式匹配与正则表达式的区别 <a id="pattern-matching-vs-regexp" href="#pattern-matching-vs-regexp">&#x23;</a>

aardio 模式匹配基本的语法尽可能沿用了正则表达式常用的一些基本语法，但模式匹配比正则表达式更简单、运行速度也更快。模式匹配与 aardio 语言完全融为一体，与字符串有关每函数大多默认就支持模式匹配语法。 

> 在测试中 string.regex 耗时 455 毫秒的查询，preg 库正则表达式需要 49 毫秒，而模式匹配仅需要 1.4 毫秒。

aardio 模式匹配与传统正则表达式对比： 

| 功能特性          | aardio 模式匹配   | 传统正则表达式    |
|-------------------|-------------------|-------------------|
| 复杂性        | 语法更简洁，限制更多，易于掌握和使用。 | 语法更复杂，功能更强大，但容易陷入“甜蜜陷阱”，导致过于复杂的表达式。 |
| 运行速度          | 模式匹配比正则表达式快数十倍到数百倍 。 | 比模式匹配慢很多。 |
| 捕获组与运算符    | 不能对捕获组 `( )` 使用任何模式运算符（例如量词），捕获组仅用于记录和返回匹配内容。 | 可以对捕获组 `( )` 使用运算符（如量词 `+`, `*` 等），功能更灵活。 |
| 非捕获组          | 使用尖括号 `< >` 定义非捕获组（Non-capturing group），非捕获组是子模式（可以对其使用量词等运算符），也是具有原子性的非捕获分组，内部贪婪匹配且不回溯。非捕获组可嵌套，但不能包含捕获组。例如 `<hello>+` 匹配 `hello` 重复一次或多次。 | 使用 `(?:subpattern)` 定义非捕获组，使用 (?>subpattern)定义原子分组，可以使用对其运算符。|
| 子模式与运算符    | aardio 严格区分子模式（subpattern）和运算符（operator）。运算符只能用于子模式（包括非捕获组），不适用于捕获组。 | 无此限制，运算符可用于捕获组和非捕获组，灵活性更高。 |
| 局部禁用模式语法  | 支持使用 `<@内容@>` 局部禁用模式语法，或 `<@@内容@>` 忽略大小写匹配。 | 无此功能，无法局部禁用正则语法。 |
| 全局禁用模式语法  | 模式串开头加 `@` 或 `@@` 可全局禁用模式语法，转换为原样字符串匹配（更快）。 | 无此功能，正则表达式始终解析语法。 |
| 对称匹配运算符    | 使用 `%` 运算符匹配成对符号及其包含内容，如 `%()` 匹配成对括号。可添加问号表示最近配对，例如 `%()?`。            | 无类似语法，需通过其他方式实现对称匹配。 |
| 边界断言          | 使用 `!p` 实现双向零宽[边界断言](#boundary)，同时进行回顾和预测断言，功能更灵活。 | 使用 `\b` 表示单词边界，或 `(?<!p)` 和 `(?=p)` 分别实现回顾和预测断言。 |
| 预测断言          | 支持 `p?=`（预测断言）和 `p?!`（反预测断言），但不能用于捕获组。 | 支持 `(?=p)`（预测断言）和 `(?!p)`（反预测断言），可用于捕获组。 |
| 回顾断言          | 不支持单独的回顾断言，边界断言 `!p` 同时包含回顾和预测功能。 | 支持 `(?<!p)`（反回顾断言），功能更全面。 |
| 字符类定义        | `\u` 表示大写字母，`\l` 表示小写字母，不支持 Unicode 范围定义。 | `\u` 常用于表示 Unicode 编码，支持 Unicode 字符范围。 |
| 多字节字符处理    | 多字节字符（如中文）不是最小匹配单位，只有放入 `[]` 或 `<>` 中才能作为子模式使用；可用 `:` 表示任意多字节字符。 | 多字节字符处理更灵活，支持 Unicode 范围和属性匹配。 |
| 惰性匹配          | 支持惰性匹配（如 `+?`, `*?`），但非捕获组内部不支持惰性匹配，总是贪婪匹配且不回溯。 | 支持惰性匹配（如 `+?`, `*?`），非捕获组无类似限制。 |
| 捕获组引用        | 使用 `\1` 到 `\9` 引用捕获组，替换字符串中还可用 `\0` 表示整个匹配；不能对捕获组引用使用运算符，除非是在非捕获组内。 | 使用 `$1` 到 `$9` 引用捕获组，可对引用使用运算符。 |
| 适用场景          | 更适合快速、简单的文本和二进制字符串处理，避免复杂度无限上升。 | 适合复杂文本处理，但可能因复杂语法导致性能下降。 |
| 与语言集成        | 与 aardio 语言深度集成，字符串相关函数默认支持模式匹配。 | 需额外导入 [preg](../../../library-reference/preg/_.html) 或 string.regex 等独立库以支持正则表达式。|

> aardio 用单引号包围的 [转义字符串](../../../language-reference/datatype/string.html#backslash-escaped)  内支持以  `\u` 前缀引导的 Unicode 编码，但在转义字符内必须用又反斜杠表示模式转义符，例如 `'\\d'` 与   `"\d"` 表示相同的模式串。

模式匹配与正则的最大区别是正则表达式强大且复杂，模式匹配小、轻、快并易于掌握和使用。很多时候我们要避免“正则表达式甜蜜陷阱”，避免执着于用正则表达式解决复杂问题，把正则表达式实现的过于复杂。把所有的需求试图用一个正则表达式去解决，这是一种非常原始的“写命令行”的思维，并非解决问题的最佳方案。  
  
很多时候更好的解决方案是去编写代码替换复杂的模式串，在 aardio 中文本分析是非常方便的，例如标准库中 string.xml 的源码大家可以看看，里面虽然用到了模式匹配，但并非用一两个模式匹配就能搞定这种复杂的文本分析。 其它的可以看看 bencoding.decoder，string.csv 等等支持库的源代码，这些里面基本没有使用或很少使用模式匹配。  

## 正则表达式 $\rightarrow$ aardio 模式匹配直译示例

- 非捕获组
    *   PCRE: `(?>atomic non-capturing group)`
    *   aardio: `<atomic non-capturing group>`
- 非捕获组加量词
    *   PCRE: `(?:abc)+` 
    *   aardio: `<abc>+`
- 或运算符
    *   PCRE: `(?>ab|bc)`
    *   aardio: `<<ab>|<bc>>` 或 `<ab>\|<bc>`
- 断言
    *   PCRE: `\d(?!后面不能是我)(?=后面必须是我)`
    *   aardio: `\d<后面不能是我>?!<后面必须是我>?=`
- 边界
    *   PCRE: `\bword\b`
    *   aardio: `!\wword!W` 
  