第20章 String库

20 String
Lua 解释器对字符串的支持很有限。一个程序可以创建字符串并连接字符串,但不能截取子串,检查字符串的大小,检测字符串的内容。在 Lua 中操纵字符串的功能基本来自于 string 库。
String 库中的一些函数是非常简单的: string.len(s) 返回字符串 s 的长度; string.rep(s, n) 返回重复 n 次字符串 s 的串;你使用 string.rep("a", 2^20) 可以创建一个 1M bytes 的字符串(比如,为了测试需要); string.lower(s) s 中的大写字母转换成小写( string.upper 将小写转换成大写)。如果你想不关心大小写对一个数组进行排序的话,你可以这样:
table.sort(a, function (a, b)
    return string.lower(a) < string.lower(b)
end)
string.upper string.lower 都依赖于本地环境变量。所以,如果你在 European Latin-1 环境下,表达式:
string.upper("a??o")
           --> "A??O".
调用 string.sub(s,i,j) 函数截取字符串 s 的从第 i 个字符到第 j 个字符之间的串。 Lua 中,字符串的第一个字符索引从 1 开始。你也可以使用负索引,负索引从字符串的结尾向前计数: -1 指向最后一个字符, -2 指向倒数第二个,以此类推。所以, string.sub(s, 1, j) 返回字符串 s 的长度为 j 的前缀; string.sub(s, j, -1) 返回从第 j 个字符开始的后缀。如果不提供第 3 个参数,默认为 -1 ,因此我们将最后一个调用写为 string.sub(s, j) string.sub(s, 2, -2) 返回去除第一个和最后一个字符后的子串。
s = "[in brackets]"
print(string.sub(s, 2, -2))     --> in brackets
记住: Lua 中的字符串是恒定不变的。 String.sub 函数以及 Lua 中其他的字符串操作函数都不会改变字符串的值,而是返回一个新的字符串。一个常见的错误是:
string.sub(s, 2, -2)
认为上面的这个函数会改变字符串 s 的值。如果你想修改一个字符串变量的值,你必须将变量赋给一个新的字符串:
s = string.sub(s, 2, -2)
string.char 函数和 string.byte 函数用来将字符在字符和数字之间转换。 string.char 获取 0 个或多个整数,将每一个数字转换成字符,然后返回一个所有这些字符连接起来的字符串。 string.byte(s, i) 将字符串 s 的第 i 个字符的转换成整数;第二个参数是可选的,缺省情况下 i=1 。下面的例子中,我们假定字符用 ASCII 表示:
print(string.char(97))                    --> a
i = 99; print(string.char(i, i+1, i+2))   --> cde
print(string.byte("abc"))                 --> 97
print(string.byte("abc", 2))              --> 98
print(string.byte("abc", -1))             --> 99
上面最后一行,我们使用负数索引访问字符串的最后一个字符。
函数 string.format 在用来对字符串进行格式化的时候,特别是字符串输出,是功能强大的工具。这个函数有两个参数,使用和 C 语言的 printf 函数几乎一模一样,你完全可以照 C 语言的 printf 来使用这个函数。第一个参数为格式化串:由指示符和控制格式的字符组成。指示符后的控制格式的字符可以为:十进制 'd' ;十六进制 'x' ;八进制 'o' ;浮点数 'f' ;字符串 's' 。在指示符 '%' 和控制格式字符之间还可以有其他的选项:用来控制更详细的格式,比如一个浮点数的小数的位数:
print(string.format("pi = %.4f", PI))
       --> pi = 3.1416
d = 5; m = 11; y = 1990
print(string.format("%02d/%02d/%04d", d, m, y))
        --> 05/11/1990
tag, title = "h1", "a title"
print(string.format("<%s>%s</%s>", tag, title, tag))
        --> <h1>a title</h1>
