字符串模式匹配
00:00
Lua模式匹配与正则
概述
Lua 的模式匹配(Pattern Matching)是一种轻量级的文本匹配工具,功能介于简单的字符串查找和完整的正则表达式之间。Lua 没有使用 POSIX 正则表达式,而是实现了自己的一套模式系统,语法更简洁,执行速度更快。
虽然 Lua 模式不如正则表达式强大(不支持交替选择、非贪婪量词的灵活组合等),但对于大多数文本处理任务已经足够。理解 Lua 模式的语法和限制,可以避免在需要正则表达式时强行使用模式匹配,也可以在模式匹配够用时避免引入正则表达式库的额外依赖。
基础概念
模式与正则的区别
Lua 模式和正则表达式有几个关键区别:
- Lua 模式不支持交替选择(正则中的 | 操作符)
- Lua 模式不支持非贪婪量词的灵活组合(只有固定的非贪婪修饰符)
- Lua 模式使用 % 作为转义字符,正则使用 \
- Lua 模式的字符类用小写/大写字母表示(%d 表示数字,%D 表示非数字)
- Lua 模式支持捕获(capture),但最多支持 9 个捕获组
模式字符类
Lua 模式中的字符类:
- %a:任何字母
- %d:任何数字
- %l:任何小写字母
- %u:任何大写字母
- %w:任何字母和数字
- %s:任何空白字符(空格、制表符、换行等)
- %p:任何标点符号
- %c:任何控制字符
- 大写版本表示补集(%A 表示非字母,%D 表示非数字等)
- . :任何字符(除了换行符)
量词
- *:0 次或多次(贪婪)
- +:1 次或多次(贪婪)
- ?:0 次或 1 次
- -:0 次或多次(非贪婪,与 * 的区别在于匹配最短而非最长)
快速上手
基本查找
-- string.find 返回匹配的起止位置
local s = "Hello World"
local start, finish = string.find(s, "World")
print(start, finish) -- 输出: 7 11
-- 没找到返回 nil
local pos = string.find(s, "Lua")
print(pos) -- 输出: nil
基本匹配
-- string.match 返回匹配的字符串
local date = "2026-06-14"
local year = string.match(date, "%d%d%d%d")
print(year) -- 输出: 2026
-- 匹配邮箱格式(简化版)
local email = "user@example.com"
local user, domain = string.match(email, "(.+)@(.+)")
print(user) -- 输出: user
print(domain) -- 输出: example.com
基本替换
-- string.gsub 替换匹配的文本
local s = "hello world"
local result = string.gsub(s, "(%w+)", string.upper)
print(result) -- 输出: HELLO WORLD
-- 限制替换次数
local s = "aaa"
local result = string.gsub(s, "a", "b", 2) -- 只替换前 2 次
print(result) -- 输出: bba
详细用法
1. string.find 查找
string.find 返回匹配的起止位置,还可以返回捕获组:
-- 基本查找
local s = "Hello World"
local start, finish = string.find(s, "World")
print(string.format("位置: %d - %d", start, finish))
-- 带捕获的查找
local s = "name=Alice age=25"
local start, finish, name, age = string.find(s, "name=(%w+) age=(%d+)")
print(name) -- 输出: Alice
print(age) -- 输出: 25
-- plain 参数:纯文本查找(不解释模式)
local s = "file.txt"
local pos = string.find(s, ".", 1, true) -- true 表示纯文本查找
print(pos) -- 输出: 5(找到字面的点号,而不是"任意字符")
-- 从指定位置开始查找
local s = "abcabc"
local pos = string.find(s, "abc", 2) -- 从第 2 个字符开始查找
print(pos) -- 输出: 4
2. string.match 匹配
string.match 只返回匹配的内容,不返回位置:
-- 简单匹配
local s = "The price is $99.9"
local price = string.match(s, "%$(%d+%.?%d*)")
print(price) -- 输出: 99.9
-- 多个捕获
local s = "2026-06-14"
local y, m, d = string.match(s, "(%d+)%-(%d+)%-(%d+)")
print(string.format("年: %s, 月: %s, 日: %s", y, m, d))
-- 没有捕获时返回整个匹配
local s = "Hello 123 World"
local num = string.match(s, "%d+")
print(num) -- 输出: 123
3. string.gmatch 迭代匹配
string.gmatch 返回一个迭代器,用于遍历所有匹配:
-- 提取所有数字
local s = "I have 3 apples and 5 oranges"
for num in string.gmatch(s, "%d+") do
print(num)
end
-- 输出: 3
-- 输出: 5
-- 提取所有键值对
local s = "name=Alice&age=25&city=Beijing"
local params = {}
for key, value in string.gmatch(s, "(%w+)=(%w+)") do
params[key] = value
end
-- params = {name = "Alice", age = "25", city = "Beijing"}
-- 提取所有单词
local s = "Hello, World! This is Lua."
for word in string.gmatch(s, "%a+") do
print(word)
end
-- 输出: Hello World This is Lua
4. string.gsub 替换
string.gsub 可以用字符串、函数或表作为替换:
-- 用字符串替换
local s = "Hello World"
local result = string.gsub(s, "World", "Lua")
print(result) -- 输出: Hello Lua
-- 用捕获组替换
local s = "2026-06-14"
local result = string.gsub(s, "(%d+)%-(%d+)%-(%d+)", "%2/%3/%1")
print(result) -- 输出: 06/14/2026
-- 用函数替换
local s = "The temperature is 25 degrees"
local result = string.gsub(s, "%d+", function(num)
return tostring(tonumber(num) * 9 / 5 + 32) -- 摄氏转华氏
end)
print(result) -- 输出: The temperature is 77 degrees
-- 用表替换
local replacements = {
hello = "hi",
world = "earth",
}
local s = "hello world"
local result = string.gsub(s, "%w+", replacements)
print(result) -- 输出: hi earth
-- 限制替换次数
local s = "aabbcc"
local result = string.gsub(s, "a", "x", 1) -- 只替换第一个
print(result) -- 输出: xabbcc
5. 锚点
Lua 模式支持两个锚点:
-- ^ 匹配字符串开头
local s = "Hello World"
print(string.match(s, "^Hello")) -- 输出: Hello
print(string.match(s, "^World")) -- 输出: nil(不以 World 开头)
-- $ 匹配字符串结尾
print(string.match(s, "World$")) -- 输出: World
print(string.match(s, "Hello$")) -- 输出: nil(不以 Hello 结尾)
-- 同时使用锚点检查完整匹配
local function isDigitOnly(s)
return string.match(s, "^%d+$") ~= nil
end
print(isDigitOnly("12345")) -- true
print(isDigitOnly("12a45")) -- false
6. 非贪婪匹配
- 使用 - 实现非贪婪匹配(匹配尽可能短的字符串):
local s = "<b>bold</b> <i>italic</i>"
-- 贪婪匹配(*):匹配最长的结果
local result = string.match(s, "<.*>")
print(result) -- 输出: <b>bold</b> <i>italic</i>
-- 非贪婪匹配(-):匹配最短的结果
local result = string.match(s, "<.->")
print(result) -- 输出: <b>
-- 提取所有 HTML 标签内容
for tag in string.gmatch(s, "<(.-)>") do
print(tag)
end
-- 输出: b /b i /i
7. 字符集
使用方括号定义自定义字符集:
-- 匹配方括号中的任意一个字符
local s = "abc123"
print(string.match(s, "[a-z]+")) -- 输出: abc
print(string.match(s, "[0-9]+")) -- 输出: 123
-- 匹配元音字母
local s = "Hello World"
for vowel in string.gmatch(s, "[aeiouAEIOU]") do
print(vowel)
end
-- 补集:以 ^ 开头表示"不在方括号中的字符"
local s = "Hello 123 World"
print(string.match(s, "[^%s]+")) -- 输出: Hello(匹配非空白字符序列)
-- 匹配十六进制字符
local hex = "1A2B3C"
for byte in string.gmatch(hex, "[%da-fA-F]+") do
print(byte)
end
常见场景
场景一:解析配置行
-- 解析 key = value 格式的配置
local function parseConfig(text)
local config = {}
for line in string.gmatch(text, "[^\n]+") do
-- 跳过注释和空行
if not string.match(line, "^%s*#") and not string.match(line, "^%s*$") then
local key, value = string.match(line, "^%s*(%w+)%s*=%s*(.-)%s*$")
if key and value then
config[key] = value
end
end
end
return config
end
local cfg = parseConfig([[
host = localhost
port = 8080
# 这是注释
name = My Server
]])
-- cfg = {host = "localhost", port = "8080", name = "My Server"}
场景二:验证输入格式
-- 验证手机号(中国大陆11位数字)
local function isValidPhone(phone)
return string.match(phone, "^1%d{10}$") ~= nil
-- 注意:Lua 模式不支持 {10} 这种量词
-- 需要改写为:
end
-- Lua 模式不支持 {n} 量词,需要变通
local function isValidPhone(phone)
return string.match(phone, "^1%d%d%d%d%d%d%d%d%d%d$") ~= nil
end
-- 验证邮箱格式(简化版)
local function isValidEmail(email)
return string.match(email, "^[%w%.]+@[%w%.]+%.[%a]+$") ~= nil
end
print(isValidEmail("user@example.com")) -- true
print(isValidEmail("invalid-email")) -- false
注意事项与常见错误
转义特殊字符
Lua 模式中的特殊字符(如 .、%、-、*、+、?、^、$、[、]、(、))需要用 % 转义:
-- 错误:. 在模式中表示"任意字符"
local s = "file.txt"
local result = string.match(s, "file.txt")
-- 可能匹配 "fileXtxt" 等
-- 正确:用 %. 匹配字面的点号
local result = string.match(s, "file%.txt")
-- 匹配百分号本身
local s = "100%"
local result = string.match(s, "%d+%%") -- %% 匹配字面的 %
模式不支持 {n,m} 量词
Lua 模式不支持正则表达式中的 {n,m} 量词。如果需要精确匹配 n 次,只能重复写 n 次:
-- 正则:\d{4}-\d{2}-\d{2}
-- Lua 模式:
local date = "2026-06-14"
local y, m, d = string.match(date, "(%d%d%d%d)%-(%d%d)%-(%d%d)")
模式不支持交替选择
Lua 模式没有 | 操作符,无法直接表达”匹配 A 或 B”。变通方法:
-- 需求:匹配 "yes" 或 "no"
-- 正则:yes|no
-- Lua 变通:使用字符集或多次匹配
local s = "yes"
if string.match(s, "^yes$") or string.match(s, "^no$") then
print("匹配成功")
end
string.gsub 的返回值
string.gsub 返回两个值:替换后的字符串和替换次数。如果只想要字符串,注意不要丢失返回值:
-- 错误:可能丢失第二个返回值
local result = string.gsub("hello", "l", "r")
print(result) -- 输出: herro(正确,但 result 实际上是一个多返回值)
-- 正确:明确只取第一个返回值
local result = string.gsub("hello", "l", "r")
print(result) -- herro
-- 如果需要替换次数
local result, count = string.gsub("hello world", "l", "r")
print(result) -- herro worrd
print(count) -- 3
进阶用法
前后查找模拟
Lua 模式不支持正则的前后查找(lookahead/lookbehind),但可以通过捕获和多次匹配来模拟:
-- 需求:提取冒号后面的内容
local s = "key: value"
-- 简单方式:直接捕获
local value = string.match(s, ":%s*(.*)")
print(value) -- 输出: value
自定义字符串分割
-- 按分隔符分割字符串
function string.split(s, sep)
sep = sep or "%s+" -- 默认按空白分割
local result = {}
for item in string.gmatch(s, "([^" .. sep .. "]+)") do
table.insert(result, item)
end
return result
end
-- 使用
local parts = string.split("hello world lua", " ")
-- parts = {"hello", "world", "lua"}
local csv = string.split("a,b,c", ",")
-- csv = {"a", "b", "c"}
模式匹配的性能
对于简单的查找,string.find 的 plain 模式(纯文本查找)比模式匹配更快:
-- 如果只需要查找固定字符串,使用 plain 模式
local s = "Hello World"
local pos = string.find(s, "World", 1, true) -- true 启用 plain 模式
-- 比 string.find(s, "World") 更快,因为不需要解释模式