模式的设计与实现
大多数模式匹配库都是用反斜杠\
作为转义符。然而,这种方式可能会导致一些不良的后果。对于 Lua
语言的解释器而言,模式仅仅是普通的字符串。模式与其他的字符串一样遵循相同的规则,并不会被特殊对待。只有模式匹配相关的函数才会把他们当成模式进行解析。由于反斜杠是 Lua
语言中的转义符,所以我们应该避免将它传递给任何阐述。模式本身就难以阅读,到处把 \
换成 \\
就更加火上浇油了。
我们可以使用双括号把模式括起来构成的长字符串来解决这个问题(某些语言在实践中推荐这种方法)。然而,长字符串的写法对于通常比较短的模式而言又往往显得冗长。此外,我们还会失去在模式内进行转义的能力(某些模式匹配工具通过再次实现常见的字符串转义来绕过这种限制)。
Lua
语言的解决方案更加简单: Lua
语言中的模式使用百分号作为转义符( C
语言中的一些函数采用的也是同样的方式,例如函数 printf
和函数 strftime
)。总体上,所有被转义的字母都具有某些特殊含义(例如 "%a"
匹配所有字母),而所有被转义的非字母则代表其本身(例如 "%."
匹配一个点)。
字符分类
所谓字符分类,就是模式中能够与一个特定集合中的任意字符像匹配的一项。例如:分类 %d
匹配的是任意数字。因此,可以使用模式 "%d%d/%d%d/%d%d%d%d"
来匹配 dd/mm/yyyy
格式的日期:
s = "Deadline is 01/03/2022" date = "%d%d/%d%d/%d%d%d%d" print(string.match(s, date)) --> 01/03/2022
下表列出了所有预置的字符分类及其对应的含义:
字符分类 | 含义 |
. | 任意字符 |
%a | 字母 |
%c | 控制字符 |
%d | 数字 |
%g | 除空格外的可打印字符 |
%l | 小写字母 |
%p | 标点符号 |
%s | 空白字符 |
%u | 大写字母 |
%w | 字母和数字 |
%x | 十六进制数字 |
这些字符分类的大写形式表示类的补集。例如: "%A"
代表任意非字母的字符:
print((string.gsub("hello, up-down!", "%A", "."))) --> hello..up.down.点击复制复制失败已复制
提示
在输出函数 gsub
的返回结果时,我们使用了额外的括号来丢弃第二个结果,也就是替换发生的次数—— 4
。
魔法字符
在模式中使用时,还有一些被称为魔法字符的字符具有特殊含义。 Lua
语言的魔法字符包括: (
)
、 .
、 %
+
、 -
、 *
、 ?
、 [
、 ]
、 ^
、 $
正如我们之前看到的,百分号%
同样可以用于这些魔法字符的转义。因此, "%?"
匹配一个问号?
, "%%"
匹配一个百分号%
。我们不仅可以用百分号对魔法字符进行转义,还可以将其用于其他所有字母和数字外的字符。当不确定是否需要转义时,为了保险起见就可以使用转义符。
字符集
字符集用于创建自定义的字符分类,只需要在方括号内将单个字符和字符分类组合起来即可。例如,字符集 "[%w_]"
匹配所有以下划线结尾的字母和数字, "[01]"
匹配二进制数字, "[%[%]]"
匹配方括号。如果想要同级一段文本中元音的数量,可以使用如下的代码:
_, nvow = string.gsub(text, "[AEIOUaeiou]", "")
还可以在字符集中包含一段字符范围,做法是写出字符范围的第一个字符和最后一个字符并用横线( -
)将他们连接在一起。由于大多数常用的字符范围都被预先定义了,所以这个功能很少被使用。例如: "%d"
相当于 "[0-9]"
, "%x"
相当于 "[0-9a-fA-F]"
。不过,如果需要查找一个八进制的数字,那么使用 "[0-7]"
就比显式地枚举 "[01234567]"
强多了。
在字符集前加一个补字符^
就可以得到这个字符集对应的补集:模式 "[^0-7]"
代表所有除八进制数字以外的字符,模式 "[^\n]"
则代表所有除换行符以外的其他字符。尽管如此,我们还是要记得对于简单的分类来说可以使用大写形式来获得对应的补集: "%S"
显然要比 "[^%s]"
更简单。
修饰符
修饰符用来描述模式中重复和可选的部分,让模式更加有用。 Lua
语言中的模式提供了 4
中修饰符:
修饰符 | 作用 |
+ | 重复一次或多次 |
* | 重复0次或多次 |
- | 重复0次或多次(最小匹配) |
? | 可选(出现0次或一次) |
+
修饰符 +
匹配原始字符分类中的一个或多个字符,它总是获取与模式相匹配的最长序列。例如:模式 "%a+"
代表一个或多个字母(即一个单词):
print((string.gsub("one, and two; and three", "%a+", "word"))) --> word, word word; word word点击复制复制失败已复制
模式 "%d+"
匹配一个或多个数字(一个整数):
print(string.match("the number 1298 is even", "%d+")) --> 1298
*
修饰符 *
类似于修饰符 +
,但是他还接受对应字符分类出现 0
次的情况。该修饰符一个典型的用法就是在模式的部分之间匹配可选的空格。例如,为了匹配像 ()
和 ( )
这样的空括号对,就可以使用模式 "%(%s*%)"
,其中的 "%s*"
匹配 0
个或多个空格(括号在模式中有特殊含义,所以必须进行转义)。另一个示例是用模式 "[_%a][_%w]*"
匹配 Lua
程序中的标识符:标识符是一个由字母或下划线开头,并紧跟着 0
个或多个由下划线、字母或数字组成的序列。
-
修饰符 -
和修饰符 *
类似,也是用于匹配原始字符分类的 0
次或多次出现,不过,跟修饰符 *
总是匹配能匹配的最长序列不同。修饰符 -
只会匹配最短序列。虽然有时他们两者并没有什么区别,但大多数情况下这两者会导致截然不同的结果。例如,当试图用模式 "[_%a][_%w]-"
查找标识符时,由于 "[_%w]-"
总是匹配空序列,所以我们只会找到第一个字母。又如,假设我们想要删掉 C
语言程序中的所有注释,通常会首先尝试使用 "/%*.*%*/"
(即 "/*"
和 "*/"
之间的任意序列,使用恰当的转义符对 *
进行转义)。然而,由于 ".*"
会尽可能长地匹配,因此程序中的第一个 "/*"
只会与最后一个 "*/"
相匹配:
test = "int x; /* x */ int y; /* y */" print((string.gsub(test, "/%*.*%*/", ""))) --> int x;
相反,模式 ".-"
则只会匹配到找到的第一个 "*/"
,这样就能得期望的结果:
test = "int x; /* x */ int y; /* y */" print((string.gsub(test, "/%*.-%*/", ""))) --> int x; int y;
?
修饰符 ?
用于匹配一个可选的字符。例如:假设我们想在一段文本中寻找一个整数,而这个整数可能包括一个可选的符号,那么就可以使用模式 "[+-]?%d+"
来完成这个需求,该模式可以匹配像 "-12"
、 "23"
和 "+1009"
这样的数字。其中,字符分类 "[+-]"
匹配加号或减号,而其后的问号则代表这个符号是可选的。
修饰符特点
与其他系统不同的是, Lua
语言中的修饰符只能作用于一个字符模式,而无法作用于一组分类。例如,我们不能写出匹配一个可选的单词的模式(除非这个单词只由一个字母组成)。通常,可以使用一些高级技巧来绕开这个限制。
锚定模式
以补字符^
开头的模式表示从目标字符串的开头开始匹配。类似的,以 $
结尾的模式表示匹配到目标字符串的结尾。我们可以同时使用这两个标记来限制匹配查找和锚定模式。例如,如下的代码可以用来检查字符串 s
是否以数字开头:
if string.find(s, "^%d") then ... end
如下代码用来检查字符串是否为一个没有多余前缀字符和后缀字符的整数:
if string.find(s, "^[+-]?%d+$") then ... end
^
和 $
字符只有位于模式的开头和结尾时才具有特殊含义;否则,他们仅仅就是与其自身相匹配的普通字符。
模式 "%b"
匹配成对的字符串,它的写法是 "%bxy"
,其中 x
和 y
是任意两个不同的字符, x
作为起始字符而 y
作为结束字符。例如,模式 "%b()"
匹配以左括号开始并以对应右括号结束的子串:
s = "a (enclosed (in) parentheses) line" print((string.gsub(s, "%b()", ""))) --> a line
通常,我们使用 "%b()"
、 "%b[]"
、 "%b<>"
、 "%b{}"
等作为模式,但实际上可以用任意不同的字符作为分隔符。
前置模式
模式 "%f[char-set]"
代表前置模式。该模式只有在后一个字符位于 char-set
内而前一个字符不在时匹配一个空字符:
s = "the anthem is the theme" print((string.gsub(s, "%f[%w]the%f[%W]", "onw"))) --> one anthem is one theme点击复制复制失败已复制
模式 "%f[%w]"
匹配位于一个非字母或数字的字符和一个字母或数字的字符之间的前置,而模式 "%f[%W]"
则匹配一个字母或数字的字符和一个非字母或数字的字符之间的前置。因此,指定的模式只会匹配完整的字符串 "the"
。
注意
即使字符集只有一个分类,也必须把它用括号括起来。
前置模式把目标字符串中第一个字符前和最后一个字符后的位置当成空字符( ASCII
编码的 \0
)。在前例中,第一个 "the"
在不属于集合 "[%w]"
的空字符和属于集合 "[%w]"
的 t
之间匹配了一个前置。