第一个例子, %.4f 代表小数点后面有 4 位小数的浮点数。第二个例子 %02d 代表以固定的两位显示十进制数,不足的前面补 0 。而 %2d 前面没有指定 0 ,不足两位时会以空白补足。对于格式串部分指示符得详细描述清参考 lua 手册,或者参考 C 手册,因为 Lua 调用标准 C printf 函数来实现最终的功能。
20.1 模式匹配函数
string 库中功能最强大的函数是: string.find (字符串查找), string.gsub (全局字符串替换), and string.gfind (全局字符串查找)。这些函数都是基于模式匹配的。
与其他脚本语言不同的是, Lua 并不使用 POSIX 规范的正则表达式 [4] (也写作 regexp )来进行模式匹配。主要的原因出于程序大小方面的考虑:实现一个典型的符合 POSIX 标准的 regexp 大概需要 4000 行代码,这比整个 Lua 标准库加在一起都大。权衡之下, Lua 中的模式匹配的实现只用了 500 行代码,当然这意味着不可能实现 POSIX 所规范的所有更能。然而, Lua 中的模式匹配功能是很强大的,并且包含了一些使用标准 POSIX 模式匹配不容易实现的功能。
string.find 的基本应用就是用来在目标串( subject string )内搜索匹配指定的模式的串。函数如果找到匹配的串返回他的位置,否则返回 nil. 最简单的模式就是一个单词,仅仅匹配单词本身。比如,模式 'hello' 仅仅匹配目标串中的 "hello" 。当查找到模式的时候,函数返回两个值:匹配串开始索引和结束索引。
s = "hello world"
i, j = string.find(s, "hello")
print(i, j)                        --> 1    5
print(string.sub(s, i, j))         --> hello
print(string.find(s, "world"))     --> 7    11
i, j = string.find(s, "l")
print(i, j)                        --> 3    3
print(string.find(s, "lll"))       --> nil
例子中,匹配成功的时候, string.sub 利用 string.find 返回的值截取匹配的子串。(对简单模式而言,匹配的就是其本身)
string.find 函数第三个参数是可选的:标示目标串中搜索的起始位置。当我们想查找目标串中所有匹配的子串的时候,这个选项非常有用。我们可以不断的循环搜索,每一次从前一次匹配的结束位置开始。下面看一个例子,下面的代码用一个字符串中所有的新行构造一个表:
local t = {}      -- table to store the indices
local i = 0
while true do
    i = string.find(s, "/n", i+1)   -- find 'next' newline
    if i == nil then break end
    table.insert(t, i)
end
后面我们还会看到可以使用 string.gfind 迭代子来简化上面这个循环。
string.gsub 函数有三个参数:目标串,模式串,替换串。他基本作用是用来查找匹配模式的串,并将使用替换串其替换掉:
s = string.gsub("Lua is cute", "cute", "great")
print(s)      --> Lua is great
s = string.gsub("all lii", "l", "x")
print(s)      --> axx xii
s = string.gsub("Lua is great", "perl", "tcl")
print(s)      --> Lua is great
第四个参数是可选的,用来限制替换的范围:
s = string.gsub("all lii", "l", "x", 1)
print(s)          --> axl lii
s = string.gsub("all lii", "l", "x", 2)
print(s)          --> axx lii
string.gsub 的第二个返回值表示他进行替换操作的次数。例如,下面代码涌来计算一个字符串中空格出现的次数:
_, count = string.gsub(str, " ", " ")
(注意, _ 只是一个哑元变量)
20.2 模式
你还可以在模式串中使用字符类。字符类指可以匹配一个特定字符集合内任何字符的模式项。比如,字符类 %d 匹配任意数字。所以你可以使用模式串 '%d%d/%d%d/%d%d%d%d' 搜索 dd/mm/yyyy 格式的日期:
s = "Deadline is 30/05/1999, firm"
date = "%d%d/%d%d/%d%d%d%d"
print(string.sub(s, string.find(s, date)))    --> 30/05/1999
下面的表列出了 Lua 支持的所有字符类:
.      任意字符
%a     字母
%c     控制字符
%d     数字
%l     小写字母
%p     标点字符
%s     空白符
%u     大写字母
%w     字母和数字
%x     十六进制数字
%z     代表 0 的字符
上面字符类的大写形式表示小写所代表的集合的补集。例如, '%A' 非字母的字符:
print(string.gsub("hello, up-down!", "%A", "."))
    --> hello..up.down. 4
