第一版是为 Lua 5.0 编写的。虽然在很大程度上仍然适用于后续版本,但也有一些不同之处。
第四版针对 Lua 5.3,可在 亚马逊 和其他书店购买。
通过购买这本书,您还可以帮助支持 Lua 项目


20.4 – 诀窍

模式匹配是操作字符串的强大工具。您只需调用 string.gsubfind 几次,即可执行许多复杂操作。但是,与任何能力一样,您都必须谨慎使用它。

模式匹配不能替代适当的解析器。对于快速而粗糙的程序,您可以在源代码上执行有用的操作,但很难构建出高质量的产品。作为一个很好的例子,考虑我们用于匹配 C 程序中注释的模式:'/%*.-%*/'。如果您的程序包含一个包含 "/*" 的字符串,您将得到错误的结果

    test = [[char s[] = "a /* here";  /* a tricky string */]]
    print(string.gsub(test, "/%*.-%*/", "<COMMENT>"))
      --> char s[] = "a <COMMENT>
具有此类内容的字符串很少见,对于您自己的用途,该模式可能会完成其工作。但您不能出售带有此类缺陷的程序。

通常,模式匹配对于 Lua 程序来说足够高效:奔腾 333MHz(按照当今标准,它不是一台快速的机器)花不到十分之一秒的时间来匹配文本中所有单词,该文本包含 200K 个字符(30K 个单词)。但您可以采取预防措施。您应该始终使模式尽可能具体;松散模式比具体模式慢。一个极端的例子是 '(.-)%$',用于获取字符串中直到第一个美元符号的所有文本。如果主题字符串包含美元符号,则一切正常;但假设该字符串不包含任何美元符号。该算法将首先尝试从字符串的第一个位置开始匹配模式。它将遍历整个字符串,寻找美元。当字符串结束时,模式将对于字符串的第一个位置失败。然后,该算法将再次执行整个搜索,从字符串的第二个位置开始,只是为了发现模式也不在那里匹配;依此类推。这将花费二次时间,对于一个包含 200K 个字符的字符串,在奔腾 333MHz 上将花费三个多小时。您只需将模式固定在字符串的第一个位置,使用 '^(.-)%$',即可纠正此问题。锚点告诉算法,如果它无法在第一个位置找到匹配项,则停止搜索。使用锚点,该模式可以在不到十分之一秒的时间内运行。

还要注意模式,即匹配空字符串的模式。例如,如果你尝试使用像“%a*”这样的模式匹配名称,你将在任何地方都能找到名称

    i, j = string.find(";$%  **#$hello13", "%a*")
    print(i,j)   --> 1  0
在此示例中,对 string.find 的调用已正确地在字符串开头找到了一个空字母序列。

编写以修饰符 -´ 开头或结尾的模式永远没有意义,因为它只会匹配空字符串。此修饰符始终需要周围有内容,以固定其扩展。类似地,包含“.*”的模式很棘手,因为此结构的扩展可能远超你的预期。

有时,使用 Lua 本身来构建模式很有用。作为一个示例,让我们看看如何在文本中查找长行,比如长度超过 70 个字符的行。那么,长行就是 70 个或更多个与换行符不同的字符序列。我们可以使用字符类“[^\n]”匹配与换行符不同的单个字符。因此,我们可以使用一个模式匹配长行,该模式将一个字符的模式重复 70 次,然后是零个或更多个此类字符。我们可以使用 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,而不将任何字符视为魔术字符。如果字符串 s1s2 是文字,则可以在编写字符串时为魔术字符添加适当的转义符。但如果这些字符串是变量值,则可以使用另一个 gsub 为你放置转义符

    s1 = string.gsub(s1, "(%W)", "%%%1")
    s2 = string.gsub(s2, "%%", "%%%%")
在搜索字符串中,我们转义所有非字母数字字符。在替换字符串中,我们仅转义“%´。

用于模式匹配的另一种有用技术是在实际工作之前预处理主题字符串。预处理使用的一个简单示例是将文本中所有带引号的字符串更改为大写,其中带引号的字符串以双引号(“"´)开头和结尾,但可能包含转义引号("\""

    follows a typical string: "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" 都必须来自编码,因为原始字符串中的任何 "\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
decode 与前一个示例类似,但它不包括最终字符串中的反斜杠;因此,我们可以直接调用 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