re
re模块是对 boost::regex 的封装,旨在完全替代 Lua 内置的正则表达式。相比 Lua 的内置正则表达式,它具有两个主要优势:
- 完整的 Unicode 支持。Lua 正则表达式操作的是字节而非字符,这在使用多字节字符时经常引发问题。
- 更强大、更灵活的语法。严格来说,Lua 并不支持正则表达式;它只具备一种基础的模式匹配语言,仅支持正则表达式功能的一小部分子集。而 boost::regex 则支持 Perl 兼容的正则表达式。
用法
通过 re = require 'aegisub.re' 导入此模块。
关于正则表达式语法,请参阅 boost.regex 文档。通常,网络上任何提及 Perl 正则表达式或 PCRE 的资料均适用于此模块的正则表达式。
匹配表(Match Tables)
下文中的几个函数会返回匹配表(以表的形式返回匹配),表的结构如下:
str(string)- 模式或捕获表达式匹配到的文本。
first(number)str在源字符串中的起始索引。注意,此索引从 1 开始,且以字节而非字符为单位,以便与 Lua 的字符串索引保持一致。last(number)str在源字符串中的结束索引。注意,此索引从 1 开始、包含结束位置,且以字节而非字符为单位,以便与 Lua 的字符串索引保持一致。
>>> re.match("abc", "b")
{
{
["str"] = "b",
["first"] = 2,
["last"] = 2
}
}
标志(Flags)
下列标志可传递给所有静态函数(包括 re.compile)。标志必须位于所有非标志参数之后,但可选参数可以跳过。
- re.ICASE
- 匹配时忽略大小写。
- re.NOSUB:
- 不设置反向引用和捕获组。当不需要它们时,可以提高性能。
- re.NEWLINE_ALT:
- 将换行符视为交替操作符(the alternation operator)
|。 - re.NO_MOD_M:
^和$仅匹配字符串的开始和结束,而不是换行处。- re.MOD_S:
- 将换行符视为普通字符,由
.匹配。 - re.MOD_X:
- 忽略表达式中未转义的空白字符,便于编写可读性更好的正则表达式。
- re.NO_EMPTY_SUBEXPRESSION:
- 不匹配空表达式/空分支。
>>> re.match("A", "a")
nil
>>> re.match("A", "a", re.ICASE, re.NOSUB)
{
{
["str"] = "A",
["first"] = 1,
["last"] = 1
}
}
re.compile
语法概要:expr = re.compile(pattern, [FLAGS])
编译一个正则表达式。重复使用已编译的正则表达式比每次使用时重新编译更快,通常也更具可读性。
@pattern(string)- 要编译的正则表达式。
expr(table)- 一个包含下文中所有函数的表,但不包含
pattern和flags参数。
>>> expr = re.compile("a")
>>> expr:split("eat banana")
{
"e",
"t b",
"n",
"n",
""
}
re.split
语法概要:chunks = re.split(str, pattern, skip_empty=false, max_splits=0)
在每次出现pattern的位置拆分字符串。
@str(string)- 要拆分的字符串。
@pattern(string)- 用于拆分的正则表达式。其捕获组会被忽略。
@skip_empty(boolean)- 在结果中不要包含长度为零的分段。
@max_splits(number)- 若大于零,表示拆分字符串的最大次数(即
#chunks最多为max_splits + 1)。 chunks(table)- 一个表,包含了字符串
str根据pattern每次匹配拆分后的各个部分。
>>> re.split("a,,b,c", ",")
{
"a",
"",
"b",
"c"
}
>>> re.split("a,,b,c", ",", true)
{
"a",
"b",
"c"
}
>>> re.split("a,,b,c", ",", false, 1)
{
"a",
",b,c",
}
re.gsplit
语法概要:iter = re.gsplit(str, pattern, skip_empty=false, max_splits=0)
re.split的迭代器版本。
@str(string)- 要拆分的字符串。
@pattern(string)- 用于拆分的正则表达式。其捕获组会被忽略。
@skip_empty(boolean)- 在结果中不要包含长度为零的分段。
@max_splits(number)- 若大于零,表示拆分字符串的最大次数(即
#chunks最多为max_splits + 1)。 iter(iterator over strings)- 一个迭代器,用于遍历字符串
str根据模式pattern每次匹配拆分后的各个部分。
>>> for str in re.gsplit("a,,b,c", ",") do
>>> print(str)
>>> end
a
b
c
>>> for str in re.gsplit("a,,b,c", ",", true) do
>>> print(str)
>>> end
a
b
c
>>> for str in re.gsplit("a,,b,c", ",", false, 1) do
>>> print(str)
>>> end
a
,b,c
re.find
语法概要:matches = re.find(str, pattern)
查找字符串str中所有匹配pattern的非重叠子串。
@str(string)- 要搜索的字符串。
@pattern(string)- 用于搜索的正则表达式。其捕获组将被忽略。
matches(table或nil)- 一个包含所有匹配项(每个匹配项都是一个匹配表)的表,若未找到任何匹配则返回
nil。
>>> re.find("☃☃", ".")
{
{
["str"] = "☃",
["first"] = 1,
["last"] = 3
},
{
["str"] = "☃",
["first"] = 4,
["last"] = 6
}
}
function contains_an_a(str)
if re.find(str, "a")
print("Has an a")
else
print("Doesn't have an a")
end
end
>>> contains_an_a("abc")
Has an a
>>> contains_an_a("def")
Doesn't have an a
re.gfind
语法概要:iter = re.gfind(str, pattern)
迭代遍历str中所有匹配pattern的非重叠子串。
@str(string)- 要搜索的字符串。
@pattern(string)- 用于搜索的正则表达式。其捕获组将被忽略。
iter(iterator over string, number, number)- 一个迭代器,每次迭代产生三个值:匹配到的字符串、该匹配在源字符串中的起始索引、匹配在源字符串中的结束索引(结束索引包含结束位置)。
>>> for str, start_idx, end_idx in re.gfind("☃☃", ".") do
>>> print(string.format("%d-%d: %s", start_idx, end_idx, str))
>>> end
1-3: ☃
4-6: ☃
re.match
语法概要:matches = re.match(str, pattern)
将模式串pattern与字符串进行匹配。与find不同,find返回所有匹配且不捕获子组,而此函数仅返回单个匹配及其捕获的子组。
@str(string)- 要搜索的字符串。
@pattern(string)- 用于搜索的正则表达式。
matches(table或nil)- 若未找到任何匹配则返回
nil。否则,返回一个表,第一项是一个完整匹配的匹配表,随后的项是每个捕获子表达式(如果有)对应的匹配表。
>>> re.match("{250 1173 380}Help!", "(\\d+) (\\d+) (\\d+)")
{
{
["str"] = "250 1173 380",
["first"] = 2,
["last"] = 13
},
{
["str"] = "250",
["first"] = 2,
["last"] = 4
},
{
["str"] = "1173",
["first"] = 6,
["last"] = 9,
},
{
["str"] = "380"
["first"] = 11,
["last"] = 13
}
}
re.gmatch
语法概要:iter = re.gmatch(str, pattern)
re.match的迭代器版本。
@str(string)- 要搜索的字符串。
@pattern(string)- 用于搜索的正则表达式。
matches(iterator over table)- 一个迭代器,用于遍历一个表。该表第一项是一个完整匹配的匹配表,随后的项是每个捕获子表达式(如果有)对应的匹配表。
re.sub
语法概要:out_str = re.sub(str, pattern, replace, max_count=0)
将str中每次出现的pattern替换为replace。
@pattern(string)- 用于搜索的正则表达式。
@replace(string或function)- 匹配项的替换内容。可以是要插入的字符串,也可以是针对每个匹配调用的函数。
如果
replace是字符串,它可以包含对匹配项的引用。&和\0会被替换为整个模式匹配到的文本,\<number>会被替换为相应的捕获子表达式。如果
replace是函数,当模式中没有捕获子表达式时,它将接收整个模式匹配到的文本作为参数进行调用;当存在捕获子表达式时,则为每个捕获的子表达式分别调用。该函数会接收三个参数:匹配到的字符串、匹配在源字符串中的起始索引、匹配在源字符串中的结束索引(结束索引包含结束位置)。如果函数返回一个字符串,则匹配项会被替换为该返回值;如果函数返回任何其他类型的值,则源字符串中对应的部分保持原样不变。 @max_count(number)- 如果大于零,表示要进行的最大替换次数。
out_str(string)- 替换后的输入字符串。
替换所有的\\k为\\kf:
>>> re.sub("{\\k10}a{\\k15}b{\\k30}c", "\\\\k", "\\\\kf")
{\kf10}a{\kf15}b{\kf30}c
替换所有的\\k和\\K为\\kf:
>>> re.sub("{\\K10}a{\\K15}b{\\k30}c", "\\\\k", "\\\\kf", re.ICASE)
{\kf10}a{\kf15}b{\kf30}c
对每个\\k的时长加1:
function add_one(str)
return tostring(tonumber(str) + 1)
end
>>> re.sub("{\\k10}a{\\k15}b{\\k30}c", "\\\\k(\\d+)", add_one)
{\k11}a{\k16}b{\k31}c
考虑以下例子:
-- 例子 1
re.sub("{\\y10}a{\\y15}b{\\y30}c", "\\\\y", "\\\\yf")
-- 例子 2
re.sub("{\\y10}a{\\y15}b{\\y30}c", "\\\\y", "\\yf")
-- 例子 3
re.sub("{\\y10}a{\\y15}b{\\y30}c", "\\\\y", function(str) return "\\yf" end)
其中,例子1和例子3是正确的,且输出结果相同。例子2是错误的,可能导致非预期结果(可能产生非预想的替换结果或者程序抛错,取决于正则引擎的实现)。
为什么几个例子反斜杠的数量会不同?
对于re模块,模式字符串pattern和替换内容replace都需要经过 Lua 转义和正则转义。但如果replace是函数,则replace仅需 Lua 转义。
在 Lua 字符串字面量中,反斜杠\必须写作\\,否则会被解释为转义字符(例如\n代表换行符)。要表示字面意义的两个字符\和n,需写为\\n。
在 Lua 解释过程中,上述例子被解释为:
源字符串(第1个参数):"{\\y10}a{\\y15}b{\\y30}c" → {\y10}a{\y15}b{\y30}c
模式串(第2个参数):"\\\\y" → \\y
替换内容(第3个参数):
例子1:
"\\\\yf" → \\yf例子2:
"\\yf" → \yf例子3(函数内的字符串):
"\\yf" → \yf
解释后的内容被传递给正则引擎。此时进行了正则转义:
源字符串:不进行正则转义。依旧为{\y10}a{\y15}b{\y30}c
模式串:\\y → \y
替换内容:
例子1:
\\yf → \yf例子2:
\yf → ??(由于\y不是有效的单字符正则类(例:字符正则类\d代表数字0-9),正则引擎可能抛出错误,或忽略\而将其视为yf)例子3:由于是函数,不进行正则转义。函数内的字符串依旧为
\yf
当然,我们可以用长格式中括号来定义字符串,Lua 不会对这样定义的字符串解释任何转义序列(但还是会正则转义),上面的例子等价于:
-- 例子 1
re.sub([[{\y10}a{\y15}b{\y30}c]], [[\\y]], [[\\yf]])
-- 例子 2
re.sub([[{\y10}a{\y15}b{\y30}c]], [[\\y]], [[\yf]])
-- 例子 3
re.sub([[{\y10}a{\y15}b{\y30}c]], [[\\y]], function(str) return [[\yf]] end)