(数字 4 不是字符串结果的一部分,他是 gsub 返回的第二个结果,代表发生替换的次数。下面其他的关于打印 gsub 结果的例子中将会忽略这个数值。)在模式匹配中有一些特殊字符,他们有特殊的意义, Lua 中的特殊字符如下:
( ) . % + - * ? [ ^ $
'%' 用作特殊字符的转义字符,因此 '%.' 匹配点; '%%' 匹配字符 '%' 。转义字符 '%' 不仅可以用来转义特殊字符,还可以用于所有的非字母的字符。当对一个字符有疑问的时候,为安全起见请使用转义字符转义他。
Lua 而言,模式串就是普通的字符串。他们和其他的字符串没有区别,也不会受到特殊对待。只有他们被用作模式串用于函数的时候, '%' 才作为转义字符。所以,如果你需要在一个模式串内放置引号的话,你必须使用在其他的字符串中放置引号的方法来处理,使用 '/' 转义引号, '/' Lua 的转义符。你可以使用方括号将字符类或者字符括起来创建自己的字符类(译者: Lua 称之为 char-set ,就是指传统正则表达式概念中的括号表达式)。比如, '[%w_]' 将匹配字母数字和下划线, '[01]' 匹配二进制数字, '[%[%]]' 匹配一对方括号。下面的例子统计文本中元音字母出现的次数:
_, nvow = string.gsub(text, "[AEIOUaeiou]", "")
char-set 中可以使用范围表示字符的集合,第一个字符和最后一个字符之间用连字符连接表示这两个字符之间范围内的字符集合。大部分的常用字符范围都已经预定义好了,所以一般你不需要自己定义字符的集合。比如, '%d' 表示 '[0-9]' '%x' 表示 '[0-9a-fA-F]' 。然而,如果你想查找八进制数,你可能更喜欢使用 '[0-7]' 而不是 '[01234567]' 。你可以在字符集 (char-set) 的开始处使用 '^' 表示其补集: '[^0-7]' 匹配任何不是八进制数字的字符; '[^/n]' 匹配任何非换行符户的字符。记住,可以使用大写的字符类表示其补集: '%S' '[^%s]' 要简短些。
Lua 的字符类依赖于本地环境,所以 '[a-z]' 可能与 '%l' 表示的字符集不同。在一般情况下,后者包括 'ç' 'ã' ,而前者没有。应该尽可能的使用后者来表示字母,除非出于某些特殊考虑,因为后者更简单、方便、更高效。
可以使用修饰符来修饰模式增强模式的表达能力, Lua 中的模式修饰符有四个:
+      匹配前一字符 1 次或多次
*      匹配前一字符 0 次或多次
-      匹配前一字符 0 次或多次
?      匹配前一字符 0 次或 1
'+' ,匹配一个或多个字符,总是进行最长的匹配。比如,模式串 '%a+' 匹配一个或多个字母或者一个单词:
print(string.gsub("one, and two; and three", "%a+", "word"))
    --> word, word word; word word
'%d+' 匹配一个或多个数字(整数):
i, j = string.find("the number 1298 is even", "%d+")
print(i,j)    --> 12   15
'*' '+' 类似,但是他匹配一个字符 0 次或多次出现 . 一个典型的应用是匹配空白。比如,为了匹配一对圆括号 () 或者括号之间的空白,可以使用 '%(%s*%)' 。( '%s*' 用来匹配 0 个或多个空白。由于圆括号在模式中有特殊的含义,所以我们必须使用 '%' 转义他。)再看一个例子, '[_%a][_%w]*' 匹配 Lua 程序中的标示符:字母或者下划线开头的字母下划线数字序列。
'-' '*' 一样,都匹配一个字符的 0 次或多次出现,但是他进行的是最短匹配。某些时候这两个用起来没有区别,但有些时候结果将截然不同。比如,如果你使用模式 '[_%a][_%w]-' 来查找标示符,你将只能找到第一个字母,因为 '[_%w]-' 永远匹配空。另一方面,假定你想查找 C 程序中的注释,很多人可能使用 '/%*.*%*/' (也就是说 "/*" 后面跟着任意多个字符,然后跟着 "*/" )。然而,由于 '.*' 进行的是最长匹配,这个模式将匹配程序中第一个 "/*" 和最后一个 "*/" 之间所有部分:
test = "int x; /* x */ int y; /* y */"
print(string.gsub(test, "/%*.*%*/", "<COMMENT>"))
    --> int x; <COMMENT>
然而模式 '.-' 进行的是最短匹配,她会匹配 "/*" 开始到第一个 "*/" 之前的部分:
test = "int x; /* x */ int y; /* y */"
print(string.gsub(test, "/%*.-%*/", "<COMMENT>"))
    --> int x; <COMMENT> int y; <COMMENT>
'?' 匹配一个字符 0 次或 1 次。举个例子,假定我们想在一段文本内查找一个整数,整数可能带有正负号。模式 '[+-]?%d+' 符合我们的要求,它可以匹配像 "-12" "23" "+1009" 等数字。 '[+-]' 是一个匹配 '+' 或者 '-' 的字符类;接下来的 '?' 意思是匹配前面的字符类 0 次或者 1 次。
与其他系统的模式不同的是, Lua 中的修饰符不能用字符类;不能将模式分组然后使用修饰符作用这个分组。比如,没有一个模式可以匹配一个可选的单词(除非这个单词只有一个字母)。下面我将看到,通常你可以使用一些高级技术绕开这个限制。
'^' 开头的模式只匹配目标串的开始部分,相似的,以 '$' 结尾的模式只匹配目标串的结尾部分。这不仅可以用来限制你要查找的模式,还可以定位( anchor )模式。比如:
if string.find(s, "^%d") then ...
检查字符串 s 是否以数字开头,而
if string.find(s, "^[+-]?%d+$") then ...
检查字符串 s 是否是一个整数。
'%b' 用来匹配对称的字符。常写为 '%bxy' x y 是任意两个不同的字符; x 作为匹配的开始, y 作为匹配的结束。比如, '%b()' 匹配以 '(' 开始,以 ')' 结束的字符串:
print(string.gsub("a (enclosed (in) parentheses) line",
                            "%b()", ""))
--> a line
常用的这种模式有: '%b()' '%b[]' '%b%{%}' '%b<>' 。你也可以使用任何字符作为分隔符。
20.3 捕获(Captures
Capture[5] 是这样一种机制:可以使用模式串的一部分匹配目标串的一部分。将你想捕获的模式用圆括号括起来,就指定了一个 capture
string.find 使用 captures 的时候,函数会返回捕获的值作为额外的结果。这常被用来将一个目标串拆分成多个:
pair = "name = Anna"
_, _, key, value = string.find(pair, "(%a+)%s*=%s*(%a+)")
print(key, value)    --> name   Anna
'%a+' 表示菲空的字母序列; '%s*' 表示 0 个或多个空白。在上面的例子中,整个模式代表:一个字母序列,后面是任意多个空白,然后是 '=' 再后面是任意多个空白,然后是一个字母序列。两个字母序列都是使用圆括号括起来的子模式,当他们被匹配的时候,他们就会被捕获。当匹配发生的时候, find 函数总是先返回匹配串的索引下标(上面例子中我们存储哑元变量 _ 中),然后返回子模式匹配的捕获部分。下面的例子情况类似:
date = "17/7/1990"
_, _, d, m, y = string.find(date, "(%d+)/(%d+)/(%d+)")
print(d, m, y)       --> 17 7 1990
我们可以在模式中使用向前引用, '%d' d 代表 1-9 的数字)表示第 d 个捕获的拷贝。看个例子,假定你想查找一个字符串中单引号或者双引号引起来的子串,你可能使用模式 '["'].-["']' ,但是这个模式对处理类似字符串 "it's all right" 会出问题。为了解决这个问题,可以使用向前引用,使用捕获的第一个引号来表示第二个引号:
s = [[then he said: "it's all right"!]]
a, b, c, quotedPart = string.find(s, "(["'])(.-)%1")
print(quotedPart)    --> it's all right
print(c)             --> "
第一个捕获是引号字符本身,第二个捕获是引号中间的内容( '.-' 匹配引号中间的子串)。
捕获值的第三个应用是用在函数 gsub 中。与其他模式一样, gsub 的替换串可以包含 '%d' ,当替换发生时他被转换为对应的捕获值。(顺便说一下,由于存在这些情况,替换串中的字符 '%' 必须用 "%%" 表示)。下面例子中,对一个字符串中的每一个字母进行复制,并用连字符将复制的字母和原字母连接起来:
print(string.gsub("hello Lua!", "(%a)", "%1-%1"))
    --> h-he-el-ll-lo-o L-Lu-ua-a!
下面代码互换相邻的字符 :
print(string.gsub("hello Lua", "(.)(.)", "%2%1"))
    --> ehll ouLa
让我们看一个更有用的例子,写一个格式转换器:从命令行获取 LaTeX 风格的字符串,形如:
/command{some text}
将它们转换为 XML 风格的字符串:
<command>some text</command>
对于这种情况 , 下面的代码可以实现这个功能:
s = string.gsub(s, "//(%a+){(.-)}", "<%1>%2</%1>")
比如,如果字符串 s 为:
the /quote{task} is to /em{change} that.
调用 gsub 之后,转换为:
the <quote>task</quote> is to change that.
另一个有用的例子是去除字符串首尾的空格:
function trim (s)
    return (string.gsub(s, "^%s*(.-)%s*$", "%1"))
end
注意模式串的用法,两个定位符( '^' '$' )保证我们获取的是整个字符串。因为,两个 '%s*' 匹配首尾的所有空格, '.-' 匹配剩余部分。还有一点需要注意的是 gsub 返回两个值,我们使用额外的圆括号丢弃多余的结果(替换发生的次数)。
最后一个捕获值应用之处可能是功能最强大的。我们可以使用一个函数作为 string.gsub 的第三个参数调用 gsub 。在这种情况下, string.gsub 每次发现一个匹配的时候就会调用给定的作为参数的函数,捕获值可以作为被调用的这个函数的参数,而这个函数的返回值作为 gsub 的替换串。先看一个简单的例子,下面的代码将一个字符串中全局变量 $varname 出现的地方替换为变量 varname 的值:
function expand (s)
    s = string.gsub(s, "$(%w+)", function (n)
       return _G[n]
    end)
    return s
end
 
name = "Lua"; status = "great"
print(expand("$name is $status, isn't it?"))
 
--> Lua is great, isn't it?
如果你不能确定给定的变量是否为 string 类型,可以使用 tostring 进行转换:
function expand (s)
    return (string.gsub(s, "$(%w+)", function (n)
       return tostring(_G[n])
    end))
end
 
print(expand("print = $print; a = $a"))
 
--> print = function: 0x8050ce0; a = nil
下面是一个稍微复杂点的例子,使用 loadstring 来计算一段文本内 $ 后面跟着一对方括号内表达式的值:
s = "sin(3) = $[math.sin(3)]; 2^5 = $[2^5]"
print((string.gsub(s, "$(%b[])", function (x)
    x = "return " .. string.sub(x, 2, -2)
    local f = loadstring(x)
    return f()
end)))
 
--> sin(3) = 0.1411200080598672; 2^5 = 32
第一次匹配是 "$[math.sin(3)]" ,对应的捕获为 "$[math.sin(3)]" ,调用 string.sub 去掉首尾的方括号,所以被加载执行的字符串是 "return math.sin(3)" "$[2^5]" 的匹配情况类似。
我们常常需要使用 string.gsub 遍历字符串,而对返回结果不感兴趣。比如,我们收集一个字符串中所有的单词,然后插入到一个表中:
words = {}
string.gsub(s, "(%a+)", function (w)
    table.insert(words, w)
end)
如果字符串 s "hello hi, again!" ,上面代码的结果将是:
{"hello", "hi", "again"}
使用 string.gfind 函数可以简化上面的代码:
words = {}
for w in string.gfind(s, "(%a)") do
    table.insert(words, w)
end
gfind 函数比较适合用于范性 for 循环。他可以遍历一个字符串内所有匹配模式的子串。我们可以进一步的简化上面的代码,调用 gfind 函数的时候,如果不显示的指定捕获,函数将捕获整个匹配模式。所以,上面代码可以简化为:
words = {}
for w in string.gfind(s, "%a") do
    table.insert(words, w)
end
下面的例子我们使用 URL 编码, URL 编码是 HTTP 协议来用发送 URL 中的参数进行的编码。这种编码将一些特殊字符(比如 '=' '&' '+' )转换为 "%XX" 形式的编码,其中 XX 是字符的 16 进制表示,然后将空白转换成 '+' 。比如,将字符串 "a+b = c" 编码为 "a%2Bb+%3D+c" 。最后,将参数名和参数值之间加一个 '=' ;在 name=value 对之间加一个 "&" 。比如字符串:
name = "al"; query = "a+b = c";  q="yes or no"
被编码为:
name=al&query=a%2Bb+%3D+c&q=yes+or+no
现在,假如我们想讲这 URL 解码并把每个值存储到表中,下标为对应的名字。下面的函数实现了解码功能:
function unescape (s)
    s = string.gsub(s, "+", " ")
    s = string.gsub(s, "%%(%x%x)", function (h)
       return string.char(tonumber(h, 16))
    end)
    return s
end
第一个语句将 '+' 转换成空白,第二个 gsub 匹配所有的 '%' 后跟两个数字的 16 进制数,然后调用一个匿名函数,匿名函数将 16 进制数转换成一个数字( tonumber 16 进制情况下使用的)然后再转化为对应的字符。比如:
print(unescape("a%2Bb+%3D+c"))     --> a+b = c
对于 name=value 对,我们使用 gfind 解码,因为 names values 都不能包含 '&' '=' 我们可以用模式 '[^&=]+' 匹配他们:
cgi = {}
function decode (s)
    for name, value in string.gfind(s, "([^&=]+)=([^&=]+)") do
       name = unescape(name)
       value = unescape(value)
       cgi[name] = value
    end
end
调用 gfind 函数匹配所有的 name=value 对,对于每一个 name=value 对,迭代子将其相对应的捕获的值返回给变量 name value 。循环体内调用 unescape 函数解码 name value 部分,并将其存储到 cgi 表中。
与解码对应的编码也很容易实现。首先,我们写一个 escape 函数,这个函数将所有的特殊字符转换成 '%' 后跟字符对应的 ASCII 码转换成两位的 16 进制数字(不足两位,前面补 0 ),然后将空白转换为 '+'
function escape (s)
    s = string.gsub(s, "([&=+%c])", function (c)
       return string.format("%%%02X", string.byte(c))
    end)
    s = string.gsub(s, " ", "+")
    return s
end
编码函数遍历要被编码的表,构造最终的结果串:
function encode (t)
    local s = ""
    for k,v in pairs(t) do
       s = s .. "&" .. escape(k) .. "=" .. escape(v)
    end
    return string.sub(s, 2)     -- remove first `&'
end
t = {name = "al", query = "a+b = c", q="yes or no"}
 
print(encode(t)) --> q=yes+or+no&query=a%2Bb+%3D+c&name=al
20.4 转换的技巧(Tricks of the Trade
模式匹配对于字符串操纵来说是强大的工具,你可能只需要简单的调用 string.gsub find 就可以完成复杂的操作,然而,因为它功能强大你必须谨慎的使用它,否则会带来意想不到的结果。
对正常的解析器而言,模式匹配不是一个替代品。对于一个 quick-and-dirty 程序,你可以在源代码上进行一些有用的操作,但很难完成一个高质量的产品。前面提到的匹配 C 程序中注释的模式是个很好的例子: '/%*.-%*/' 。如果你的程序有一个字符串包含了 "/*" ,最终你将得到错误的结果:
test = [[char s[] = "a /* here"; /* a tricky string */]]
print(string.gsub(test, "/%*.-%*/", "<COMMENT>"))
    --> char s[] = "a <COMMENT>
虽然这样内容的字符串很罕见,如果是你自己使用的话上面的模式可能还凑活。但你不能将一个带有这种毛病的程序作为产品出售。
一般情况下, Lua 中的模式匹配效率是不错的:一个奔腾 333MHz 机器在一个有 200K 字符的文本内匹配所有的单词 (30K 的单词 ) 只需要 1/10 秒。但是你不能掉以轻心,应该一直对不同的情况特殊对待,尽可能的更明确的模式描述。一个限制宽松的模式比限制严格的模式可能慢很多。一个极端的例子是模式 '(.-)%$' 用来获取一个字符串内 $ 符号以前所有的字符,如果目标串中存在 $ 符号,没有什么问题;但是如果目标串中不存在 $ 符号。上面的算法会首先从目标串的第一个字符开始进行匹配,遍历整个字符串之后没有找到 $ 符号,然后从目标串的第二个字符开始进行匹配,……这将花费原来平方次幂的时间,导致在一个奔腾 333MHz 的机器中需要 3 个多小时来处理一个 200K 的文本串。可以使用下面这个模式避免上面的问题 '^(.-)%$' 。定位符 ^ 告诉算法如果在第一个位置没有没找到匹配的子串就停止查找。使用这个定位符之后,同样的环境也只需要不到 1/10 秒的时间。
也需要小心空模式:匹配空串的模式。比如,如果你打算用模式 '%a*' 匹配名字,你会发现到处都是名字:
i, j = string.find(";$% **#$hello13", "%a*")
print(i,j)    --> 1 0
这个例子中调用 string.find 正确的在目标串的开始处匹配了空字符。永远不要写一个以 '-' 开头或者结尾的模式,因为它将匹配空串。这个修饰符得周围总是需要一些东西来定位他的扩展。相似的,一个包含 '.*' 的模式是一个需要注意的,因为这个结构可能会比你预算的扩展的要多。
有时候,使用 Lua 本身构造模式是很有用的。看一个例子,我们查找一个文本中行字符大于 70 个的行,也就是匹配一个非换行符之前有 70 个字符的行。我们使用字符类 '[^/n]' 表示非换行符的字符。所以,我们可以使用这样一个模式来满足我们的需要:重复匹配单个字符的模式 70 次,后面跟着一个匹配一个字符 0 次或多次的模式。我们不手工来写这个最终的模式,而使用函数 string.rep
pattern = string.rep("[^/n]", 70) .. "[^/n]*"
另一个例子,假如你想进行一个大小写无关的查找。方法之一是将任何一个字符 x 变为字符类 '[xX]' 。我们也可以使用一个函数进行自动转换:
function nocase (s)
    s = string.gsub(s, "%a", function (c)
       return string.format("[%s%s]", string.lower(c),
                                          string.upper(c))
    end)
    return s
end
 
print(nocase("Hi there!"))
    --> [hH][iI] [tT][hH][eE][rR][eE]!
有时候你可能想要将字符串 s1 转化为 s2 ,而不关心其中的特殊字符。如果字符串 s1 s2 都是字符串序列,你可以给其中的特殊字符加上转义字符来实现。但是如果这些字符串是变量呢,你可以使用 gsub 来完成这种转义:
s1 = string.gsub(s1, "(%W)", "%%%1")
s2 = string.gsub(s2, "%%", "%%%%")
在查找串中,我们转义了所有的非字母的字符。在替换串中,我们只转义了 '%' 。另一个对模式匹配而言有用的技术是在进行真正处理之前,对目标串先进行预处理。一个预处理的简单例子是,将一段文本内的双引号内的字符串转换为大写,但是要注意双引号之间可以包含转义的引号( """ ):
这是一个典型的字符串例子:
"This is "great"!".
我们处理这种情况的方法是,预处理文本把有问题的字符序列转换成其他的格式。比如,我们可以将 """ 编码为 "/1" ,但是如果原始的文本中包含 "/1" ,我们又陷入麻烦之中。一个避免这个问题的简单的方法是将所有 "/x" 类型的编码为 "/ddd" ,其中 ddd 是字符 x 的十进制表示:
function code (s)
    return (string.gsub(s, "//(.)", function (x)
       return string.format("//%03d", string.byte(x))
    end))
end
注意,原始串中的 "/ddd" 也会被编码,解码是很容易的:
function decode (s)
    return (string.gsub(s, "//(%d%d%d)", function (d)
       return "/" .. string.char(d)
    end))
end
如果被编码的串不包含任何转义符,我们可以简单的使用 ' ".-" ' 来查找双引号字符串:
s = [[follows a typical string: "This is "great"!".]]
s = code(s)
s = string.gsub(s, '(".-")', string.upper)
s = decode(s)
print(s)
    --> follows a typical string: "THIS IS "GREAT"!".
更紧缩的形式:
print(decode(string.gsub(code(s), '(".-")', string.upper)))
我们回到前面的一个例子,转换 /command{string} 这种格式的命令为 XML 风格:
<command>string</command>
但是这一次我们原始的格式中可以包含反斜杠作为转义符,这样就可以使用 "/" "/{" "/}" ,分别表示 '/' '{' '}' 。为了避免命令和转义的字符混合在一起,我们应该首先将原始串中的这些特殊序列重新编码,然而,与上面的一个例子不同的是,我们不能转义所有的 /x ,因为这样会将我们的命令( /command )也转换掉。这里,我们仅当 x 不是字符的时候才对 /x 进行编码:
function code (s)
    return (string.gsub(s, '//(%A)', function (x)
       return string.format("//%03d", string.byte(x))
    end))
end
解码部分和上面那个例子类似,但是在最终的字符串中不包含反斜杠,所以我们可直接调用 string.char
function decode (s)
    return (string.gsub(s, '//(%d%d%d)', string.char))
end
 
s = [[a /emph{command} is written as //command/{text/}.]]
s = code(s)
s = string.gsub(s, "//(%a+){(.-)}", "<%1>%2</%1>")
 
print(decode(s))
--> a <emph>command</emph> is written as /command{text}.
我们最后一个例子是处理 CSV (逗号分割)的文件,很多程序都使用这种格式的文本,比如 Microsoft Excel CSV 文件十多条记录的列表,每一条记录一行,一行内值与值之间逗号分割,如果一个值内也包含逗号这个值必须用双引号引起来,如果值内还包含双引号,需使用双引号转义双引号(就是两个双引号表示一个),看例子,下面的数组:
{'a b', 'a,b', 'a,"b"c', 'hello "world"!', }
可以看作为:
a b,"a,b"," a,""b""c", hello "world"!,
将一个字符串数组转换为 CSV 格式的文件是非常容易的。我们要做的只是使用逗号将所有的字符串连接起来:
function toCSV (t)
    local s = ""
    for _,p in pairs(t) do
       s = s .. "," .. escapeCSV(p)
    end
    return string.sub(s, 2)     -- remove first comma
end
如果一个字符串包含逗号活着引号在里面,我们需要使用引号将这个字符串引起来,并转义原始的引号:
function escapeCSV (s)
    if string.find(s, '[,"]') then
       s = '"' .. string.gsub(s, '"', '""') .. '"'
    end
    return s
end
CSV 文件内容存放到一个数组中稍微有点难度,因为我们必须区分出位于引号中间的逗号和分割域的逗号。我们可以设法转义位于引号中间的逗号,然而并不是所有的引号都是作为引号存在,只有在逗号之后的引号才是一对引号的开始的那一个。只有不在引号中间的逗号才是真正的逗号。这里面有太多的细节需要注意,比如,两个引号可能表示单个引号,可能表示两个引号,还有可能表示空:
"hello""hello", "",""
这个例子中,第一个域是字符串 "hello"hello", 第二个域是字符串 " """ (也就是一个空白加两个引号),最后一个域是一个空串。
我们可以多次调用 gsub 来处理这些情况,但是对于这个任务使用传统的循环(在每个域上循环)来处理更有效。循环体的主要任务是查找下一个逗号;并将域的内容存放到一个表中。对于每一个域,我们循环查找封闭的引号。循环内使用模式 ' "("?) ' 来查找一个域的封闭的引号:如果一个引号后跟着一个引号,第二个引号将被捕获并赋给一个变量 c ,意味着这仍然不是一个封闭的引号
function fromCSV (s)
    s = s .. ','      -- ending comma
    local t = {}      -- table to collect fields
    local fieldstart = 1
    repeat
       -- next field is quoted? (start with `"'?)
       if string.find(s, '^"', fieldstart) then
       local a, c
       local i = fieldstart
       repeat
           -- find closing quote
           a, i, c = string.find(s, '"("?)', i+1)
       until c ~= '"'    -- quote not followed by quote?
       if not i then error('unmatched "') end
           local f = string.sub(s, fieldstart+1, i-1)
           table.insert(t, (string.gsub(f, '""', '"')))
           fieldstart = string.find(s, ',', i) + 1
       else              -- unquoted; find next comma
           local nexti = string.find(s, ',', fieldstart)
           table.insert(t, string.sub(s, fieldstart,
                                              nexti-1))
           fieldstart = nexti + 1
       end
    until fieldstart > string.len(s)
    return t
end
 
t = fromCSV('"hello "" hello", "",""')
for i, s in ipairs(t) do print(i, s) end
    --> 1       hello " hello
    --> 2        ""
    --> 3
 
 
 
 
 

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值