Lua中获取字符串长度
首先是关于字符长度的一些结束(可以不看)
在 Lua 中,获取字符串长度我们一般使用 #str(不建议使用 string.len(str) )!
local str = "abc" local len = #str print(len) -- 3 str = "你们好" len = #str print(len) -- 9
疑惑:
这里就出现了一个问题:为啥字符串 abc 的长度为 3,而字符串 你们好 的长度却是 9 呢?难道是哪里出问题了?当然不是!
其实这是字符编码导致的,在使用 UTF-8 字符编码的情况下,一个中文字符一般占 3 个字节,所以 3 个中文字符自然就是 9 个字节咯!
那么问题来了,现在我需要不管是中文字符还是其他字符,长度都为 1 该咋整呢?
查找资料了解了具体原因:
不同的编码格式占字节数是不同的,UTF-8编码下一个中文所占字节也是不确定的,通常是3个字符,可能是2个、4个字节;
出于效率考虑,于是又弄了一个UTF-16,不严谨地来说它等价于Unicode原生编码,它统一采用双字节表示一个字符
下面是Unicode和UTF-8转换的规则
Unicode
||
UTF-8
0000 - 007F
||
0xxxxxxx
0080 - 07FF
||
110xxxxx 10xxxxxx
0800 - FFFF
||
1110xxxx 10xxxxxx 10xxxxxx
例如"汉"字的Unicode编码是6C49
6C49在0800-FFFF之间,所以要用3字节模板:1110xxxx 10xxxxxx 10xxxxxx。将6C49写成二进制是:0110 1100 0100 1001,将这个比特流按三字节模板的分段方法分为0110 110001 001001,依次代替模板中的x,得到:
1110-0110 10-110001 10-001001,即E6 B1 89,这就是其UTF8的编码
GBK、GB2312收编的汉字占2个字节,严格地用iso8859-1无法表示汉字,只能转为问号
回到Lua中获取字符串长度问题:
这里记录两种方案:
方案一
-- 获取字符串的长度(任何单个字符长度都为1) --由于编码格式的原因,【#字符串】 的方式获取中文时是字节数量,所以按照视觉效果来说会觉得返回有误 function getStringLength(inputstr) if not inputstr or type(inputstr) ~= "string" or #inputstr <= 0 then --inputstr不为nil、类型为字符串、且长度不为0 return nil end local length = 0 -- 字符的个数 local i = 1 --累计的每个字符的字节数,如果 i = 0,那么跳出while条件就是 i >= #intutstr 或 i == #inputstr while true do --这里我们是通过获取一个字符的头字节来判断是几字节的,如汉字的头字节ASCII是大于223的,所以直接跳过后面2个字节的判断,byteCount = 3 local curByte = string.byte(inputstr, i) --获取单个字节的ASCII码 local byteCount = 1 --单个字符的字节数,根据ASCII判断字节数 if curByte > 239 then byteCount = 4 -- 4字节字符 elseif curByte > 223 then byteCount = 3 -- 汉字,3字节 elseif curByte > 128 then byteCount = 2 -- 双字节字符 else byteCount = 1 -- 单字节字符 end -- local char = string.sub(inputstr, i, i + byteCount - 1) -- print(char) -- 打印单个字符 i = i + byteCount length = length + 1 if i > #inputstr then break end end return length --返回字符个数 end local str = "I think,故我在.bmp" local len = getStringLength(str) --获取字符串字符长度 print(string.format("Number of characters: %s\nNumber of bytes: %s", len, #str))
以上代码输出结果如下:
注:这里可能不是很能理解上面的循环中的跳过条件,在上述示例中,当 “i”的值大于字符串的字节数时会跳出循环,而 “i” 的值时根据每一个字符的头字节来判断一个字符的字节长度,前面也说了,汉字的头字节是1110xxxx开头,也就是头字节的ASCII值是大于223的,竟然已经知道了这个字符是汉字,那么我们不再进行后面两个字节的判断,所以 “i” 的值也就 + 3(因为byteCount = 3),这样就直接跳到了下一个字符的头字节判断。这里要知到,字符和字节是不同的概念,字符可以由多个字节组成,下面我再通过图的方式来说明一下
1. 首先输出我们示例字符串的每个字节
local str = "I think,故我在.bmp" --示例字符串 print(str:byte(1, #str)) --输出每个字节的值
2. 我们会得到下面数据
可以明确的看到,英文和符号 “字符” 的头 “字节” ASCII码都是小于小于128的,也就是只占 1 字节
而中文 “字符” 的头 “字节” ASCII都是大于223的,说明占了3字节,所以我们也不在判断中文 “字符” 的其他 “字节”
如果不理解 “字符” 和 “字节”,可以参考:相关文章1 相关文章2
方案二
-- 计算 UTF8 字符串的长度,每一个中文算一个字符 function utf8len(input) local len = string.len(input) --这里获取到的长度为字节数,如示例长度为:21,而我们肉眼看到的长度应该是15(包含空格) local left = len --将字节长度赋值给将要使用的变量,作为判断退出while循环的字节长度 local cnt = 0 --将要返回的字符长度 local arr = {0, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc} --用来判断是否满足字节长度的列表 while left ~= 0 do --遍历每一个字符 --获取字节的ASCII值,这里的 “-” 代表反向对应的索引,-left:input反着第left --假设字符串字符input长度是:21,left的值是:21,那string.byte(input, -left)就是第一个字节的ASCII值 local tmp = string.byte(input, -left) --看上面两行 local i = #arr --获取判断列表的长度,同时作为字节长度 while arr[i] do --循环判定列表 if tmp >= arr[i] then --判定当前 “字符” 的 头“字节” ACSII值符合的范围 left = left - i --字符串字节长度 -i,也就是 减去字节长度 break --结束判断 end i = i - 1 --每次判断失败都说明不符合当前字节长度 end cnt = cnt + 1 --“字符” 长度+1 end return cnt --返回 “字符” 长度 end local str = "I think,故我在.bmp" local len = utf8len(str) --获取字符串字符长度 print(string.format("Number of characters: %s\nNumber of bytes: %s", len, str:len()))
注:方案二与方案一原理差不多,都是根据每个字符的头 “字节” 来判断字节长度,不同的是方案二是先遍历一个判断列表,根据符合 / 不符合条件来判断字节长度,如果一开始就符合了范围,那么字节长度就是判断列表的长度 “5”了,不过显然我们的示例字符串最大的字节长度也就 “3”