07 - 字符串与模式匹配 / Strings & Pattern Matching
字符串与模式匹配 / Strings & Pattern Matching
Lua 的模式匹配不是正则表达式,而是一个更简单、更轻量的子集。它没有 |(或)、零宽断言、贪婪/非贪婪切换——但足够覆盖 90% 的文本处理场景。
Lua’s pattern matching is not regex — it’s a simpler, lighter subset. No alternation |, zero-width assertions, or greedy/lazy switches — but it covers 90% of text processing needs.
🟢 基础 / Basics — 字符串操作
1. 字符串创建 / Creating Strings
-- 三种引号
local s1 = "hello"
local s2 = 'hello'
local s3 = [[multi
line]]
-- 转义字符
print("line1\nline2") -- 换行
print("tab\there") -- 制表符
print("quote: \"hello\"") -- 双引号
print("backslash: \\") -- 反斜杠
print("null: \0") -- 空字节
print("unicode: \u{1F600}") -- Unicode(笑脸)(Lua 5.3+)
2. 基本操作 / Basic Operations
local s = "Hello, Lua!"
-- 长度(字节数)
print(#s) -- 11
-- 索引(返回 ASCII 码)/ Indexing (returns byte value)
print(string.byte(s, 1)) -- 72('H' 的 ASCII 码)
print(s:byte(1, 3)) -- 72 101 108
-- 子串 / Substring
print(s:sub(1, 5)) -- "Hello"
print(s:sub(8)) -- "Lua!"
print(s:sub(-4, -1)) -- "Lua!"(负数从末尾计数)
-- 查找 / Find
print(s:find("Lua")) -- 8 10(起始位置和结束位置)
-- 大小写转换 / Case conversion
print(s:upper()) -- "HELLO, LUA!"
print(s:lower()) -- "hello, lua!"
-- 格式化 / Format
print(string.format("Name: %s, Age: %d", "Alice", 30))
print(string.format("%.2f", 3.14159)) -- "3.14"
-- 重复 / Repeat
print(string.rep("ab", 3)) -- "ababab"
-- 翻转 / Reverse
print(string.reverse("hello")) -- "olleh"
-- 字符串不可变!修改需要创建新串
local s = "Hello"
-- s[1] = "h" -- 错误!不能修改
s = "h" .. s:sub(2) -- 创建新字符串 "hello"
3. 字符串连接 / Concatenation
-- 用 .. 连接
local greeting = "Hello" .. " " .. "World" .. "!"
print(greeting) -- "Hello World!"
-- 隐式转换:数字自动转为字符串
print("Age: " .. 30) -- "Age: 30"
print("Pi = " .. 3.14) -- "Pi = 3.14"
-- 但 boolean 和 table 不会自动转
-- print("Value: " .. true) -- 错误!
print("Value: " .. tostring(true)) -- "Value: true"
-- 性能:大量拼接用 table.concat
local parts = {}
for i = 1, 10000 do
parts[i] = tostring(i)
end
local result = table.concat(parts, ", ") -- 高效!
🟡 进阶 / Intermediate — 模式匹配
1. 模式匹配基础字符类 / Pattern Character Classes
-- Lua 模式字符类 / Pattern character classes:
-- . 任意字符 / any character
-- %a 字母 / letters
-- %c 控制字符 / control characters
-- %d 数字 / digits
-- %l 小写字母 / lowercase letters
-- %p 标点 / punctuation
-- %s 空白字符 / whitespace
-- %u 大写字母 / uppercase letters
-- %w 字母和数字 / alphanumeric
-- %x 十六进制数字 / hexadecimal digits
-- %z 空字节 / the zero byte (represented as 0)
-- 对应的大写版本取反:
-- %A 非字母 / non-letters
-- %D 非数字 / non-digits
-- %S 非空白 / non-whitespace
-- 等等...
-- 示例 / Examples:
print(("Hello 123"):match("%a+")) -- "Hello"(连续字母)
print(("Hello 123"):match("%d+")) -- "123"(连续数字)
print(("Hello 123"):match("%w+")) -- "Hello123"(连续字母数字)
2. 四个核心函数 / Four Core Functions
-- 1. string.find(s, pattern, init, plain)
-- 返回匹配的起始和结束位置
local s = "Hello, World! Hello, Lua!"
local start, end_pos = s:find("Hello")
print(start, end_pos) -- 1 5
-- 从指定位置开始搜索
local start2, end2 = s:find("Hello", 10)
print(start2, end2) -- 15 19
-- 第四个参数为 true 时,按字面值匹配(关闭模式)
local start3 = s:find("Hello.", 1, true) -- '.' 不是通配符
print(start3) -- nil(字面值 "Hello." 不存在)
-- 2. string.match(s, pattern, init)
-- 返回匹配到的捕获内容(或整个匹配)
local version = "Lua 5.4.7"
local num = version:match("(%d+%.%d+%.%d+)")
print(num) -- "5.4.7"
-- 3. string.gsub(s, pattern, replacement, n)
-- 替换匹配到的内容
local s = "Hello, World!"
local result = s:gsub("World", "Lua")
print(result) -- "Hello, Lua!"
-- 第四个参数限制替换次数
local s = "aaa bbb aaa ccc aaa"
local result = s:gsub("aaa", "xxx", 2)
print(result) -- "xxx bbb xxx ccc aaa"
-- 4. string.gmatch(s, pattern)
-- 返回迭代器,遍历所有匹配
local s = "one 1, two 2, three 3"
for word, num in s:gmatch("(%a+) (%d+)") do
print(word, num)
end
-- one 1
-- two 2
-- three 3
3. 量词 / Quantifiers
-- + 一次或多次 / one or more (greedy)
-- * 零次或多次 / zero or more (greedy)
-- - 零次或多次 / zero or more (lazy)
-- ? 零次或一次 / zero or one
local s = "<b>bold</b> and <i>italic</i>"
-- 贪婪匹配 / Greedy
print(s:match("<.+>")) -- "<b>bold</b> and <i>italic</i>"
-- .+ 尽可能多地匹配!
-- 惰性匹配 / Lazy
print(s:match("<.->")) -- "<b>"
-- .- 尽可能少地匹配
-- 实用:提取所有 HTML 标签
for tag in s:gmatch("<.->") do
print(tag)
end
-- <b>
-- </b>
-- <i>
-- </i>
-- ? 匹配可选字符
local s = "color: red; colour: blue;"
for word in s:gmatch("colou?r") do -- u 是可选的
print(word)
end
-- color
-- colour
4. 捕获 / Captures
-- () 捕获匹配位置(数字)
-- (...) 捕获匹配内容
-- 捕获子匹配
local date = "2026-05-10"
local y, m, d = date:match("(%d+)-(%d+)-(%d+)")
print(y, m, d) -- 2026 05 10
-- 命名捕获(Lua 5.2 不支持,用编号代替)
local email = "[email protected]"
local user, domain = email:match("(%S+)@(%S+)")
print(user, domain) -- user example.com
-- 捕获位置(用 () )
local s = "Hello, World!"
local pos = s:match("World()")
print(pos) -- 12(匹配结束后的下一个位置)
-- 多捕获
local s = "key = value"
local key, value = s:match("(%S+)%s*=%s*(.+)")
print(key, value) -- key value
-- 嵌套捕获
local s = "function foo() end"
local func_name = s:match("function%s+(%a[%w_]*)%s*%(%)")
print(func_name) -- "foo"
5. 边界匹配 / Anchors
-- ^ 字符串开头 / beginning of string
-- $ 字符串结尾 / end of string
-- 检查是否以某模式开头
print(("Hello"):match("^Hel")) -- "Hel"
print(("Hello"):match("^el")) -- nil
-- 检查是否以某模式结尾
print(("Hello"):match("llo$")) -- "llo"
print(("Hello"):match("lo$")) -- nil
-- 整串匹配(^...$)
print(("123"):match("^%d+$")) -- "123"(全是数字)
print(("12a3"):match("^%d+$")) -- nil(不全是数字)
-- 实用:去除首尾空格
local function trim(s)
return s:match("^%s*(.-)%s*$")
end
print(trim(" hello ")) -- "hello"
🔴 高级 / Advanced — 模式匹配的底层与替代方案
1. 模式匹配的工作原理
-- Lua 的模式匹配使用回溯法(backtracking)
-- 回溯的最坏时间复杂度可能是 O(2^n)
-- 灾难性回溯示例(模式设计不当)
local s = string.rep("a", 30) .. "!"
-- 这个模式会极慢(不要在生产环境使用):
-- s:match("a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a$")
-- 每个 a? 有两种选择(匹配或不匹配),2^30 次回溯
-- 避免方法:用量词时确保模式不会产生大量重叠匹配
-- 使用 .- (惰性)代替 .* (贪婪),或更精确的字符类
2. 模式替换的高级用法 / Advanced gsub
-- 替换可以是函数
local s = "price: $10, tax: $2, total: $12"
local result = s:gsub("%$(%d+)", function(amount)
return string.format("$%.2f", tonumber(amount) * 1.1) -- 加 10% 税
end)
print(result)
-- "price: $11.00, tax: $2.20, total: $13.20"
-- 替换可以是表
local translations = {
hello = "你好",
world = "世界",
}
local s = "hello world"
local result = s:gsub("(%w+)", translations)
print(result) -- "你好 世界"
-- 表替换 + 函数替换的组合
local s = "name={{name}}, age={{age}}"
local data = {name="Alice", age=30}
local result = s:gsub("{{(%w+)}}", data)
print(result) -- "name=Alice, age=30"
3. 捕获位置的用法 / Position Captures
-- () 返回匹配位置而不是内容
local s = "Hello World"
local pos = s:match("World()")
print(pos) -- 12(匹配结束后的下一个位置)
-- 实用:在模式中插入非捕获的分组
-- Lua 没有非捕获组,用 % % 来匹配但不捕获
local s = "foo123bar"
-- 想匹配 "foo数字bar" 但只捕获数字
local num = s:match("foo(%d+)bar")
print(num) -- "123"
-- 用位置捕获来获取精确位置
local s = "Hello, World! Hello, Lua!"
local positions = {}
for startPos in s:gmatch("()Hello") do
positions[#positions + 1] = startPos
end
print(table.concat(positions, ", ")) -- "1, 15"
4. 字符串内部化机制 / String Interning
-- Lua 对所有字符串进行内部化(interning)
-- 相同内容的字符串在内存中只有一份
-- 字符串比较非常快(先比指针,再比长度,最后比内容)
-- 短字符串(通常 < 40 字节)会被内部化
-- 长字符串不内部化,但会缓存哈希值
-- 性能影响:
-- 1. 字符串比较很快(指针比较 + 哈希比较)
-- 2. 字符串作为 table 的 key 效率很高
-- 3. 创建大量不同字符串有内存开销
-- 实际建议:
-- 对于重复出现的字符串(如枚举值),直接使用字面量即可
-- Lua 会自动处理内部化,不需要手动管理
local STATUS_OK = "ok"
local STATUS_ERR = "error"
-- 这两个字符串被内部化后,多次使用时只有一份内存
5. 何时不用模式匹配 / When NOT to Use Patterns
-- 场景 1:简单的子串查找 → 用 plain=true
local s = "Hello, [World]!"
-- 不需要转义方括号:
local pos = s:find("[World]", 1, true) -- plain 模式
print(pos) -- 8
-- 场景 2:复杂的解析任务 → 用 LPEG
-- LPEG (Lua Parsing Expression Grammars) 是 Lua 的解析器组合库
-- 比模式匹配更强大,不会灾难性回溯
local lpeg = require "lpeg"
local P, S, R, V = lpeg.P, lpeg.S, lpeg.R, lpeg.V
-- 示例:用 LPEG 解析 CSV
local field = (P(1) - S(',\n"'))^0 / tostring
local quotedField = '"' * ((P(1) - P('"')) + P('""' * 1))^0 * '"'
local record = lpeg.Ct((quotedField + field) * (',' * (quotedField + field))^0)
local csv = record * (P('\n') * record)^0 * -1
-- LPEG 的优势:
-- - 线性时间复杂度(不会灾难性回溯)
-- - 可以处理更复杂的语法
-- - 更好的错误报告
小结 / Summary
| 层级 | 你需要知道的 / What You Need to Know |
|---|---|
| 🟢 基础 | sub、find、byte、format、..连接、#长度 |
| 🟡 进阶 | %d %a %s 字符类、+*?- 量词、^$ 锚定、() 捕获、gsub 替换 |
| 🔴 高级 | 回溯法原理与陷阱、gsub 用函数/表替换、LPEG 替代方案 |