模式表达式语法

参考:模式匹配函数

模式表达式(Text Pattern Expressions)

模式表达式使用模式匹配语法组成的表达式执行特定语义的文本匹配、以用于文本查找、替换操作。
模式表达式由表示字面值的字符(Literal Characters)、表示特定文本集合的模式(Pattern)、以及表示特定匹配规则的修饰符组成.

注意在aardio的模式匹配中,模式与表达式有不同的语义,
在aardio中一个模式表达式中可以包含模式串,也可以包含非模式串,这与传统的模式匹配有较大区别,
而模式元可以匹配一个字符,也可以匹配一个串,它们同样是最小的匹配单元,所以aardio中没有元字符一说,只有模式元.

基本概念

1、模式匹配(Pattern matching)

使用模式匹配函数、并使用特定的模式语法组成的模式表达式查找匹配有规则的字符串。

2、模式表达式(Pattern Expressions)

根据模式语法编写的表达式,用来查找匹配有规则的目标字符串。

2、模式串(Pattern String)

存放模式表达式的字符串对象。

4、模式元(Pattern)

模式元是模式表达式中的最小的匹配单元,用于表示特定的字符集合.模式元指模式串中的一个普通字符、或转义后的字符、或用转义符标记的字符类、或一个置于中括号([])中的字符类,或一个置于尖括号(<>)中的串匹配

5、模式修饰符(Pattern Modifiers)

用于指定模式的匹配次数、匹配规则。.

模式 - 转义符

转义符用于将表示字面值的普通字符转换为特殊的模式符号.
对于特定的模式符号,在前面加上转义符,可用于表示普通的字面值字符.

转义符的作用:

  1. 使用“\转义符 + 特定字母字符”表示特定的字符类
  2. 使用“\转义符 + 数字”表示向前引用匹配分组(正则表达式中每对括号标明一个匹配分组),
  3. 使用“\转义符 + 标点符号”表示标点符号字面值本身(即取消原来的模式语义),例如“\\”表示“\”。

在aardio中,置于单引号中的字符也可以使用\转义符表示特定意义,例如:\n表示换行。这种字符串转义符与模式转义符有类似的作用,但是用途有较大区别.

