一、Lua 中的模式匹配
与其他脚本不同,Lua 语言既没有使用 POSIX 正则表达式,也没有使用 Perl 正则表达式进行模式匹配。
主要是因为大小问题,因为一个典型的 POSIX 正则表达式实现需要 4000 多行,比 Lua 语言标准库总大小的一半还大,所以 Lua 语言模式匹配自行实现,代码只有 600 行。
二、模式匹配相关函数
Lua 的字符串标准库提供了基于模式的 4 个函数。分别是 string.find
、string.gsub
、string.match
、string.gmatch
值得一提,这里的下标都是从 1 开始,和 java、kotlin 有区别。
2-1、string.find(s, pattern, init, plain)
在指定的目标 s (字符串)中搜索指定的模式 pattern
参数:
- s:需要匹配的目标字符串
- pattern:匹配的模式,在简单的模式中,可以是需要匹配的一个字符串
- init:可选,是一个索引,用于说明从目标字符串的哪个位置开始索引
- plain:可选,是一个布尔值,用于说明是否进行简单搜索。所谓简单搜索就是忽略模式而在目标字符串中进行单纯的"查找子字符串"。在匹配一些特殊字符时,可以使用。
返回值:
如果匹配成功,则返回匹配到的模式开始位置的索引 和结束位置的索引。
举几个例子
普通使用
一般通过 find
匹配到后,会返回开始和结束的下标,然后可以通过 sub
进行截取
lua
s1 = "hello world"
local i, j = string.find(s1, "hello")
print(i, j) --> 1 5
print(string.sub(s1, i, j)) --> hello
print(string.sub(s1, s1:find("world"))) --> world
指定位置搜索
指定起始位置进行搜索,第三行代码中,因为从第二个开始搜索(即 "ello world") "hello" ,所以搜索不到,则返回 nil
lua
s1 = "hello world"
print(string.find(s1, "hello", 1)) --> 1 5
print(string.find(s1, "hello", 2)) --> nil
简单搜索
如果需要进行一些特殊字符的匹配,例如 [
、]
这些,则可以简单匹配。
如果取消简单模式,[world]
就是正则表达式了,意思为匹配这五个字母中的任意一个即可,a [world]
中最先出现的是 w
,即第四个字母。
lua
s2 = "a [world]"
print(string.find(s2, "[world]", 1, true)) --> 3 9
print(string.find(s2, "[world]", 1, false)) --> 4 4
正则匹配
如果没有设置为简单搜索,则会进行正则匹配
lua
date = "Today is 12/04/2022"
print(string.sub(date,string.find(date,"%d+/%d+/%d+"))) --> 12/04/2022
local s3 = ";$% **#$hello13"
-- %a+ 可以匹配 1-n 字母,会匹配到开头的空字符串
print(string.sub(s3, string.find(s3, "%a+", 3))) --> hello
值得注意,必须要传入第三个参数,才能传第四个参数
2-2、string.match(s, pattern, init)
与 string.find
一样,都用于在一个字符串中搜索模式。
区别在于两者的返回值,string.match
返回的是目标字符串中与模式相匹配的那部分子串,而非模式所在的位置。
参数:
- s: 需要匹配的目标字符串
- pattern: 匹配的模式
- init: 可选,说明从目标字符串的哪个位置开始搜索,默认值为 1
返回值:
如果匹配成功,则返回匹配成功的那部分子串
举个例子
lua
s1 = "hello world"
date = "Today is 12/04/2022"
print(string.match(s1, "hello")) --> hello
print(string.match(date, "%d+/%d+/%d+")) --> 12/04/2022
2-3、string.gsub(s, pattern, repl, n)
是将字符串 s 中所有出现(如果设置了 n ,则表示前 n 次)能被匹配模式匹配的地方替换为字符串 repl。
参数:
- s: 需要匹配的目标字符串
- pattern: 匹配模式
- repl: 替换字符串或可以查询需要替换的表
- n: 可选,替换次数
返回值:
会放回两个值,一个是替换后的字符串,一个是替换次数
举个例子:
lua
print(string.gsub("Lua is cute", "cute", "great")) --> Lua is great 1
print(string.gsub("jiang peng yong", "n", "0")) --> jia0g pe0g yo0g 3
如果设置了第四个参数,则表示只替换前 n 次
lua
-- 只替换一次
print(string.gsub("jiang peng yong", "n", "0", 1)) --> jia0g peng yong 1
-- 只替换两次
print(string.gsub("jiang peng yong", "n", "0", 2)) --> jia0g pe0g yong 2
repl 可以使用 table
repl 参数可以使用 table ,会将匹配到的内容作为键,在 table 中查找对应的值,进行替换,如果查询不到则为 nil ,则不替换。
lua
local content = "My name is $name. I'm $age old."
local info = {
name = "jiang peng yong",
age = 29
}
-- 会将匹配到的内容作为键,在 info 中查找对应的值,进行替换,如果查询不到则为 nil ,则不替换
print(string.gsub(content, "$(%a+)", info)) --> My name is jiang peng yong. I'm 29 old. 2
repl 可以使用 函数
会将匹配到的内容作为参数,传入函数,使用返回值进行替换,如果为 nil ,则不替换。
如果捕获多个,则多值入参。
lua
local content = "My name is $name. I'm $age old."
-- 会将匹配到的内容作为参数,传入函数,会将返回值进行替换,如果为 nil ,则不替换
print(string.gsub(content, "$(%a+)", function(key)
print("sub key is ", key)
if key == "name" then
return "江澎涌"
end
return "nil"
end))
--> sub key is name
--> sub key is age
--> My name is 江澎涌. I'm nil old. 2
-- 如果多个捕获,则可以用多个参数进行接收
print(string.gsub(content, "($)(%a+)", function(key1, key2)
print("sub key is ", key1, key2)
if key1 == "name" then
return "江澎涌"
end
return "nil"
end))
--> sub key is $ name
--> sub key is $ age
--> My name is nil. I'm nil old. 2
2-4、string.gmatch(s, pattern)
参数:
- s: 需要匹配的目标字符串
- pattern: 匹配模式
返回值:
返回值是一个函数,通过返回值函数可以遍历一个字符串中所有出现的指定模式。
举个例子:
匹配 s 中所有的单词
lua
s = "some thing"
words = {}
for w in string.gmatch(s, "%a+") do
words[#words + 1] = w
end
for i, v in ipairs(words) do
print(i, "-->", v)
end
--> 1 --> some
--> 2 --> thing
三、匹配模式
Lua 语言中的模式使用百分号作为转义符。
所有被转义的字母都具有某些特殊含义(例如 '%a' 匹配所有字母),而所有被转义的非字母则代表其本身(例如 '%.' 匹配一个点)
字符 | 作用 |
---|---|
. | 任意字符 |
%a | 字母 |
%c | 控制字符 |
%d | 数字 |
%g | 除空格外的可打印字符 |
%l | 小写字母 |
%p | 标点符号 |
%s | 空白字符 |
%u | 大写字母 |
%w | 字母和数字 |
%x | 十六进制数字 |
%b | 匹配成对的字符串(具体见例子) |
%f[char-set] | 前置模式(具体见例子) |
lua
-- 把所有的字母都转为 .
print(string.gsub("one, and two; and three", "%a", ".")) --> ..., ... ...; ... ..... 17
-- %bxy 会进行成对匹配字符串,x 和 y 可以是任意字符,x 作为起始字符而 y 作为结束字符
print(string.gsub("a (enclosed (in) parentheses) line", " %b() ", " Jiang ")) --> a Jiang line 1
s = "the anthem is the theme"
-- %f[char-set] 模式,只有后一个字符位于 char-set 内而前一个字符不在时,符合匹配
-- %f[%w]the%f[%W] 这样只能完整匹配 "the"
-- %f[%w] 表示前一字符不是字母和数字,后一个字符是字母或数字
-- %f[%W] 表示前一字符是字母和数字,后一个字符不是字母或数字
-- 所以使用这一匹配 "anthem" 和 "theme" 的 "the" 不算被匹配
print(string.gsub(s, "%f[%w]the%f[%W]", "Jiang")) --> Jiang anthem is Jiang theme 2
print(string.gsub(s, "the", "Jiang")) --> Jiang anJiangm is Jiang Jiangme 4
3-1、字符的大写形式表示类的补集
例如:'%A' 代表任意非字母的字符
lua
-- 把所有不是字母的字符都换为 .
print(string.gsub("one, and two; and three", "%A", ".")) --> one..and.two..and.three 6
3-2、魔法字符
Lua 中的魔法字符包括以下,具有特殊含义,所以在使用时需要特别注意,当需要原本含意时,需要加上转义字符 %
:
lua
( ) . % + - * ? [ ] ^ $
举个例子:
lua
-- ^ 表示匹配目标字符串开始
print(string.gsub("123abc%d123", "^%d", ".")) --> .23abc%d123 1
-- $ 表示匹配目标字符串结束
print(string.gsub("123abc%d123", "%d$", ".")) --> 123abc%d12. 1
-- 什么都使用的话,就是直接匹配所有可匹配的数字
print(string.gsub("123abc%d123", "%d", ".")) --> ...abc%d... 6
-- 使用 %% 则将 %
print(string.gsub("123abc%d123", "%%d", ".")) --> 123abc.123 1
可以使用
%
对魔法字符进行转义,例如%%
匹配%
,%?
匹配?
。 其实和 java、kotlin 的转义一样,只是 java、kotlin 用的是\
魔法字符的含义其实和 java、kotlin 的使用相差不大。
可以用方括号 []
进行创建一个匹配的集合。
lua
-- 替换所有字母和数字
print(string.gsub(text, "[%a%d]","$")) --> $$$$$$%$$$$ 10
可以在集合的第一个字符用 ^
,表示这一字符集的补集。
举个例子:
lua
-- 替换所有非数字
print(string.gsub(text, "[^%d]","$")) --> 123$$$$$123 5
-- 就和之前的用大写字母表示补集是一样的
print(string.gsub(text, "%D","$")) --> 123$$$$$123 5
3-3、修饰符(限定符)
用来描述匹配模式中重复次数
修饰符 | 描述 |
---|---|
+ | 重复一次或多次(匹配最长序列) |
* | 重复零次或多次(最长匹配) |
- | 重复零次或多次(最小匹配) |
? | 可选(出现零次或一次) |
+
和 *
的区别
lua
-- %a+ 会尽可能的匹配最长序列,例如会匹配整个 Jiang ,而不只是 Ji 或 Jian
print(string.gsub("Jiang peng yong", "%a+", "J")) --> J J J 3
-- * 和 + 都会匹配最长序列,区别在于 * 可以匹配零个,所以当不确定中间是否有存在字符时,可用 *
print(string.gsub("Jiang++peng+ +yong", "+%s*+", " ")) --> Jiang peng yong 2
*
和 -
的区别
例如我们要进行去除注释 /* */
lua
local content = "/* 名字 */ int name; /* 年龄 */ int age;"
print(string.gsub(content, "/%*.*%*/", "")) --> int age; 1
print(string.gsub(content, "/%*.-%*/", "")) --> int name; int age; 2
因为 *
是特殊字符,所以如果需要使用原本含义则需要转义,即 %*
,.
会匹配任意字符
.*
会匹配最长的序列,则会导致匹配最后的*/
.-
会匹配最小的序列,则会匹配中间的*/
?
作用
lua
-- ? 进行匹配零或一次,可以匹配数字前带或不带 [+-]
print(string.gsub("-123; +456; 7890;", "[+-]?%d+;", "number")) --> number number number 3
四、捕获
捕获机制允许根据一个模式从目标字符串中抽出与该模式匹配的内容,通过把模式中需要捕获的部分放到一对圆括号哪来指定捕获
举个例子
lua
pair = "name = jiang"
key, value = string.match(pair, "(%a+)%s*=%s*(%a+)")
print("捕获", key, value) --> 捕获 name jiang
如果不用括号
lua
pair = "name = jiang"
key, value = string.match(pair, "%a+%s*=%s*%a+")
print("常规", key, value) --> 常规 name = jiang nil
4-1、%n
匹配
%n
的分类(其中 n 是一个数字),表示匹配第 n 个捕获的副本。( n 是从 1 开始)
lua
s = [[then he said: "it's all right"]]
-- 这里的匹配是 " ' 组合
print(string.match(s, "[\"'].-[\"']")) --> "it'
-- 这里使用了 %1 ,表示和第一个捕获保持一致,例如此处为 " "
-- 如果第一个匹配到的是 ' ,则为 ' '
print(string.match(s, "([\"'])(.-)%1")) --> " it's all right
%0
用在 string.gsub
替换字符串的函数中,则表示匹配到的所有内容。使用 %n
( n >= 1 )则表示捕获到的第几个
lua
s = "Hello Lua!"
-- %0 则表示每次匹配到的字符
print(string.gsub(s, "%a", "%0-%0")) --> H-He-el-ll-lo-o L-Lu-ua-a! 8
-- 此处 %1 和 %0 其实是一样的,因为只有一个匹配,没有什么区别
print(string.gsub(s, "%a", "%1-%1")) --> H-He-el-ll-lo-o L-Lu-ua-a! 8
-- 这里 %1 就是第一个捕获的内容, %2 就是第二个捕获的内容, %0 就是整个匹配内容
print(string.gsub(s, "(.)(.)", "%2%1%0,")) --> eHHe,llll, oo ,uLLu,!aa!, 5
print(string.gsub(s, "(.)(.)(.)", "%0 - %2")) --> Hel - elo - oLua - u! 3
-- %0 整个匹配内容,包括不在括号内的字符也囊括
print(string.gsub(s, "(.).(.)", "%0,")) --> Hel,lo ,Lua,! 3
4-2、()
使用
()
用于捕获模式在目标字符串的位置
lua
print(string.match("hello jiang", "()ang()")) --> 9 12
print(string.find("hello jiang", "ang")) --> 9 11
五、写在最后
Lua 项目地址:Github传送门 (如果对你有所帮助或喜欢的话,赏个star吧,码字不易,请多多支持)
如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀。
公众号搜索 "江澎涌",更多优质文章会第一时间分享与你。