1. Lua字符串标点规范化处理实战
在游戏开发或UI交互场景中,我们经常需要处理用户输入的文本内容,确保其符合统一的标点规范。最近我在开发一个游戏对话框系统时,遇到了一个典型需求:无论玩家输入什么文本,最终显示时都要保证以合适的标点结尾,同时保留原有的HTML样式标签。下面分享我实现的Lua标点规范化方案。
这个ensurePunctuation函数主要解决三个核心问题:
- 当字符串没有标点时自动添加句号
- 当字符串已有标点时保留原标点
- 当字符串包含多个连续感叹号时只保留一个
同时还需要正确处理包含HTML标签的情况,比如加粗、颜色等样式不能被打断。
2. 核心算法设计与实现
2.1 标点符号识别逻辑
首先我们需要定义什么是"标点符号"。在中文环境下,常见的句子结尾标点包括:
lua复制local punctuations = {
["。"] = true, -- 中文句号
["."] = true, -- 英文句号
["!"] = true, -- 中文感叹号
["!"] = true, -- 英文感叹号
["?"] = true, -- 中文问号
["?"] = true, -- 英文问号
-- 其他标点虽然不算句子结尾,但也需要识别
[","] = true, -- 中文逗号
[","] = true, -- 英文逗号
[";"] = true, -- 中文分号
[";"] = true, -- 英文分号
[":"] = true, -- 中文冒号
[":"] = true -- 英文冒号
}
使用哈希表存储标点符号可以实现O(1)时间复杂度的查找,这是考虑到性能优化的选择。在Lua中,table的key查找比数组遍历要高效得多。
2.2 多重感叹号处理
连续多个感叹号在游戏对话中很常见,但为了UI美观需要规范化。处理逻辑需要考虑两种情况:
- 纯文本情况:直接使用正则替换
lua复制textPart = textPart:gsub("!+", "!") -- 替换连续英文叹号
textPart = textPart:gsub("!+", "!") -- 替换连续中文叹号
- 包含HTML标签的情况:需要先解析标签位置,确保不破坏标签结构
lua复制local function processMultipleExclamation(text)
local result = ""
local pos = 1
while pos <= #text do
local startTag, endTag = string.find(text, "<[^>]+>", pos)
if startTag then
-- 处理标签前的文本
if startTag > pos then
local textPart = string.sub(text, pos, startTag - 1)
textPart = textPart:gsub("!+", "!")
textPart = textPart:gsub("!+", "!")
result = result .. textPart
end
-- 添加标签本身
result = result .. string.sub(text, startTag, endTag)
pos = endTag + 1
else
-- 处理剩余文本
local textPart = string.sub(text, pos)
textPart = textPart:gsub("!+", "!")
textPart = textPart:gsub("!+", "!")
result = result .. textPart
break
end
end
return result
end
关键点:HTML标签的正则表达式
<[^>]+>会匹配所有尖括号内的内容,确保样式标签不会被当作普通文本处理。
2.3 标点添加策略
当字符串没有标点时,我们需要在合适的位置添加默认标点(通常是中文句号)。这里有几个技术难点:
- 如何正确获取最后一个字符:
lua复制local lastChar = ""
local len = #plainText
if len > 0 then
-- 处理ASCII和非ASCII字符
local lastByte = string.byte(plainText, len)
if lastByte < 128 then
lastChar = string.sub(plainText, len, len) -- ASCII字符占1字节
else
lastChar = string.sub(plainText, -3) -- 中文字符通常占3字节
end
end
- 处理带HTML标签的字符串时,标点应该添加在标签内部还是外部:
lua复制if lastTagStart then
-- 在最后一个标签前添加句号(确保句号在标签内)
local beforeTag = string.sub(str, 1, lastTagStart - 1)
local tagAndAfter = string.sub(str, lastTagStart)
return beforeTag .. defaultPunct .. tagAndAfter
else
-- 没有标签,直接在末尾添加
return str .. defaultPunct
end
3. 完整实现与测试用例
3.1 函数完整代码
lua复制function CommonTipsPop.ensurePunctuation(str, defaultPunct)
if not str or str == "" then return str end
defaultPunct = defaultPunct or "。" -- 默认使用中文句号
-- 定义标点符号集合
local punctuations = {
["。"] = true, ["."] = true, -- 句号
["!"] = true, ["!"] = true, -- 感叹号
["?"] = true, ["?"] = true, -- 问号
[","] = true, [","] = true, -- 逗号
[";"] = true, [";"] = true, -- 分号
[":"] = true, [":"] = true -- 冒号
}
-- 处理多重感叹号(保留HTML标签)
local function processMultipleExclamation(text)
local result = ""
local pos = 1
while pos <= #text do
local startTag, endTag = string.find(text, "<[^>]+>", pos)
if startTag then
-- 处理标签前的文本
if startTag > pos then
local textPart = string.sub(text, pos, startTag - 1)
textPart = textPart:gsub("!+", "!")
textPart = textPart:gsub("!+", "!")
result = result .. textPart
end
-- 添加标签
result = result .. string.sub(text, startTag, endTag)
pos = endTag + 1
else
-- 处理剩余文本
local textPart = string.sub(text, pos)
textPart = textPart:gsub("!+", "!")
textPart = textPart:gsub("!+", "!")
result = result .. textPart
break
end
end
return result
end
-- 先处理多重感叹号
str = processMultipleExclamation(str)
-- 获取纯文本(去除HTML标签)
local plainText = string.gsub(str, "<[^>]+>", "")
if plainText == "" then return str end
-- 获取最后一个字符(根据字符长度正确获取)
local lastChar = ""
local len = #plainText
if len > 0 then
local lastByte = string.byte(plainText, len)
if lastByte < 128 then
-- ASCII字符(1字节,如英文标点)
lastChar = string.sub(plainText, len, len)
else
-- 非ASCII字符(可能是3字节中文)
lastChar = string.sub(plainText, -3)
end
end
-- 检查最后一个字符是否是标点符号
if lastChar and punctuations[lastChar] then
-- 有标点,直接返回原字符串
return str
else
-- 没有标点,需要添加句号
-- 找到最后一个标签的位置
local lastTagStart, lastTagEnd = string.find(str, "<[^>]+>[^<]*$")
if lastTagStart then
-- 在最后一个标签前添加句号(确保句号在标签内)
local beforeTag = string.sub(str, 1, lastTagStart - 1)
local tagAndAfter = string.sub(str, lastTagStart)
return beforeTag .. defaultPunct .. tagAndAfter
else
-- 没有标签,直接在末尾添加
return str .. defaultPunct
end
end
end
3.2 测试用例设计
完善的测试用例应该覆盖各种边界情况:
lua复制function CommonTipsPop.Test()
-- 无HTML标签测试
print("=== 无HTML标签测试 ===")
print("1. " .. CommonTipsPop.ensurePunctuation("是否返回角色?")) -- 是否返回角色?
print("2. " .. CommonTipsPop.ensurePunctuation("是否返回角色!!!")) -- 是否返回角色!
print("3. " .. CommonTipsPop.ensurePunctuation("是否返回角色")) -- 是否返回角色。
print("4. " .. CommonTipsPop.ensurePunctuation("是否返回角色。")) -- 是否返回角色。
print("5. " .. CommonTipsPop.ensurePunctuation("是否返回角色?")) -- 是否返回角色?
-- 有HTML标签测试
print("\n=== 有HTML标签测试 ===")
print("6. " .. CommonTipsPop.ensurePunctuation("<b>是否返回角色?</b>")) -- <b>是否返回角色?</b>
print("7. " .. CommonTipsPop.ensurePunctuation("<b>是否返回角色!!!</b>")) -- <b>是否返回角色!</b>
print("8. " .. CommonTipsPop.ensurePunctuation("<b>是否返回角色</b>")) -- <b>是否返回角色。</b>
print("9. " .. CommonTipsPop.ensurePunctuation("<font color='red'>是否返回角色?</font>")) -- <font color='red'>是否返回角色?</font>
print("10. " .. CommonTipsPop.ensurePunctuation("<font color='red'>是否返回角色!!!</font>")) -- <font color='red'>是否返回角色!</font>
print("11. " .. CommonTipsPop.ensurePunctuation("<font color='red'>是否返回角色</font>")) -- <font color='red'>是否返回角色。</font>
print("12. " .. CommonTipsPop.ensurePunctuation("<b>是否返回角色?</b>")) -- <b>是否返回角色?</b>
print("13. " .. CommonTipsPop.ensurePunctuation("<font color='red'>是否返回角色?</font>")) -- <font color='red'>是否返回角色?</font>
-- 边界情况测试
print("\n=== 边界情况测试 ===")
print("14. " .. CommonTipsPop.ensurePunctuation("")) -- 空字符串
print("15. " .. CommonTipsPop.ensurePunctuation(nil)) -- nil值
print("16. " .. CommonTipsPop.ensurePunctuation("纯标签<b></b>")) -- <b>。</b>
print("17. " .. CommonTipsPop.ensurePunctuation("测试英文标点.Test")) -- 测试英文标点.Test
print("18. " .. CommonTipsPop.ensurePunctuation("测试混合标点!?")) -- 测试混合标点!?
end
4. 性能优化与注意事项
4.1 性能考量
-
字符串操作优化:Lua的字符串拼接会产生新对象,在频繁操作时需要注意。我们的实现中只在必要时才进行字符串拼接。
-
正则表达式效率:
gsub操作虽然方便,但在大文本处理时可能会有性能问题。这里因为处理的都是短文本(通常是UI提示),所以影响不大。 -
内存使用:函数内部创建了多个临时字符串变量,在Lua中这些都会产生垃圾回收压力。如果要在高频调用的场景使用,可能需要进一步优化。
4.2 常见问题与解决方案
-
中英文字符混合问题:
- 中文字符通常是多字节的,直接取最后一个字节可能导致乱码
- 解决方案:通过
string.byte检查字符编码范围,区分处理
-
HTML标签嵌套问题:
- 复杂的嵌套标签可能导致正则表达式匹配失败
- 解决方案:目前实现只处理简单标签,如需处理复杂嵌套需要更强大的解析器
-
标点符号优先级问题:
- 当字符串以多个不同标点结尾时,应该保留哪个?
- 当前实现保留原样,实际可能需要根据业务需求调整
4.3 扩展建议
-
支持更多标点规则:
- 可以增加配置参数,允许自定义哪些标点算作句子结束
- 支持不同语言的标点习惯(如英文通常用句点而非中文句号)
-
性能监控:
- 在游戏中使用时,可以添加执行时间统计
- 如果发现性能瓶颈,可以考虑用C扩展实现核心逻辑
-
错误处理增强:
- 当前实现对异常输入处理比较简单
- 可以增加参数校验和更友好的错误提示
在实际游戏开发中,这类文本处理函数虽然看起来简单,但对用户体验影响很大。一个健壮的实现可以避免很多奇怪的显示问题,特别是在多语言支持的情况下。我在项目中就遇到过因为标点处理不当导致韩文显示异常的情况,后来通过类似的规范化处理解决了问题。