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 万个单词)的文本中所有单词的数量。但仍然需要注意,应该永远使用尽可能精确的模式,不精确的模式会比精确的模式慢很多。 一个极端的例子是模式 '(.-)%$' ,它用于获取字符串中第一个 $ 符号前的所有内容。 如果目标字符串中有 $ 符号,那么这个模式工作很正常;但是,如果字符串中没有 $ 符号,那么模式匹配算法就会首先从字符串起始位置开始匹配,直至为了搜索 $ 符号而遍历完整个字符串 。 当到达字符串结尾时,这次从字符串起始位直开始的模式匹配就失败了。之后,模式匹配算法又从字符串的第二个位置开始第二次搜索,结果仍然是无法匹配这个模式。这个匹配过程会在字符串的每个位置上进行一次,从而导致的时间复杂度。在笔者的新机器上,搜索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])。
相关文章:
Lua 第10部分 模式匹配
10.1 模式匹配的相关函数 字符串标准库提供了基于模式的 4 个函数。 我们已经初步了解过函数 find 和 gsub,其余两个函数分别是 match 和 gmatch (Global Match 的缩写)。 函数 string.find 用于在指定的目标字符串中搜索指定的模式。最简单的模式就是一…...
Maven 4.0.0 模式-pom.xml配置详解
Maven 4.0.0 模式-pom.xml配置详解 此 pom.xml 文件涵盖了 Maven 4.0.0 模式支持的所有主要标签,包括项目元数据、依赖管理、构建配置、发布管理等。每个标签都配有详细注释,说明其作用、常见用法和可能的值。 此文件旨在展示标签的完整性&#…...
IDEA 连接 Oracle 数据库
IDEA 连接 Oracle 数据库...
机器人快速启动
机器人快速启动 ES机器人开机操作流程 方法一(一体化底座启动) 接通48V电源点击底座“Power”按钮观察电源指示灯亮起,蜂鸣器发出“嘀”声,代表底座启动完成 方法二(控制手柄启动) 长按手柄开关机键2秒后松…...
使用 MediaPipe 和 OpenCV 快速生成人脸掩膜(Face Mask)
在实际项目中,尤其是涉及人脸识别、换脸、图像修复等任务时,我们经常需要生成人脸区域的掩膜(mask)。这篇文章分享一个简单易用的小工具,利用 MediaPipe 和 OpenCV,快速提取人脸轮廓并生成二值掩膜图像。 …...
《全球反空间能力》报告翻译——部分1
全球反空间能力 已进行过破坏性反卫星测试的国家 美国 美国目前拥有世界上最先进的军事太空能力,尽管与中国的相对差距正在缩小。在冷战期间,美国开创了许多现今使用的国家安全太空应用,并在几乎所有类别中保持技术领先地位。美国军方在将…...
云原生课程-Docker
一次镜像,到处运行。 1. Docker详解: 1.1 Docker简介: Docker是一个开源的容器化平台,可以帮助开发者将应用程序和其依赖的环境打包成一个可移植的,可部署的容器。 docker daemon:是一个运行在宿主机(DO…...
组件的基本知识
组件 组件的基本知识 组件概念组成步骤好处全局注册生命周期scoped原理 父子通信步骤子传父 概念 就是将要复用的标签,抽离放在一个独立的vue文件中,以供主vue文件使用 组成 三部分构成 template:HTML 结构 script: JS 逻辑 style: CSS 样…...
空间矩阵的思考
今天又看了些线性代数,引发了许多思考。 矩阵是以长和宽存储数据,那有没有一种新型的矩阵,以长宽高的形式存储数据呢?我不知道有没有,所以暂且称其为空间矩阵。 它肯定是存在的,可以这样抽象&#…...
【数据挖掘】时间序列预测-常用序列预测模型
常用序列预测模型 (1)AR(自回归)模型(2)ARIMA模型(3)Prophet模型(4)LSTM模型(5)Transformer模型(6)模型评估6.…...
将你的本地项目发布到 GitHub (新手指南)
目录 第 1 步:在 GitHub 上创建新的仓库 (Repository)第 2 步:将本地仓库连接到 GitHub 远程仓库第 3 步:(可能需要) 重命名你的默认分支第 4 步:将本地代码推送到 GitHub第 5 步:在 GitHub 上检查结果后续工作流程 你…...
[论文梳理] 足式机器人规划控制流程 - 接触碰撞的控制 - 模型误差 - 自动驾驶车的安全合规(4个课堂讨论问题)
目录 问题 1:足式机器人运动规划 & 控制的典型流程 (pipline) 1.1 问题 1.2 目标 1.3 典型流程(Pipeline) 1.3.1 环境感知(Perception) 1.3.2 高层规划(High-Level Planning) 1.3.3 …...
初中级前端面试全攻略:自我介绍模板、项目讲解套路与常见问答
为了给面试官留下专业而亲切的第一印象,自我介绍要突出与岗位相关的技能和项目经验,同时以自己擅长的领域开放式结尾。通常可以按照以下思路组织自我介绍内容:首先简单介绍个人信息和工作年限,然后列出精通的前端技术栈…...
Android开发中svg转xml工具使用
要使用 svg2vector-cli 工具通过命令行将 SVG 文件转换为 Android 可用的 XML 矢量图标文件,可以单个文件转换或者整个文件夹批量转换,以下是详细的步骤和说明: 1. 准备工作 1.1 下载工具 首先需要下载 svg2vector-cli-1.0.0.jar 或更高版本…...
爬虫技术入门:基本原理、数据抓取与动态页面处理
引言 在当今数据驱动的时代,网络爬虫技术已成为获取和分析互联网数据的重要手段。无论是搜索引擎的网页收录、竞品数据分析,还是学术研究的语料收集,爬虫技术都发挥着关键作用。本文将深入浅出地讲解爬虫的基本原理,分析它能获取…...
AI预测3D新模型百十个定位预测+胆码预测+去和尾2025年4月27日第65弹
从今天开始,咱们还是暂时基于旧的模型进行预测,好了,废话不多说,按照老办法,重点8-9码定位,配合三胆下1或下2,杀1-2个和尾,再杀6-8个和值,可以做到100-300注左右。 (1)定…...
服务器数据备份,服务器怎么备份数据呢?
企业数据量呈指数级增长,服务器数据备份已成为保障业务连续性、抵御勒索攻击与合规审查的核心技术环节。当前,服务器数据备份方案需兼顾数据完整性、恢复时效性、存储经济性三大核心诉求,其实现路径可根据技术架构、数据规模及容灾等级划分为…...
语音识别质量的跟踪
背景 这个项目是用来生成结构化的电子病历的。数据的来源是医生的录音。中间有一大堆的处理,语音识别,关键字匹配,结构化处理,病历编辑......。最多的时候给上百家医院服务。 语音识别质量的跟踪 一、0225医院的训练后的情况分…...
【数据挖掘】时间序列预测-时间序列的平稳性
时间序列的平稳性 (1)平稳性定义(2)平稳性处理方法2.1 差分法2.2 季节调整(Seasonal Adjustment)2.3 趋势移除(Detrending)2.4 对数转换(Logarithmic Transformation&…...
成都蒲江石象湖旅游攻略之石象湖郁金香最佳观赏时间
石象湖坐落于成都蒲江,拥有绝美的郁金香花海,吸引了很多的游客。如果大家想要观赏比较诱惑人的郁金香,那自然就应该知道正确的观赏时间。 心想郁金香合适的时间是每年的3月份到3月底。石象湖会还会举办盛大的郁金香节,在花园内有数…...
大模型、知识图谱和强化学习三者的结合,可以形成哪些研究方向?
大模型(Large Language Models, LLMs)、知识图谱(Knowledge Graph, KG)与强化学习(Reinforcement Learning, RL)作为人工智能领域的三大核心技术,其融合正推动着认知智能迈向新高度。本文结合2023-2025年的最新研究成果,系统梳理三者结合的七大科研方向及其技术路径。 …...
Linux文件操作
在C语言中,我们已经学习了文件相关的知识,那么在Linux中我们为什么还要再来学习文件呢?这是因为C语言中和Linux中,"文件"是2个不同的概念。所以我们要来学习Linux中对文件的操作。 在学习之前,我们先来回顾一…...
PostSwigger Web 安全学习:CSRF漏洞3
CSRF 漏洞学习网站:What is CSRF (Cross-site request forgery)? Tutorial & Examples | Web Security Academy CSRF Token 基本原理 CSRF Token 是服务端生成的唯一、随机且不可预测的字符串,用于验证客户端合法校验。 作用:防止攻击…...
【Node.js 】在Windows 下搭建适配 DPlayer 的轻量(简陋)级弹幕后端服务
一、引言 DPlayer官网:DPlayer 官方弹幕后端服务:DPlayer-node MoePlayer/DPlayer-node:使用 Docker for DPlayer Node.js 后端(https://github.com/DIYgod/DPlayer) 本来想直接使用官网提供的DPlayer-node直接搭建…...
淘宝tb.cn短链接生成
淘宝短链接简介 1. 一键在线生成淘宝短链接tb.cn,m.tb.cn等 2. 支持淘宝优惠券短链接等淘宝系的所有网址 3. 生成的淘宝短链接是官方的,安全稳定有保证 4.适合多种场景下使用,如:网站推广,短信推广 量大提供api接口࿰…...
在web应用后端接入内容审核——以腾讯云音频审核为例(Go语言示例)
腾讯云对象存储数据万象(Cloud Infinite,CI)为用户提供图片、视频、语音、文本等文件的内容安全智能审核服务,帮助用户有效识别涉黄、违法违规和广告审核,规避运营风险。本文以音频审核为例给出go语言示例代码与相应结…...
优化无头浏览器流量:使用Puppeteer进行高效数据抓取的成本降低策略
概述 使用 Puppeteer 进行数据抓取时,流量消耗是一个重要考虑因素。特别是在使用代理服务时,流量成本可能显著增加。为了优化流量使用,我们可以采用以下策略: 资源拦截:通过拦截不必要的资源请求来减少流量消耗。请求…...
【C语言】fprintf与perror对比,两种报错提示的方法
它们的主要区别在于 信息来源 和 自动包含的系统错误详情。 1. fprintf(stderr, "自定义错误信息\n"); 功能: 这是标准库中的一个通用格式化输出函数。你可以用它向任何文件流(包括 stdout 标准输出, stderr 标准错误, 或任何用 fopen 打开的文件&#x…...
C语言复习笔记--内存函数
在复习完字符函数和字符串函数之后,今天让我们复习一下内存函数吧.这一块的东西不太多,并且与之前的字符串函数有一些地方很相似,所以这里应该会比较轻松. memcpy使用和模拟实现 老规矩,先看函数原型 void * memcpy ( void * destination, const void * source, size_t num );…...
前端面试高频算法
前端面试高频算法 1 排序算法;1.1 如何分析一个排序算法1.1.1 执行效率3.1.2 内存消耗1.1.3 稳定性 1.2 冒泡排序(Bubble Sort)1.3 插入排序(Insertion Sort)1.4 选择排序(Selection Sort)1.5 归…...
云原生--核心组件-容器篇-4-认识Dockerfile文件(镜像创建的基础文件和指令介绍)
1、Dockerfile的定义与作用 定义: Dockerfile是一个文本文件,包含一系列Docker指令,用于自动化构建Docker镜像。Docker 在构建镜像时会按照Dockerfile中的指令逐步执行,每一行指令都会生成一个新的镜像层(layer&#x…...
13.组合模式:思考与解读
原文地址:组合模式:思考与解读 更多内容请关注:7.深入思考与解读设计模式 引言 在软件开发中,是否曾经遇到过这样一种情况:你有一个对象,它本身很简单,但是它包含了其他类似的对象。随着系统变得越来越复…...
Pycharm(十七)生成器
一、生成器介绍 1.1 概述 生成器指的是Generator对象,它不再像以往一样,一次性生成所有的数据,而是用一个,再生成一个,基于用户写的规则(条件)来生成数据,如果条件不成立ÿ…...
盛元广通实验材料管理系统-实验室管理系统-LIMS
一、引言 在当下科学研究及各类实验日益频繁的背景下,实验材料管理成为实验室高效运作的核心环节。从“人工低效”到“智能自动化”,盛元广通可覆盖实验材料的采购、存储、使用、追踪等全流程,从功能适配性、技术性能、成本效益、供应商服务…...
检查 NetCDF Fortran的版本
执行 nf-config --all命令后,它会输出一堆信息,大致像这样: This netCDF-Fortran version: 4.6.0 netCDF-Fortran installation dir: /usr/local/netcdf4 Fortran compiler: gfortran Fortran compiler flags: -g -O2 Fortran preprocesso…...
MySQL 存储引擎与服务体系深度解析
一、存储引擎核心概念 基本定义 存储引擎:MySQL服务的核心组件,负责数据的存储、检索和管理版本演进: MySQL 5.0/5.1 默认使用MyISAM引擎MySQL 5.5/5.6+ 默认采用InnoDB引擎关键特性 不同存储引擎采用不同的数据存储结构和处理机制直接影响表的CRUD操作性能和数据安全特性作…...
乐企数电发票分布式发票号码生成重复的问题修复思路分享
文章目录 1.前言2.解决思路2.1错误姿势2.2歪打正着2.3正确姿势 3.总结 1.前言 由于之前接了乐企数电开票,服务上线之后,使用的公司少没有啥问题,后面切换了两家日开票量大的公司上线之后,就发现发票号码生成重复了,后面…...
多级缓存架构设计与实践经验
多级缓存架构设计与实践经验 在互联网大厂Java求职者的面试中,经常会被问到关于多级缓存的架构设计和实践经验。本文通过一个故事场景来展示这些问题的实际解决方案。 第一轮提问 面试官:马架构,欢迎来到我们公司的面试现场。请问您对多级…...
LCD1602液晶显示屏详解(STM32)
目录 一、介绍 二、传感器原理 1.原理图编辑 2.接口说明 三、程序设计 main文件 lcd1602.h文件 lcd1602.c文件 四、实验效果 五、资料获取 项目分享 一、介绍 LCD1602A字符型液晶显示模块是专门用于显示字母、数字元、符号等的点阵型液晶显示模块。分4位和8位数据…...
Golang | 集合求交
文章目录 bitmap求交集2个有序链表多个有序链表跳表 bitmap求交集 2个有序链表 多个有序链表 为什么非最大的所有都要往后移动呢?因为现在已经知道交集即使有,也最小都是这个目前最大的了,其他不是最大的不可能是交集,所有除了最大…...
手机充电进入“秒充“时代:泡面刚下锅,电量已满格
现代人的生活节奏越来越快,手机充电技术也在飞速发展。从最初的"充电一整晚"到如今的"秒充"时代,充电效率的提升正在悄然改变着我们的生活习惯。最新数据显示,目前最快的手机充电技术仅需4分30秒就能充满一部手机的电量&…...
网站字体文件过大 导致字体从默认变成指定字体的时间过长
1.选择字体中只用到的字符集较小的包 只用到了数字,所以使用了 xx-sans.ttf的版本(86kb) 2.转换ttf格式为woff2 转换后26kb 3.使用字体 // 定义字体 font-face {font-family: "myFont";src: url(/assets/fonts/myFont.woff2) format(woff2);font-weigh…...
WPF常用技巧汇总 - Part 2
WPF常用技巧汇总-CSDN博客 主要用于记录工作中发现的一些问题和常见的解决方法。 目录 WPF常用技巧汇总-CSDN博客 1. DataGrid Tooltip - Multiple 2. DataGrid Tooltip - Cell值和ToolTip值一样 3. DataGrid Tooltip - Cell值和ToolTip值不一样 4. DataGrid - Ctrl A /…...
C++中析构函数
析构函数 析构函数(Destructor)是类的一种特殊成员函数,用于在对象的生命周期结束时执行清理操作,他的主要作用是释放对象占用资源,例如动态分配的内存,文件句柄或网络连接等。 特点 名称与类名称相同 单…...
树莓派超全系列教程文档--(44)如何在树莓派上编译树莓派内核
如何在树莓派上编译树莓派内核 构建内核下载内核源代码 本地构建内核构建配置使用 LOCALVERSION 自定义内核版本构建安装内核 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 构建内核 操作系统预装的默认编译器和链接器被配置为构建在该操作系统…...
flask返回文件的同时返回其他参数
参考:flask实现上传文件与下载文件_flask 文件上传和下载-CSDN博客 在 Flask 中,返回文件的同时附加额外参数(如处理时间)可以通过 自定义 HTTP 响应头 或 返回 JSON 数据与文件结合 的方式实现。以下是具体方法和示例: 方法 1:通过 HTTP 响应头 附加参数(推荐) 将参…...
C++23 std::move_only_function:一种仅可移动的可调用包装器 (P0288R9)
文章目录 一、定义与基本概念1.1 定义1.2 基本概念 二、特点2.1 仅可移动性2.2 支持多种限定符2.3 无target_type和target访问器2.4 强前置条件 三、使用场景3.1 处理不可复制的可调用对象3.2 性能优化3.3 资源管理 四、与其他可调用包装器的对比4.1 与std::function的对比4.2 …...
Zookeeper实现分布式锁实战应用
Zookeeper实现分布式锁实战应用示例 1. 分布式锁概述 在分布式系统中,当多个进程或服务需要互斥地访问共享资源时,就需要分布式锁来协调。Zookeeper因其强一致性和临时节点特性,非常适合实现分布式锁。 2. Zookeeper实现分布式锁的核心原理…...
使用 Playwright 构建高效爬虫:原理、实战与最佳实践
随着网站前端技术日益复杂,传统的基于请求解析(如 requests、BeautifulSoup)的爬虫在处理 JavaScript 渲染的网站时变得力不从心。Playwright,作为微软推出的一款强大的自动化浏览器控制框架,不仅适用于自动化测试,也成为了处理现代网站爬取任务的利器。 本篇文章将带你…...
ComfyUI for Windwos与 Stable Diffusion WebUI 模型共享修复
#工作记录 虽然在安装ComfyUI for Windwos时已经配置过extra_model_paths.yaml 文件,但升级ComfyUI for Windwos到最新版本后发现原先的模型配置失效了,排查后发现,原来是 extra_model_paths.yaml 文件在新版本中被移动到了C盘目录下&#x…...