因此模式串置于单引号内时,应在所有\前面再加一个\以表示模式表达式中的转义符。例如模式串:"\w+",置于单引号中应表示为:'\\w+'。一般为避免混淆以及书写方便,模式字符串应置于双引号(")或反引号(`)内(此时\转义符仅适用于模式语义)。

模式 - 任意字符

圆点'.'表示任意单字节字符.
冒号':'表示任意多字节字符(中文字符)
串匹配里的冒号后面有+,*修饰符 ,例如:"<:+>" 表示任意多字节字符

模式 - 字符类

使用“\转义符 + 特定字母字符”表示特定的字符类,

\a 字母
\c 控制字符
\d 数字
\i ASCII 字符( 字节码 < 0x80 )
\l 小写字母
\p 标点字符
\s 空白符
\u 大写字母
\w 字母和数字、以及下划线
\x 十六进制数字
\n 换行符
\r 回车符
\f 换页符 '\x0c'
\v 匹配一个垂直制表符。等价于 '\x0b'
\z 表示'\0'
注意模式串是纯文本,'\0'表示模式串结束,所以需要用"\z"表示'\0'
上面字符类的大写形式表示小写所代表的集合的补集

\A 不是字母的字符
\C 不是控制字符的字符
\D 不是数字的字符
\I 不是ASCII字符( 字节码 >= 0x80 )
\L 不是小写字母的字符
\P 不是标点的字符
\S 不是空白符的字符
\U 不是大写字母的字符
\W 不是字母和数字、并且不是下划线的字符
\X 不是十六进制数字的字符
\N 不是换行符
\R 不是回车符
\F 不是换页符
\V 不是垂直制表符
\Z 不是'\0'的字符

上面字符类的大写形式表示小写所代表的集合的补集, 这些字符类仅匹配单个字节(包含单字节字符,或者宽字符的单个字节)。

 模式 - 自定义字符类  

1、自定义字符类

语法示例:

"[abc0-9\n]"


方括号用来创建自定义的字符类,
例如[abcd]匹配一个可能是abcd其中之一的字符。[\a\d_]匹配数字、字母和下划线。
我们还可以在两个字符之间用连字符连接表示这两个字符之间范围内的字符集合。例如[0-7]等同于[01234567]。
自定义字符集中可以包含UTF8编码的多字节汉字(但不可指定编码范围,有些需求请使用preg正则支持库)。
自定义字符集中冒号表示任意中文字符而不是字面值。

在「自定义字符类」里圆点表示字面值,而不是表示任意匹配, 但是冒号表示任意中文字符而不是字面值。


2、自定义字符类补集

语法示例

"[^abc0-9\n]"

在字符集的开始处使用 `^′ 表示其补集,'[^0-7]' 匹配任何不是八进制数字的字符; '[^\n]' 匹配任何非换行符户的字符。

在「自定义字符类补集」里圆点表示字面值,「自定义字符类补集」中使用中文字符或:无意义(即使不匹配中文字符也可以匹配二进制字节)。

模式 - 字符串匹配

1、字符串匹配

语法示例:

<abc0-9\n\d+:+>


尖括号匹配一组有序的字符串而不是单个字符。
例如<hello>匹配hello单词。而不是hello其中的一个字符。

在串匹配的尖括号内部也可以使用字符类,或者自定义的字符类,其语法类似使用[]定义字符类的用法。
例如:<\aA-Z>

你也可以在串匹配中插入用中括号来定义的自定义字符类,如下:
<[a-z0-9]>
在串匹配内可以使用圆点表示任意单字节字符,使用冒号表示任意多字节字符(汉字).
而在中括号定义的字符类内圆点仅表示字面值.

可在串匹配内使用'+'或'*'限定修饰符表示贪婪匹配,可使用逻辑修饰符'|'
但是不能在尖括号内部使用其他修饰符,不能使用问号来定义惰性匹配.

不能在尖括号内部使用圆括号来定义捕获组.


2、反义串匹配

示例语法:

<^a-zA-Z\d*>

例如 <^hello> 匹配所有不是hello并且等长的字符串.如果是在串的结束,则允许至少匹配一个字符.

3、原始串匹配

示例语法:

<@任意内容@>

例如 <@a-zbc@> 匹配"a-zbc",而不能匹配"abc"

@表示在尖括号内部禁用模式语法.这是aardio模式语法中非常有用的一个功能,它可以在一个查找串中对部分字符串禁用模式语法.

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

这在批量处理文本时经常遇到,要么细心的组织表达式,或者编写大量的代码来实现该逻辑,先查出开始段,再找出...............总之是很麻烦的一件事。

在模式匹配中使用临时禁用模式语法的@字符,能彻底的解决这一问题,使模式语法的使用更为简单,功能更为强大.当然,因为可以按需禁用模式语法,查找速度也会显著的提升.

模式元匹配修饰符

修饰符用于指定一个模式元的匹配次数、或是否消费字符宽度

修饰符 说明
pattern{min,max}
匹配前一模式最少min次,最多max次
可以省略其中一个参数,或者仅用一个参数限定匹配长度,例如:
pattern{min,}
pattern{,max}
pattern{len}
pattern+ 匹配pattern模式1次或多次,等价于{1,}
pattern* 匹配pattern模式0次或多次,类似{0,}
pattern 匹配pattern模式0次或1次,类似{0,1}
pattern?= 向右预测式零宽断言,测试pattern模式是否可匹配1次,不消费任何字符宽度
pattern?! 向右预测式零宽断言(逻辑取反),测试pattern模式是否不匹配至少1次,不消费任何字符宽度

贪婪匹配:又称之为最长匹配,{min,max},+,* 等可匹配1次以上的修饰符默认都会在指定语义下尽可能获得最长的匹配结果,除非在这几个修饰符后面再加一个?号将其转换为惰性匹配。

对匹配次数修饰符指定惰性匹配

惰性匹配:指在限定符后面附加一个问号(?),以表示使用最短匹配.又称之为非贪婪匹配.
例如,对于字符串 "aaaaaa",'a+?' 将匹配单个 "a",而 'a+' 将匹配所有 'a'。

io.open();

str = "a1234z"

str2 = string.match(str, "a\d*?")
io.print(str,"a\d*?",str2); /*附加?号表示最短匹配*/

str2 = string.match(str, "a\d*")
io.print(str,"a\d*",str2); /*默认最长匹配*/

str2 = string.match(str, "a\d+\d")
io.print(str,"a\d+\d",str2);/*最长匹配*/

str2 = string.match(str, "a\d+?\d")
io.print(str,"a\d+?\d",str2);/*显示a12,附加?号表示最短匹配 */

str2 = string.match(str, "a\d{2,3}")
io.print(str,"a\d{2,3}", str2); /*显示a123, 也是最长匹配*/

str2 = string.match(str, "a\d{2,3}?")
io.print(str,"a\d{2,3}?",str2); /*显示a12,附加?号表示最短匹配*/

边界断言

语法:

!pattern

边界断言用一个感叹号加一个自定义的模式元组成,用于判断当前位置是从不满足该匹配条件切换到满足该条件的字符串分界。边界断言是一种零宽断言(Zero-width Assertions)语法,匹配时只是做断言检测,但并不消费任何字符宽度,也就是说下一次匹配仍然是从当前位置开始。

该语法类似正则表达式的Lookaround(Lookahead and Lookbehind)特性,可以用正则表达式描述类似的语义如下:"(?<!pattern)(?=pattern)" 可以理解为一个向左回顾式零宽断言(逻辑取反) + 向右预测式零宽断言。 ( 本文档没有沿用:“负向向后零宽断言” - 这类找不到方向感的术语,请厘清边界的前后方向、"look"着的前后方向、以及逻辑正反向 )。


例如 "!\w([a-zA-Z]\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();
 
 

可以看到正则表达式、模式匹配的匹配结果是一样的,模式匹配的写法虽然简洁 - 但功能没有正则表达式的零宽断言那么强大(只能用于表达式的最小单位:模式元)。

模式元 逻辑修饰符

在一个或多个模式元(不能都是普通字符)中间添加一个'|'字符以表示匹配其中任意一个模式,称之为逻辑或匹配.
逻辑或匹配返回其中最早匹配成功的一个匹配结果.注意'|'两侧不能都是普通字符,'A|B'可以用'[AB]'表示就可以了。

注意逻辑或操作符匹配成功后不会回退,例如:

string.match("https://","^<http>|<https>|\:") 将不会匹配成功,匹配时<http>匹配成功,而s不匹配\:,匹配将会失败。

我们需要修改为 string.match("https://","^<https>|<http>|\:") 这样 http:,https: 开头的网址都能匹配成功, 如果要忽略大小写,需要改为 string.match("https://","^<@@https@>|<@@http@>|\:") 。
小提示:标准库里有个更简单的函数 inet.url.is(url)可以直接判断一个字符串是不是网址。

在一个或多个模式元(不能都是普通字符)中间添加一个'&'字符以表示匹配其中所与模式,称之为逻辑与匹配 注意'&'两侧不能都是普通字符,'A&B'这样写是无意义的。

逻辑与匹配返回其中最长匹配结果.参与匹配的模式元中包含串,返回结果将按最长的串返回匹配结果.

与其他修饰符不同,逻辑修饰符可以连接任意多个模式元( 逻辑修饰符两侧不能都是普通字符 ),下面是一个示例

io.open()

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

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

修饰符% 对称匹配

% 用来匹配两个对称的模式元及其位置区间内包含的字符串,例如'%()'匹配以`(′开始, 以 `)′结束的字符串:,而'%""'匹配以引号开始, 以引号结束的字符串:

str = 'a = (a(b)cd) ' //字符串如果包含双引号则应放在单引号中

str2 = string.match(str, '%()')

io.open();
io.print(str2); //显示 (a(b)cd)

对于匹配也可以在后面附加问号(?)以表示惰性匹配(最短匹配),例如:

str = 'a = (a(b)cd) ' //字符串如果包含双引号则应放在单引号中

str2 = string.match(str, '%()?')

io.open();
io.print(str2); //显示 (b)


起止限定符

 

限定符 说明
^ 模式串必须从文本的开始处匹配
$ 模式串必须匹配至文本结束处

起止限定符也属于零宽断言语法(测试匹配条件,但是不消费字符宽度),下面是示例:

import win;
str = "1234"

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

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

修饰符@ 禁用模式语法

1、全局禁用模式语法

aardio提供一系列用于模式匹配的函数,例如string.find string.match string.gmatch 等等。
默认的,查找替换的字符串都会使用模式语法进行解析,如果我们需要禁用默认的模式语义,可以在模式串前面添加'@'字符禁用模式语法,而使用更快速的文本查找替换功能(这时候替换对象只能是字符串,不能用函数,表等作为替换参数)。

例如:

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

这里的\d不是表示数字,仅仅是表示\d,等价于:
str = string.match("a\d", "a\\d")。 

如果在模式串前增加两个"@"字符,表示禁用模式匹配并忽略大小写,实现上aardio会将其转换为"<@@.....@>"


2、局部禁用模式语法

也可在模式表达式的一个串匹配中禁用模式语法,
串匹配以'<@'开始,并以'@>'结束则表示进行原始的字符串比较,并在此子串中忽略所有模式语法.

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


如果串匹配以两个@@字符开始,即以'<@@'开始,并以'@>'结束,则表示进行原始的文本比较(不包含'\0'),忽略所有模式语法,并且忽略大小写


参考: 串匹配

捕获组(capture group)

将模式串的一部分用圆括号 () 括起来可以指定一个匹配分组,
匹配分组不能使用任何限定符进行修饰,可使用空的分组即"()"返回一个索引值。

()可以包含() ,也可以包含[] 或 {}、<>等。
而[]{}<>等指定的模式元不能用任何括号表示模式语义(表示字面值、或用转义符转义为字面值除外)

我们可以在模式中使用'\d'(d代表1-9的数字) 表示第d个捕获分组。

对于string.match string.gmatch 等模式匹配函数,每个用括号显式指定的分组都会增加一个返回值.

限制

1、你不能在尖括号包含的匹配串内使用圆括号指定捕获分组,但是可以使用中括号定义字符类。
2、你不能对一个用圆括号指定的捕获分组使用模式修饰符来指定匹配行为、匹配次数等。
3、在中括号指定的自定义字符类内圆点与冒号表示字面值(无模式语义)。

字符串转义符、模式转义符的区别

请参考:字符串

放在单引号中的字符串支持"\"转义符,例如'\n'表示换行符.
这称为字符串转义符,字符串转义符是发生在编译过程中的.

而模式表达式是一个字符串,即模式串,模式串中也支持"\"转义符,而这是发生在运行时的.
例如模式串"\a",用转义字符串来表示就是'\\a'

无论你写'\\a' 还是 "\a" 这都是编译过程中的事,它们的数据是一样的,当然也是同样的模式串.

最好将模式串置于双引号中,这样仅支持模式转义符,可以避免不必要的混淆.

模式表达式与正则表达式的区别

模式匹配与aardio语言完全融为一体,很多标准库的函数默认就支持模式匹配语法。
模式表达式基本的语法参考了正则表达式的语法,但比正则表达式更简单、运行速度也更快。 实际上在aardio的设计中一直在尽可能的运用大家现有的、传统的知识和书写习惯,以降低学习成本。当我们在不同语言、不同技术间切换时,很多不必要的差异会导致不必要的混乱、不必要的记忆成本。aardio在设计的每一个细节上都努力避免这一点。当然,我们仍然要注意正则表达式与模式匹配的一些区别,例如:

正则表达式一个强大的功能就是可以对圆括号指定的捕获组指定匹配次数,而模式匹配并不支持此功能,模式匹配只能对表达式中的最小单位“模式元”使用修饰符设定匹配次数。 为了可以对一个子字符串设定匹配次数 - 模式匹配提供了串匹配语法,但仍然没有正则表达式那么强大的功能。

但是模式匹配在串匹配可以使用'@'字符全局禁用、或局部串禁用模式语法,或指定局部串忽略大小写,正则表达式无此功能,这个功能在匹配大段的文本时尤其有用。

模式匹配%表示成对出现的符号及包含的字符串,正则表达式没有类似的语法。

模式匹配提供边界断言功能可以实现简单的自定义零宽断言以及边界测试,正则表达式使用\b表示单词边界,另外正则表达式提供更强大的自定义零宽断言(Lookaround)。

正则表达式中\u表示unicode编码,而模式匹配中\u表示大写字符,\l表示小写字符 。

模式匹配与正则的最大匹别是正则表达式强大且复杂,模式匹配更简单、小、轻、快,并易于掌握和使用。
Jamie Zawinski有一句名言被很多正则表达式的资料所引用:“Some people, when confronted with a problem, think “I know, I’ll use regular expressions.” Now they have two problems”
很多时候我们要避免这种“正则表达式甜蜜陷阱”,避免执着于用正则表达式解决复杂问题,把正则表达式实现的过于复杂。把所有的需求试图用一个正则表达式去解决,这是一种非常原始的“写命令行”的思维,并不是一个良好的编写程序解决问题的思路。

很多时候更好的解决方案是去编写程序,在aardio中文本分析是非常方便的,
例如标准库中string.xml的源码可以看看,里面虽然用到了模式匹配,但并非指望一两个模式匹配就能搞定这种复杂的文本分析。 其它的可以看看bencoding.decoder,string.database等等支持库,这些里面基本没有使用或很少使用模式匹配。

我们经常看到新手在询问我这个文本分析的想法怎么用一个模式匹配解决?解决了以后我现在有新的更复杂的想法了,我该怎么把模式表达式改的更复杂以解决新的问题? 实际上当你把模式匹配越写越复杂的时候,你就要提醒自己可能掉入“正则表达式甜蜜陷阱”了。

那么aardio中的模式匹配之所以设计的比正则表达式简单,其用意也正在于此。
一些新手会纠结:“你都说了跟正则表达式基本语法一样了,那么我用正则的这个那个功能,你模式匹配里没有”,其实没有必要去纠结这个。 aardio中也提供了正则表达式的良好支持,例如string.regex,preg 等正则表达式支持库。

模式表达式-效率优化

1、惰性匹配比贪婪匹配更快

在匹配次数修饰符后面加上"?"总是会更快.这一点非常重要。实际上贪婪匹配是由无数的惰性匹配组成的,请把模式匹配想象为吃豆子游戏。惰性匹配相当于吃到一粒豆子就停了。而贪婪匹配则不停的吃更多的豆子,直到吃完所有的豆子找不到新的豆子为止,可以想象贪婪匹配是多么低效了。

2、限制严格的模式表达式比一个限制宽松的模式更快。

3、严格限制模式表达式匹配的开始字符可以显著提升效率。

例如模式表达式"(.*)test" 用来获取test以前的全部字符
上面的算法从目标串的第一个字符开始匹配直到字符串结束,
如果没有找到,则从目标串的第二个字符开始再次查找。
这样的查找效率是很低的。

解决办法是加上限定符"^(.*)test" 限定仅与目标串的开始字符匹配。

4、尽可能不要使用可能匹配空字符串的模式表达式。

例如"\a*",因为可以出现零次则空字符串也是符合条件的。
应尽量避免使用。并且永远不要在模式串开始处使用。