前置知识: Lua

字符串模式匹配

00:00
5 min Intermediate 2026/6/14

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") 更快,因为不需要解释模式

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式