一、Lua字符串介绍
- Lua语言中的字符串是一串字节组成的序列。在Lua语言中,字符使用8个比特位来存储
- Lua语言中的字符串可以存储包括空字符在内的所有数值代码,这意味着我们可以在字符串中存储任意的二进制数据
- 我们也可以使用任意一种编码方式(UTF-8、UTF-16等)来存储Unicode字符串(在文章下面会详细介绍)
二、字符串常量
- 我们可以使用一对双引号或者单引号来声明字符串常量。例如:
a = "a line"
a
b = 'another line'
b
- 双引号和单引号声明字符串是等价的,区别在于:
- 使用双引号声明的字符串中出现单引号时,单引号可以不用转义
- 使用单引号声明的字符串中出现单引号时,双引号可以不用转义
a = "I am 'C++'"
a
b = 'I am "Lua"'
b
- 双引号与单引号如何选择使用呢?
- 在同一个程序中,一般规定统一使用哪种形式的字符串
- 另外,根据实际的需求使用哪一种。例如,在XML文本中会有双引号,因此一个操作XML的库可能就会使用单引号来声明XML片段
三、转义字符
- Lua支持C语言风格的转义字符
- 下面是一些使用案例
print("one line\nnext line\n\"in quotes\", 'in quotes'")
print('a backslash inside quotes: \'\\\'')
print("a simpler way: '\\'")
其他转义模式
- 在字符串中,你还可以通过下面的方式来声明字符
- \ddd:其中ddd是由最多3个十进制数字组成的序列(其中d可以为1~3个)
- \xhh:hh是由两个且必须是两个十六进制数字组成的序列(此处的h必须为2个)
- 演示案例:在一个使用ASCII编码的系统中,下面两个字符串是等价于的
- 0x41(十进制的65)在ASCII编码中对应的字符为A(此处就使用到了上面的\xhh模式)
- \10对应的就是换行符\n
- \049对应数字1(此处就用到了上面的\ddd模式)
"ALO\n123\"" '\x41LO\10\04923"'
- 例如,我们还可以把上述字符串全部改为十六进制,来表示字符串中的每一个字符
'\x41\x4c\x4f\x0a\x31\x32\x33\x22'
转义序列
- 从Lua 5.3开始,可以使用转义序列\u{h...h}来声明UTF-8字符,花括号中可以支持任意有效的十六进制
- 例如:
"\u{3b1} \u{3b2} \u{3b3}"
四、字符串的长度
- 可以使用长度操作符(#)获取字符串的长度
- 备注:该操作符返回字符串占用的字节数,在某些编码中,这个值可能与字符串中字符的个数不同
- 例如:
a = "hello"
print(#a)
print(#"good bye")
五、字符串的拼接
- 可以使用连接操作符..来进行字符串连接。例如
"Hello ".."World"
"Hello ".."World ".."!!"
- 如果操作数中有数值,那么会把数值转换为字符串。例如:
- 注意:数字后面使用连接符时,必须要用一个空格隔开,否则Lua会把数字后面的第一个.当成小数点
"result is "..3
-- 3与后面的..必须要有空格
"result is "..3 .." dollar"
- 注意,在Lua中,字符串是不可变变量。字符串连接总是创建一个新字符串,而不会改变原来操作数的字符串。例如
a = "Hello"
a.." World"
a
六、长字符串/多行字符串
- 像常长注释/多行注释一样,可以使用一对双方括号来声明长字符串/多行字符串常量
- 例如:
-- </html>之后会两次换行, 一次为page的末尾有换行, 另一次为print()的换行
page = [[
<html>
<head>
<title> An HTML Page</tile>
</head>
<body>
<a href="http://www.lua.org">Lua</a>
</body>
</html>
]]
print(page)
两个注意事项
- 在这种字符串中,转义序列不会被转义
page = [[ Hello \n World ]] print(page)
- 如果多行字符串中的第一个字符是换行符,那么这个换行符会被忽略
-- 字符串开始换了两次行, 但是第一次换行被忽略了 page = [[ Hello \n World ]] print(page)
-- 第一个字符不是换行, 因此不会换行 page = [[aHello \n World ]] print(page)
特殊字符串声明
- 有时候长字符串可能会包含"a=b[c[i]]"这样的内容,或者字符串中可能有被注释掉的代码"--[[]]",所以其会与长字符串的声明产生冲突。例如下面的代码在运行时产生了错误
string = [[ Hello -- [[ 注释 ]] World ]] print(string)
- 为了应对这种情况,可以在左方括号之间加上任意数量的等号,例如[===[,这样当长字符串遇到]===]时才会结束
- 这种方式的原理是,Lua语言的语法扫描器会忽略中间所含等号数量不相同的方括号
- 对于注释而言,这种机制也同样使用,例如可以使用--[=[]=]来表示长注释
string = [===[ Hello -- [[ 注释 ]] World ]===] print(string)
\z转义字符
- 当代吗中需要使用常量文本时,使用长字符串是一种理想的选择。但是,对于非文本的常量我们不应该滥用长字符串。虽然Lua语言中的字符串常量可以包含任意字节,但是滥用这个特性并不明智(例如,可能导致某些文本编辑器出现异常)。同时,像"\r\n"一样的EOF序列在被读取的时候可能会被归一化为"\n"
- 作为替代方案,最好就是把这些可能引起歧义的二进制数据用十进制数值或者十六进制的数值转义序列进行表示,例如"\x13\x01\xA1\xBB"。不过,由于这种转义表示形成的字符串往往很长,所以对于长字符串来说仍可能是个问题
- 针对这种问题,Lua 5.2开始引入了转义序列\z,该转义符会跳过其后的所有空白字符,直到遇到第一个非空白字符
- 演示案例:
data1 = "\x00\x01\x02\x0A\x0B" -- [[ 这种声明方式是错误的 data1 = "\x00\x01\x02 \x0A\x0B" ]] -- \z会忽略\x02后面的所有换行符和空白符 data2 = "\x00\x01\x02\z \x0A\x0B"
七、强制类型转换
自动强制类型转换
- Lua语言在运行时提供了数值与字符串之间的自动转换
- 字符串转数字:
- 针对字符串的所有算术操作都会尝试将字符串转换为数值,然后再进行计算
- 不仅仅在算术操作时进行这种强制类型转转,还会在任何需要数值的情况下进行,例如函数math.sin()的参数
'2' - '1' -- 不可以进行强制类型转换, 因为H不是数字 'H' - '1' -- 会将参数转换为90 math.sin('90')
- 数字转字符串:在需要字符串的地方出现了数值时,它就会把数值转换为字符串(这种使用在上面已经介绍过了)
print(10 .. 20) print(10 .. '20') print('10' .. 20)
- 备注:与算术操作不同,比较操作符不会对操作数进行强制类型转换。为了避免出现不一致的结果,当比较操作符中混用了字符串和数值时,Lua语言会抛出异常。例如
2 < 15 -- 第一个字符2比第一个字符1大, 因此直接返回false "2" < "15" -- 抛出异常, 一个为数值, 另一个为字符串 2 < "15"
显式转换:字符串转数值(tonumber()函数)
- 可以调用该函数显式地将字符串转换为数值。例如:
tonumber(" -3") tonumber("10e4") tonumber("0x1.3p-4")
- 当字符串的内容不能表示为有效数字时将会返回nil。例如:
tonumber("10e") tonumber("a")
- tonumber()默认情况下使用的是十进制,但是也可以指明二进制到三十六进制之间的任意进制。例如:
tonumber("100101", 2) tonumber("fff", 16) tonumber("-ZZ", 36) -- 错误的, 因为9超出了八进制的范围 tonumber("987", 8)
显式转换:数值转字符串(tostring()函数)
- 可以调用该函数显式地将数值转换为字符串。例如:
tostring(10) type(tostring(10)) print(tostring(10) == "10")
- 这种转换并不能控制输出字符串的格式(例如,结果中十进制数字的个数),在文章下面我们会介绍通过函数string.format()来全面的控制字符串的格式
八、字符串标准库
- Lua的字符串标准库提供了很多功能同来处理字符串
- 字符串标准库默认处理的是1字节8bit的字符(数值为0~255之间)。这对于某些编码方式(例如ASCII或ISO-8859-1)适用,但对所有的Unicode编码来说都不适用。不过尽管如此,我们在文章下面可以看到字符串标准库的某些功能对UTF-8编码来说还是非常有用的
- 下面是一些常用的函数:
- string.len(s):返回字符串s的长度
- string.rep(s, n):将字符串s重复n此的结果。例如,可以通过调用string.rep("a", 2^20)来创建一个1MB大小的字符串(例如用于测试)
- string.reverse(s):返回一份s的副本(s本身不变),其中将s进行反转
- string.lower(s):返回一份s的副本(s本身不变),其中所有的大写字母都变为小写字母
- string.upper(s):返回一份s的副本(s本身不变),其中所有的小写字母都变为大写字母
string.len("Hello World")
string.rep("abc", 3)
string.reverse("A Long Line!")
string.lower("A Long Line!")
string.upper("A Long Line!")
- 其中lower()和upper()函数可以用来在忽略大小写的差异的原则下比较两个字符串
a = "Hello"
b = "World"
string.lower(a) < string.lower(b)
stirng.sub()
- string.sub(s, i, j):从字符串s中提取第i个到第j个字符(包括i和j),字符串的索引从1开始。例如:
s ="HelloWorld" string.sub(s, 1, 1) string.sub(s, 1, 3)
- 该函数还支持负数索引:
- 负数索引从字符串的结尾开始计数(从-1开始)
- -1代表最后一个字符,-2代表倒数第二个字符,以此类推.....
s = "HelloWorld" -- 得到字符串s从开头开始长度为5的前缀 string.sub(s, 1, 5) -- 得到字符串s从第6个字符开始的后缀 string.sub(s, 6, -1) -- 去掉字符串s的第一个字符和最后一个字符 string.sub(s, 2, -2)
- 该函数不会改变参数所指的字符串
string.char()、string.byte()
- string.char():接收0个或多个整数作为参数,然后将每个整数转换为对应的字符,最后返回这些字符连接而成的字符串
string.char(65) string.char(66) string.char(67) string.char(65, 66, 67)
- string.byte(s, i):返回字符串s中第i个字符的数值表示
string.byte("ABC", 1) string.byte("ABC", 2) string.byte("ABC", 3)
- 同理,string.byte()也支持负数索引。例如:
string.byte("ABC", -1) string.byte("ABC", -2) string.byte("ABC", -3)
- string.byte(s):如果忽略第二个参数,那么默认返回字符串s中第一个字符的数值表示
string.byte("ABC") string.byte("BC") string.byte("C")
- string.byte(s, i ,j):返回索引i到j之间(包括i和j)的所有字符的数值表示
-- 返回AB的数值表示 string.byte("ABCDEF", 1, 2) -- 返回所有字符的数值表示 string.byte("ABCDEF", 1, -1)
- string.byte(s, i ,j)的使用技巧:
- 一种常见的写法是利用{string.byte(s , 1, -1)}创建一个由字符串s中的字符组成的表
- 由于Lua语言限制了栈大小,所以也限制了一个函数的返回值的最大个数,默认最大为一百万个。因此,这个技巧不能用于大小超过1MB的字符串
{string.byte("ABCDEF", 1, 2)}
string.format()
- 该函数类似于C语言的print()函数,用来进行格式化的输出。例如:
x = 10 string.format("x = %d, y = %d", x, 20) -- 将x使用十六进制打印 string.format("x = %x", x) -- 在前面加上修饰符 string.format("x = 0x%x", x) -- 将x以浮点数的格式打印 string.format("x = %d", x) -- 打印字符串 tag, title = "hi", "a tile" string.format("<%s>%s</%s>", tag, title, tag)
- 还可以控制浮点数中小数点的位数。例如:
-- 保留4位小数点 string.format("pi = %.4f", math.pi)
- 还可以对数字进行补齐。例如:
d = 5; m =11; y = 1990 -- 每个数字必须占用2个位置, 不足的时候用0补齐 string.format("%02d/%02d/%04d", d, m, y) -- 每个数字必须占用2个位置, 不足的时候用空白补齐 string.format("%2d/%2d/%4d", d, m, y)
- 更多细节请参阅C语言的printf()函数,与printf()的函数是一样的
- 字符串标准库还提供了几个基于模式匹配的函数,例如find()和gsub()
stirng.find()
- string.find():
- 用于在指定的字符串中进行模式搜索
- 返回值:如果找到了返回模式的开始和结束位置,否则返回nil
string.find("Hello World", "Wor") string.find("Hello World", "wor") string.find("Hello World", "war")
string.gsub()
- string.gsub():
- 把所有匹配的模式用另一个字符串替换
- 返回值:返回替换之后的字符串,并且返回替换的字符个数
string.gsub("Hello World", "l", ".") string.gsub("Hello World", "ll", "..") string.gsub("Hello World", "a", ".")
冒号操作符的使用
- 可以使用冒号操作符像调用字符串的一个方法那样调用字符串标准库的所有函数
- 例如:
s = "HelloWorld" string.len(s) -- 使用冒号操作符调用 s:len() string.sub(s, 1, 2) -- 使用冒号操作符调用 s:sub(1,2)
- 在后面的“模式匹配”文章中还将继续学习上面的所有函数和关于模式匹配的所有知识
九、Unicode标准库
- 从Lua 5.3开始,Lua语言引入了一个用于操作UTF-8编码的Unicode字符串的标准库。当然,在引入这个标准之前,Lua语言也提供了对UTF-8字符串的合理支持
- UTF-8是Web环境中用于Unicode的主要编码之一。由于与AUTF-8编码SCII编码部分兼容,所以UTF-8对于Lua语言来说也是一种理想的编码方式。这种兼容性保证了用于ASCII字符串的一些字符串操作技巧也无须修改就可以用于UTF-8字符串
- UTF-8使用变长的多字节来编码一个Unicode字符。例如,UTF-8编码使用一个字节的65来代表A,使用两个字节的215-144代表希伯来语字符Aleph(其在Unicode中的编码是1488)。UTF-8编码使用一个字节表示所有ASCII范围内的字符(小于128)。对于其他字符,则使用字节序列表示,其中第一个字节的范围是[194, 244],而后续的字节范围是[128, 191]。更准确的说,对于两字节组成的序列来说,第一个字节的范围是[194, 223];对于三个字节组成的序列来说,第一个字节的范围是[224, 239];对于四个字节组成的序列来说,第一个字节的范围是[240, 244],这些范围相互之间均不重叠。这种特点保证了任意字符对应的字节序列不会在其他字符对应的字节序列中出现。特别地,一个小于128的字节永远不会出现在多字节序列中,它只会代表阈值对应的ASCII字符
- Lua语言中的一些机制对UTF-8字符串来说同样“有效”。由于Lua使用8个字节来编码字符,所以可以像操作其他字符串一样读写和存储UTF-8字符串。字符串常量也可以包含UTF-8数据(当然,读者可能需要使用支持UTF-8编码的编辑器来处理UTF-8编码的源文件)。字符串连接对UTF-8字符串同样适用。对字符串的比较(小于、小于等于,等等)会按照Unicode编码中的字符代码顺序进行(即代码点,下面会介绍)
- Lua语言的操作系统库和输入输出库是与对应系统之间的主要接口,所以它们是否支持UTF-8取决于对应的操作系统。例如,在Linux操作系统下文件名使用UTF-8编码,而在Windows操作系统下文件名使用UTF-8编码。因此,如果要在Windows操作系统中处理Unicode文件名,那么要么使用额外的库,要么就要修改Lua语言的标准库
字符串标准库的函数处理UTF-8字符串
- 上面介绍的字符串标准库中有些函数可以处理UTF-8字符串,而有的不行
- 使用语法有:
- 函数string.reverse()、string.upper()、string.lower()、string.byte()、string.char()不适用于UTF-8字符串,因为它们针对的都是1字节的字符
- 函数string.format()、string.rep()适用于UTF-8字符串(格式选项'%c'除外,该格式选项针对1字节的字符)
- 函数string.len()、string.sub()可以用于UTF-8字符串,其中的索引以字节为单位而不是以字符为单位
- 通常,这些函数已经够用了
- 下面介绍新的utf8标准库
- utf8.len()函数:
- 该函数返回字符串中UTF-8字符(代码点)的个数
- 备注:一个诸如Unicode等的超大字符集中的字符可能需要用两个或两个以上的字节表示,一个完整的Unicode字符就叫做代码点,不能直接使用字节位置或字节长度来对Unicode字符进行操作
- 如果该函数发现字符串中包含无效的字节序列,则返回nil外加第一个无效字节的位置
-- 在控制台打印乱码了
utf8.len("résumé")
utf8.len("ação")
utf8.len("Månen")
utf8.len("ab\x93")
- utf8.char():等价于string.char(),接收0个或多个整数作为参数,然后将每个整数转换为对应的Unicode字符,最后返回这些字符连接而成的字符串
-- 对应的为résumé, 在控制台打印乱码了
utf8.char(114, 233, 155, 117, 109, 233)
- utf8.codepoint():
- 等价于string.byte(),不过其是以字节为索引的
- utf8.codepoint(s, i):返回字符串s中第i个字符的数值表示
- utf8.codepoint(s):如果忽略第二个参数,那么默认返回字符串s中第一个字符的数值表示
- utf8.codepoint(s, i ,j):返回索引i到j之间(包括i和j)的所有字符的数值表示(以字节为索引的)
-- 在控制台打印乱码了
utf8.codepoint("résumé", 1)
utf8.codepoint("résumé")
utf8.codepoint("résumé", 6, 7)
- utf8.offset():
- 默认情况下,utf8.xx()系列函数都是以字节为索引的
- 如果想要以字符位置为索引,则可以使用该函数把字符位置转换为字节位置
- utf.offset()的索引也可以是负值,表示从字符串末尾开始计数
s = "Nähdän"
-- utf8.offset获取字符串中第5个字符的字节索引, 然后作为参数调用codepoint()函数
-- 打印288
utf8.codepoint(s, utf8.offset(s, 5))
-- ä
utf8.char(288)
- utf8.codes():该函数用于遍历UTF-8字符串中的每一个字符
for i, c in utf8.codes("Ação") do
print(i, c)
end
- 除了上面的内容,Lua语言没有再提供其他几只。Unicode具有如此多稀奇古怪的特性,以至于想从特定的语言中抽象出其中的任意一个概念基本上都是不太可能的。由于Unicode编码的字符和字素之间没有一对一的关系,所以甚至连字符的概念都是模糊的。例如,常见的字素é既可以使用单个代码点"\u{E9}"表示,也可以使用两个代码点表示("e\u{301}",即e后面跟一个区分标记)。其他诸如字母之类的基本概念在不同的语系中也有差异。由于这些复杂性的存在,如果想支持完整的Unicode就需要巨大的表,而这又与Lua语言精简的大小相矛盾。因此,对于这些特殊需求来说,最好的选择就使用外部库