当前位置: 首页 > news >正文

Lua 第10部分 模式匹配

10.1  模式匹配的相关函数

        字符串标准库提供了基于模式的 4 个函数。 我们已经初步了解过函数 find gsub,其余两个函数分别是 match gmatch (Global Match 的缩写)。

        函数 string.find 用于在指定的目标字符串中搜索指定的模式。最简单的模式就是一个单词,它只会匹配到这个单词本身。例如,模式’ hello ’ 会在目标字符串中搜索子串 ” hello ”。函数 string.find 找到一个模式后,会返回两个值 : 匹配到模式开始位置的索引和结束位置的索引 。 如果没有找到任何匹配,则返回 nil:

> s = "hello world"
> i, j = string.find(s, "hello")
> print(i, j)
1       5
> print(string.sub(s,i,j))
hello
> print(string.find(s,"world"))
7       11
> i,j=string.find(s,"l")
> print(i,j)
3       3
> print(string.find(s,"lll"))
nil

匹配成功后,可以以函数 find 返回的结果为参数调用函数 string.sub 来获取目标字符串中匹配相应模式的子串。对于简单的模式来说,这一般就是模式本身。

        函数 string.find 具有两个可选参数。第3个参数是一个索引,用于说明从目标字符串的哪个位置开始搜索。第4个参数是一个布尔值,用于说明是否进行简单搜索。字如其名,所谓简单搜索就是忽略模式而在目标字符串中进行单纯的“查找子字符串”的动作:

> string.find("a [word]", "[")
stdin:1: malformed pattern (missing ']')
> string.find("a [word]", "[", 1, true)
3       3

