LuaString库转换的技巧(标准库相关)

转换的技巧(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 秒。但是你不能掉以轻心,应该一直对不同的情况特殊对待,尽可能的更明确的模式描述。一个限制宽松的模式比限制严格的模式可能慢很多。一个极端的例子是模式 ‘(.-)% ′ 用 来 获 取 一 个 字 符 串 内 ' 用来获取一个字符串内 符号以前所有的字符,如果目标串中存在 符 号 , 没 有 什 么 问 题 ; 但 是 如 果 目 标 串 中 不 存 在 符号,没有什么问题;但是如果目标串中不存在 符号。上面的算法会首先从目标串的第一个字符开始进行匹配,遍历整个字符串之后没有找到 符 号 , 然 后 从 目 标 串 的 第 二 个 字 符 开 始 进 行 匹 配 , … … 这 将 花 费 原 来 平 方 次 幂 的 时 间 , 导 致 在 一 个 奔 腾 333 M H z 的 机 器 中 需 要 3 个 多 小 时 来 处 理 一 个 200 K 的 文 本 串 。 可 以 使 用 下 面 这 个 模 式 避 免 上 面 的 问 题 ′ ( . − ) 符号,然后从目标串的第二个字符开始进行匹配,……这将花费原来平方次幂的时间,导致在一个奔腾 333MHz 的机器中需要 3 个多小时来处理一个 200K 的文本串。可以使用下面这个模式避免上面的问题 '^(.-)% 333MHz3200K使(.)’。定位符^告诉算法如果在第一个位置没有没找到匹配的子串就停止查找。使用这个定位符之后,同样的环境也只需要不到 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 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值