由于 ’[’ 在模式中具有特殊含义,因此第 1 个函数调用会报错。 在第 2 个函数调用中,函数只是把 ’[’ 当作简单字符串 。请注意,如果没有第 3 个参数,是不能传入第 4 个可选参数的

10.1.2  函数 string.match

        由于函数 string.match 也用于在一个字符串中搜索模式,因此它与函数 string.find非常相似。 不过,函数 string.match 返回的是目标字符串中与模式相匹配的那部分子串,而非该模式所在的位置:

> print(string.match("hello world", "hello"))
hello

对于诸如 'hello' 这样固定的模式,使用这个函数并没有什么意义。 然而,当模式是变量时,这个函数的强大之处就显现出来了,例如 :

> date="Today is 17/7/1990"
> d=string.match(date,"%d+/%d+/%d+")
> print(d)
17/7/1990

后续,我们会讨论模式 '%d+/%d+/%d+' 的含义及函数 string.match 的更高级用法。

10.1.3  函数 string.gsub 

        函数 string.gsub 有 3 个必选参数 :目标字符串、模式和替换字符串,其基本用法是将目标字符串中所有出现模式的地方换成替换字符串

> s=string.gsub("Lua is cute","cute","great")
> print(s)
Lua is great
> s=string.gsub("all lii", "l", "x")
> print(s)
axx xii
> s=string.gsub("Lua is great","Sol","Sun")
> print(s)
Lua is great

此外, 该函数还有一个可选的第 4 个参数,用于限制替换的次数

> s=string.gsub("all lii", "l", "x",1)
> print(s)
axl lii
> s=string.gsub("all lii", "l", "x",2)
> print(s)
axx lii

        除了替换字符串以外 , string.gsub 的第 3 个参数也可以是一个函数或一个表,这个函数或表会被调用(或检索)以产生替换字符串 。

        函数 string.gsub 还会返回第 2 个结果,即发生替换的次数。

10.1.4  函数 string.gmatch

        函数 string.gmatch 返回一个函数,通过返回的函数可以遍历一个字符串中所有出现的指定模式。 例如 ,以下示例可以找出指定字符串 s 中出现的所有单词 :

s = "some string"
words = {}
for w in string.gmatch(s, "%a+") dowords[#words + 1] = w 
end

后续我们马上会学习到,模式"%a+" 会匹配一个或多个字母组成的序列( 也就是单词)。因此 , for 循环会遍历所有目标字符串中的单词,然后把它们保存到列表 words中 。

10.2 模式

        大多数模式匹配库都使用反斜杠作为转义符。 然而,这种方式可能会导致一些不良的后果。 对于 Lua 语言的解析器而言,模式仅仅是普通的字符串。模式与其他的字符串一样遵循相同的规则 ,并不会被特殊对待;只有模式匹配相关的函数才会把它们当作模式进行解析。由于反斜杠是 Lua 语言中的转义符,所以我们应该避免将它传递给任何函数。模式本身就难以阅读,到处把”\” 换成”\\” 就更加火上浇油了 。
        我们可以使用双括号把模式括起来构成的长字符串来解决这个问题(某些语言在实践中推荐这种办法)。然而,长字符串的写法对于通常比较短的模式而言又往往显得冗长。此外, 我们还会失去在模式内进行转义的能力(某些模式匹配工具通过再次实现常见的字符串转义来绕过这种限制 )。
        Lua 语言的解决方案更加简单: Lua 语言中的模式使用百分号作为转义符( C 语言中的一些函数采用的也是同样的方式,如函数 printf 和函数 strftime )。 总体上,所有被转义的字母都具有某些特殊含义(例如 ’%a’ 匹配所有字母),而所有被转义的非字母则代表其本身(例如 ’%.’ 匹配一个点)。
        我们首先来学习字符分类的模式。 所谓字符分类,就是模式中能够与一个特定集合中的任意字符相匹配的一项。 例如,分类%d 匹配的是任意数字。因此,可以使用模式"%d%d/%d%d/%d%d%d%d"来匹配 dd/mm/yyyy 格式的日期:

> s="Deadline is 30/05/1990, firm"
> date="%d%d/%d%d/%d%d%d%d"
> print(string.match(s,date))
30/05/1990

下表列出了所有预置的字符分类及其对应的含义:

.任意字符
%a字母
%c控制字符
%d数字
%g除空格外的可打印字符
%l小写字母
%p标点符号
%s空白字符
%u大写字母
%w字母和数字
%x十六进制数字

这些类的大写形式表示类的补集。 例如,”%A’ 代表任意非字母的字符:

> print((string.gsub("hello,up-down!", "%A", ".")))
hello.up.down.

在输出函数gsub的返回结果时,我们使用了额外的括号来丢弃第二个结果,也就是替换发生的次数。

        当在模式中使用时,还有一些被称为魔法字符的字符具有特殊含义。Lua 语言的模式所使用的魔法字符包括 :( ).   %   +   -   *   ?   [   ]   ^   $

        正如我们之前已经看到的,百分号同样可以用于这些魔法字符的转义。因此,’%?’ 匹配一个问号,’%%’ 匹配一个百分号。 我们不仅可以用百分号对魔法字符进行转义,还可以将其用于其他所有字母和数字外的字符。 当不确定是否需要转义时,为了保险起见就可以使用转义符。

        可以使用字符集来创建自定义的字符分类,只需要在方括号内将单个字符和字符分类组合起来即可。例如,字符集'[%w_]'匹配所有以下画线结尾的字母和数字,'[01]'匹配二进制数字,'[%[%]]' 匹配方括号。 如果想要统计一段文本中元音的数量,可以使用如下的代码:

_, nvow = string.gsub(text, "[AEIOUaeiou]", " ")

还可以在字符集中包含一段字符范围,做法是写出字符范围的第一个字符和最后一个字符并用横线将它们连接在一起。 由于大多数常用的字符范围都被预先定义了,所以这个功能很少被使用 。 例如,'%d' 相当于'[0-9]','%x' 相当于 '[ 0-9a-fA-F]'。不过,如果需要查找一个八进制的数字,那么使用'[ 0-7]' 就比显式地枚举 '[01234567]' 强多了 。

        在字符集前加一个补字符 ^ 就可以得到这个字符集对应的补集: 模式'[^0-7]'代表所有八进制数字以外的字符,模式 '[^\n]' 则代表除换行符以外的其他字符。 尽管如此 ,我们还是要记得对于简单的分类来说可以使用大写形式来获得对应的补集:'%S' 显然要比'[^%s]'更简单。

        还可以通过描述模式中重复和可选部分修饰符( modifier ,在其他语言中也被译为限定符)来让模式更加有用 。 Lua 语言中的模式提供了 4 种修饰符:

+重复一次或多次
*重复零次或多次
-重复零次或多次(最小匹配)
?可选(出现零次或一次)

        修饰符 + 匹配原始字符分类中的一个或多个字符,它总是获取与模式相匹配的最长序列 。 例如,模式 ’%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

        修饰符*类似于修饰符 + ,但是它还接受对应字符分类出现零次的情况。 该修饰符一个典型的用法就是在模式的部分之间匹配可选的空格。例如,为了匹配像()或 () 这样的空括号对,就可以使用模式 ’%(%s*%)’ ,其中的 '%s*' 匹配零个或多个空格( 括号在模式中有特殊含义,所以必须进行转义 ) 。 另一个示例是用模式 '[_%a] [_%w]*' 匹配 Lua 程序中的标识符 : 标识符是一个由字母或下画线开头,并紧跟零个或多个由下画线、字母或数字组成的序列 。

        修饰符- 和修饰符*类似,也是用于匹配原始字符分类的零次或多次出现。不过,跟修饰符*总是匹配能匹配的最长序列不同,修饰符 - 只会匹配最短序列 。 虽然有时它们两者并没有什么区别,但大多数情况下这两者会导致截然不同的结果。例如,当试图用模式 '[_%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 ...

如下的代码用来检查字符串是否为一个没有多余前缀字符和后缀字符的整数 :

if string.find(s, "^[+-]?%d+$") then ...

^ 和 $ 字符只有位于模式的开头和结尾时才具有特殊含义;否则,它们仅仅就是与其自身相匹配的普通字符。

        模式 '%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]", "one")))
one anthem is one theme

模式 '%f[%w]' 匹配位于一个非字母或数字的字符和一个字母或数字的字符之间的前置 ,而模式 '%f[%W]' 则匹配一个字母或数字的字符和一个非字母或数字的字符之间的前置。因此,指定的模式只会匹配完整的字符串"the"。 请注意,即使字符集只有一个分类,也必须把它用括号括起来 。

        前置模式把目标字符串中第一个字符前和最后一个字符后的位置当成空字符( ASCII 编码的\0)。 在前例中,第一个"the" 在不属于集合'[%w]'的空字符和属于集合'[%w]'的 t 之间匹配了一个前置。

10.3  捕获

        捕获机制允许根据一个模式从目标字符串中抽出与该模式匹配的内容来用于后续用途,可以通过把模式中需要捕获的部分放到一对圆括号内来指定捕获。

        对于具有捕获的模式,函数 string.match 会将所有捕获到的值作为单独的结果返回;换句话说, 该函数会将字符串切分成多个被捕获的部分:

> pair = "name = Anna"
> key, value = string.match(pair, "(%a+)%s*=%s*(%a+)")
> print(key, value)
name    Anna

模式 '%a+' 表示一个非空的字母序列,模式 '%s*' 表示一个可能为空的空白序列。因此,上例中的这个模式表示一个字母序列、紧跟着空白序列、一个等号、空白序列以及另一个字母序列 。 模式中的两个字母序列被分别放在圆括号中,因此在匹配时就能捕获到它们。下面是一个类似的示例:

> date = "Today is 17/7/1990"
> d, m, y = string.match(date, "(%d+)/(%d+)/(%d+)")
> print(d, m, y)
17      7       1990

在这个示例中,使用了 3 个捕获,每个捕获对应一个数字序列 。

        在模式中,形如 '%n' 的分类(其中 n 是一个数字),表示匹配第 n 个捕获的副本。 举一个典型的例子,假设想在一个字符串中寻找一个由单引号或双引号括起来的子串 。那么可能会尝试使用模式 '[" '].-[" ']',它表示一个引号后面跟任意内容及另外一个引号;但是,这种模式在处理像 "it's all right " 这样的字符串时会有问题。要解决这个问题,可以捕获第一个引号然后用它来指明第二个引号:

> s = [[then he said: "it's all right"!]]
> q, quotedPart = string.match(s, "([\"'])(.-)%1")
> print(quotedPart)
it's all right
> print(q)
"

第 1 个捕获是引号本身,第 2 个捕获是引号中的内容(与 '.-' 匹配的子串)。

        下例是一个类似的示例,用于匹配 Lua 语言中的长字符串的模式:

%[(=*)%[(.-)%]%l%]

它所匹配的内容依次是 : 一个左方括号、零个或多个等号、另一个左方括号 、 任意内容(即字符串的 内容)、一个右方括号、相同数量的等号及另一个右方括号 :

> p = "%[(=*)%[(.-)%]%l%]"
> s = "a = [=[[[ something ]] ]==] ]=]; print(a)"
> print(string.match(s, p))
[[ something ]] ]==]

第 1 个捕获是等号序列(在本例中只有一个),第 2 个捕获是字符串内容。

        被捕获对象的第 3 个用途是在函数gsub的替代字符串中。 像模式一样,替代字符串同样可以包括像 "%n" 一样的字符分类,当发生替换时会被替换为相应的捕获。特别地,"%0"意味着整个匹配,并且替换字符串中的百分号必须被转义为"%%" 。下面这个示例会重复字符串中的每个字母,并且在每个被重复的字母之间插入一个减号:

> print((string.gsub("hello Lua", "%a", "%0-%0")))
h-he-el-ll-lo-o L-Lu-ua-a

下例交换了相邻的字符:

> print((string.gsub("hello Lua", "(.)(.)", "%2%1")))
ehll ouLa

        以下是一个更有用的示例,让我们编写一个原始的格式转换器,该格式转换器能读取LaTeX风格的命令,并将它们转换成 XML 风格:

\command{some text}		-->		<command>some text</command>

如果不允许嵌套的命令 ,那么以下调用函数 string.gsub 的代码即可完成这项工作 :

> s = [[the \quote{task} is to \em{change} that.]]
> s = string.gsub(s, "\\(%a+){(.-)}", "<%1>%2</%1>")
> print(s)
the <quote>task</quote> is to <em>change</em> that.

        另一个有用的示例是剔除字符串两端空格:

function trim (s)s = string.gsub(s, "^%s*(.-)%s*$", "%l")return s 
end

请注意模式中修饰符的合理运用。 两个定位标记(^ 和 $ )保证了我们可以获取到整个字符串 。 由于中间的 '.-' 只会匹配尽可能少的内容,所以两个 '%s*' 便可匹配到首尾两端的空格。

10.4  替换

        正如我们此前已经看到的,函数 string.gsub 的第 3 个参数不仅可以是字符串,还可以是一个函数或表。 当第 3 个参数是一个函数时,函数 string.gsub 会在每次找到匹配时调用该函数,参数是捕获到的内容而返回值则被作为替换字符串 。当第 3 个参数是一个表时,函数 string.gsub 会把第一个捕获到的内容作为键,然后将表中对应该键的值作为替换字符串。如果函数的返回值为 nil 或表中不包含这个键或表中键的对应值为 nil ,那么函数 gsub 不改变这个匹配。

        先举一个例子,下述函数用于变量展开,它会把字符串中所有出现的 $varname 替换为全局变量 varname 的值 :

> function expand (s)
>> return (string.gsub(s, "$(%w+)", _G))
>> end
>
> name = "Lua"; status = "great"
> print(expand("$name is $status, isn't it?"))
Lua is great, isn't it?

( _G 是预先定义的包括所有全局变量的表)对于每个与 '$(%w+)' 匹配的地方( $ 符号后紧跟一个名字),函数 gsub 都会在全局表 _G 中查找捕获到的名字,并用找到的结果替换字符串中相匹配的部分;如果表中没有对应的键,则不进行替换 :

> print(expand("$othername is $status, isn't it?"))
$othername is great, isn't it?

        如果不确定是否指定变量具有字符串值,那么可以对它们的值调用函数 tostring。在这种情况下,可以用一个函数来返回要替换的值 :

function expand (s)return (string.gsub(s, "$(%w+)", function (n)return tostring(_G[n])end))
end> print(expand("print = $print; a = $a"))
print = function: 0000000063bdb1d0; a = nil

        在函数 expand 中,对于所有匹配 '$(%w+)' 的地方,函数 gsub 都会调用给定的函数,传入捕获到的名字作为参数,并使用返回字符串替换匹配到的内容。

        最后一个例子,让我们再回到上一节中提到的格式转换器。我们仍然是想将 LaTeX 风格的命令( \example{text})转换成 XML 风格的( <example>text</example>),但这次允许嵌套的命令。 以下的函数用递归的方式完成了这个需求 :

function toxml (s)s = string.gsub(s, "\\(%a+)(%b{})", function (tag, body)body = string.sub(body, 2, -2)body = toxml(body)return string.format("<%s>%s</%s>", tag, body, tag)end)return s 
end> print(toxml("\\title{The \\bold{big} example}"))
<title>The <bold>big</bold> example</title>

10.4.1  URL 编码

        我们的下一个示例中将用到 URL 编码,也就是 HTTP 所使用的在 URL 中传递参数的编码方式。 这种编码方式会将特殊字符(例如 = 、&和+ )编码为"%xx"的形式,其中 xx 是对应字符的十六进制值。此外,URL 编码还会将空格转换为加号。 例如,字符串” a+b =c ”的URL 编码为 "a%2Bb+%3D+c" 。最后,URL 编码会将每对参数名及其值用等号连接起来,然后将每对 name=value 用&连接起来。 例如,值

name = "al"; query = "a+b = c"; q = "yes or no"

对应的 URL 编码为:

"name=al&query=a%2Bb+%3D+c&q=yes+or+no"

        现在,假设要将这个 URL 解码井将其中的键值对保存到一个表内,以相应的键作为索引,那么可以使用以下的函数完成基本的解码 :

function unescape (s)s = string.gsub(s, "+", " ")s = string.gsub(s, "%%(%x%x)", function(h)return string.char(tonumber(h, 16))end)return s 
endprint(unescape("a%2Bb+%3D+c"))

        第一个 gsub 函数将字符串中的所有加号替换为空格,第二个 gsub 函数则匹配所有以百分号开头的两位十六进制数,并对每处匹配调用一个匿名函数。 这个匿名函数会将十六进制数转换成一个数字(以 16 为进制,使用函数 tonumber )并返回其对应的字符(使用函数string. char )。

        可以使用函数 gmatch 来对键值对 name=value 进行解码。 由于键名和值都不能包含&或=, 所以可以使用模式 '[^&=]+' 来匹配它们 :

cgi = {}
function decode (s)for name, value in string.gmatch(s, "([^&=]+)=([^&=]+)") doname = unescape(name)value = unescape(value)cgi[name] = valueend
end

        调用函数 gmatch 会匹配所有格式为 name=value 的键值对。 对于每组键值对,迭代器会返回对应的捕获(在匹配的字符串中被括号括起来了),捕获到的内容也就是 name 和 value的值。 循环体内只是简单地对两个字符串调用函数 unescape ,然后将结果保存到表 cgi 中 。

        对应的编码函数也很容易编写。 先写一个 escape 函数,用它将所有的特殊字符编码为百分号紧跟对应的十六进制形式(函数 format 的参数"%02X"用于格式化输出一个两位的十六进制数,若不足两位则以 0 补齐),然后把空格替换成加号:

function escape (s)s = string.gsub(s, "[&=+%%%c]", function (c)return string.format("%%%02X", string.byte(c))end)s = string.gsub(s, " ", "+")return s 
end

encode 函数会遍历整个待编码的表,然后构造出最终的字符串:

function encode (t)local b = {}for k,v in pairs(t) dob[#b + 1] = (escape(k) .. "=" .. escape(v))end-- 将'b'中所有的元素连接在一起,使用"&"分隔return table.concat( b, "&")
endt = {name = "al", query = "a+b = c", q = "yes or no"}
print(encode(t))
> q=yes+or+no&query=a%2Bb+%3D+c&name=al

10.4.2  制表符展开

        在 Lua 语言中,像 '()' 这样的空白捕获具有特殊含义。 该模式并不代表捕获空内容(这样的话毫无意义),而是捕获模式在目标字符串中的位置(该位置是数值)

> print(string.match("hello", "()ll()"))
3       5
>

(请注意,由于第 2 个空捕获的位置是在匹配之后,所以这个示例的结果与调用函数 string.find 得到的结果并不一样。 )

        另一个关于位置捕获的良好示例是在字符串中进行制表符展开:

function expandTabs (s, tab)tab = tab or 8 		-- 制表符的“大小”(默认是8)local corr = 0 		-- 修正量s = string.gsub(s, "()t", function(p)local sp = tab - (p - 1 + corr)%tab corr = corr - 1 + sp return string.rep(" ", sp)end)return s 
end

        函数 gsub 会匹配字符串中所有的制表符并捕获它们的位置。对于每个制表符,匿名函数会根据其所在位置计算出需要多少个空格才能恰好凑够一列(整数个 tab ):该函数先将位置减去 1 以从 0 开始计数,然后加上 corr 凑整之前的制表符(每一个被展开的制表符都会影响后续制表符的位置)。之后,该函数更新下一个制表符的修正量:为正在被去掉的制表符减 1 ,再加上要增加的空格数 sp 。 最后,这个函数返回由替代制表符的合适数量的空格组成的字符串 。

        为了完整起见,让我们再看一下如何实现逆向操作,即将空格转换为制表符。 第一种方法是通过空捕获来对位置进行操作,但还有一种更简单的方法: 即在字符串中每隔 8 个字符插入一个标记,然后将前面有空格的标记替换为制表符。

function unexpandTabs (s, tab)tab = tab or 8s = expandTabs(s, tab)local pat = string.rep(".", tab)		-- 辅助模式s = string.gsub(s, pat, "%0\1")			-- 在每8个字符后添加一个标记\1s = string.gsub(s, " +1", "\t")			-- 将所有以此标记结尾的空格序列-- 都替换为制表符\ts = string.gsub(s, "1", "")				-- 将剩下的标记\1删除return s
end

这个函数首先对字符串进行了制表符展开以移除其中所有的制表符,然后构造出一个用于匹配所有 8 个字符序列的辅助模式,再利用这个模式在每 8 个字符后添加一个标记(控制字符\1)。 接着,它将所有以此标记结尾的空格序列都替换为制表符。 最后,将剩下的标记删除(即那些没有位于空格后的标记)。

10.5 诀窍

        模式匹配是进行字符串处理的强大工具之一。 虽然通过多次调用函数 string.gsub 就可以完成许多复杂的操作,但是还是应该谨慎地使用该函数。

        模式匹配替代不了传统的解析器。对于那些用后即弃的程序来说,我们确实可以在源代码中做一些有用的操作,但却很难构建出高质量的产品 。 例如,考虑一下之前曾经用来匹配C 语言程序中注释的模式 '/%*.-%*/' 。 如果 C 代码中有一个字符串常量含有 "/*" ,那么就会得到错误的结果 :

> test = [[char s[] = "a /* here"; /* a tricky string */]]
> print((string.gsub(test, "%*.-%*/", "<COMMENT>")))
char s[] = "a /<COMMENT>

由于含有注释标记的字符串十分少见,因此对于我们自用的程序而言,这个模式可能能够满足需求 ;但是,我们不应该将这个带有缺陷的程序发布出去。

        通常,在 Lua 程序中使用模式匹配时的效率是足够高的 : 笔者的新机器可以在不到 0.2秒的时间内计算出一个 4.4MB 大小(具有 85 万个单词)的文本中所有单词的数量。但仍然需要注意,应该永远使用尽可能精确的模式,不精确的模式会比精确的模式慢很多。 一个极端的例子是模式 '(.-)%$' ,它用于获取字符串中第一个 $ 符号前的所有内容。 如果目标字符串中有 $ 符号,那么这个模式工作很正常;但是,如果字符串中没有 $ 符号,那么模式匹配算法就会首先从字符串起始位置开始匹配,直至为了搜索 $ 符号而遍历完整个字符串 。 当到达字符串结尾时,这次从字符串起始位直开始的模式匹配就失败了。之后,模式匹配算法又从字符串的第二个位置开始第二次搜索,结果仍然是无法匹配这个模式。这个匹配过程会在字符串的每个位置上进行一次,从而导致O(n^{2})的时间复杂度。在笔者的新机器上,搜索20 万个字符需要耗费超过 4 分钟的时间 。 要解决这个问题,我们只需使用 '^(.-)%$' 将模式锚定在字符串的开始位置即可。这样,如果不能从起始位置开始找到匹配,搜索就会停止。有了^的锚定以后,该模式匹配就只需要不到 0.01 秒的时间了 。

        此外,还要留心空模式,也就是那些匹配空字符串的模式。 例如,如果试图使用模式'%a*'来匹配名字,那么就会发现到处都是名字:

> i, j = string.find(";$% **#$hello13", "%a*")
> print(i, j)
1       0

在这个示例中,函数 string.find 在字符串的开始位置正确地找到一个空的字母序列 。

        在模式的结束处使用修饰符- 是没有意义的,因为这样只会匹配到空字符串 。 该修饰符总是需要在其后跟上其他的东西来限制扩展的范围 。同样,含有 '.*' 的模式也非常容易出错,这主要是因为这种模式可能会匹配到超出我们预期范围的内容。

        有时,用 Lua 语言来构造一个模式也很有用。我们已经在将空格转换为制表符的程序中使用过这个技巧。接下来再看另外一个示例,考虑如何找出一个文本中较长的行(比如超过70 个字符的行)。较长的行就是一个具有 70 个或更多字符的序列,其中每个字符都不为换行符,因而可以用字符分类 '[^\n]' 来匹配除换行符以外的其他单个字符。 这样,就能够通过把这个匹配单个字符的模式重复 70 次来匹配较长的行。 除了手写以外,还可以使用函数 string.rep 来创建这个模式:

pattern = string.rep("[^\n]", 70) .. "+"

        再举一个例子,假设要进行大小写无关的查找。一种方法就是将模式中的所有字母x用 '[xX]' 替换,即同时包含原字母大小写形式的字符分类。我们可以使用如下函数来自动地完成这种转换:

function nocase (s)s = string.gsub(s, "%a", function(c)return "[" .. string.lower(c) .. string.upper(c) .. "]"end)return s
endprint(nocase("Hi there!"))
> [hH][iI] [tT][hH][eE][rR][eE]!

        有时,我们可能需要将所有出现的 s1 替换为 s2 ,而不管其中是否包含魔法字符。 如果字符串 s1 和 s2 是常量 ,那么可以在编写字符串时对魔法字符进行合理的转义;但如果字符串是一个变量,那么就需要用另一个 gsub 函数来进行转义:

s1 = string.gsub(s1, "(%W)", "%%%l")
s2 = string.gsub(s1, "%%", "%%%%")

在进行字符串搜索时,我们对所有字母和数字外的字符进行了转义(即大写的 W ) 。 而在替换字符串中,我们只对百分号进行了转义。

        模式匹配的另一个有用的技巧就是,在进行实际工作前先对目标字符串进行预处理。 假设想把一个字符串中所有被双引号 (") 引起来的内容改为大写,但又允许内容中包含转义的引号("\""):

follows a typical string: "This is \"great\"!".

处理这种情况的方法之一就是先对文本进行预处理,将所有可能导致歧义的内容编码成别的内容。 例如,可以将"\""编码为"\1" 。 不过,如果原文中本身就含有 "\1",那么就会遇到问题。 另一种可以避免这个问题的简单做法是将所有 "\x" 编码为 "\ddd",其中ddd为字符x的十六进制表示形式:

function code (s)return (string.gsub(s, "\\(.)", function(x)return string.format("%03d", string.byte(x))end))
end

这样,由于原字符串中所有的 "\ddd" 都进行了编码,所以编码后字符串中的 "\ddd" 序列一定都是编码造成的 。 这样,解码也就很简单了 :

function decode (s)return (string.gsub(s, "\\(%d%d%d)", function (d)return "\\" .. string.char(tonumber(d))end))
end

        现在我们就可以完成把一个字符串中被双引号(")引起来的内容改为大写的需求。由于编码后的字符串中不包含任何转义的引号("\""),所以就可以直接使用 '".-"' 来查找位于一对引号中的内容:

> s = [[follows a typical string: "This is \"great\"!".]]
> s = code(s)
> s = string.gsub(s, '".-"', string.upper)
> s = decode(s)
> print(s)
follows a typical string: "THIS IS 034GREAT034!".

或者写成:

print(decode(string.gsub(code(s), '".-"', string.upper)))

        是否能够将模式匹配函数用于 UTF-8 字符串取决于模式本身。由于 UTF-8 的主要特性之一就是任意字符的编码不会出现在别的字符的编码中,因此文本类的模式一般可以正常工作。字符分类和字符集只对 ASCII 字符有效。例如,可以对 UTF- 8 字符串使用模式'%s',但它只能匹配 ASCII 空格,而不能匹配诸如 HTML 空格或蒙古文元音分隔符等其他的 Unicode 空格。

        恰当的模式能够为处理 Unicode 带来额外的能力。一个优秀的例子是预定义模式 utf8.charpattern ,该模式只精确地匹配一个UTF- 8 字符。 utf 8 标准库中就是按照下面的方法定
义这个模式的 :

utf8.charpattern = [\0-\x7F\xC2-\xF4][\x80-\xBF]*

该模式的第 1 部分匹配 ASCII 字符(范围[0,0x7F])或多字节序列的起始字节(范围[0xC2,0xF4]),第 2 部分则匹配零个或多个后续的字节(范围 [0x80,0xBF])。

相关文章:

  • 【嵌入式八股22】排序算法与哈希算法
  • 辞九门回忆
  • windows安装docker,发现没有hyper
  • WSL2里手动安装Docker 遇坑
  • 14【模块学习】74HC595:使用学习
  • SpringMVC 前后端数据交互 中文乱码
  • 微服务基础-Ribbon
  • 同样开源的自动化工作流工具n8n和Dify对比
  • 从零搭建云原生后端系统 —— 一次真实项目实践分享
  • 迷你世界UGC3.0脚本Wiki触发器脚本交互
  • 云原生--核心组件-容器篇-4-认识Dockerfile文件(镜像创建的基础文件和指令介绍)
  • 企业数据赋能 | 应用模板分享:汽车销售仪表板
  • 《一键式江湖:Docker Compose中间件部署108式》开篇:告别“配置地狱”,从此笑傲云原生武林!》
  • 以科技之力,启智慧出行 —— 阅读《NVIDIA 自动驾驶安全报告》及观看实验室视频有感
  • 【计算机视觉】CV实战项目- Four-Flower:基于TensorFlow的花朵分类实战指南
  • 大数据学习栈记——Hive4.0.1安装
  • 运算符分为哪几类?哪些运算符常用作判断?简述运算符的优先级
  • MCP+A2A协议终极指南:AI系统构建技术全解析(医疗/金融实战+Streamable HTTP代码详解)
  • Vue Router 核心指南:构建高效单页应用的导航艺术
  • Spring MVC 拦截器教程
  • 商务部:4月份以来的出口总体延续平稳增长态势
  • 加拿大今日大选:房价、印度移民和特朗普,年轻人在焦虑什么?
  • 张译、惠英红分获第二十届中国电影华表奖优秀男、女演员奖
  • 闲暇时间的“堕落”
  • 首映|马丽:真想抱抱臧姑娘,对她说辛苦了
  • “70后”通化市委书记孙简已任吉林省政府领导