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


20.3 – 捕获

捕获机制允许模式提取与模式部分匹配的主题字符串部分,以便进一步使用。通过在圆括号中编写要捕获的模式部分,您可以指定捕获。

当您为 string.find 指定捕获时,它会将捕获的值作为调用的额外结果返回。此功能的典型用法是将字符串分解为各个部分

    pair = "name = Anna"
    _, _, key, value = string.find(pair, "(%a+)%s*=%s*(%a+)")
    print(key, value)  --> name  Anna
模式 '%a+' 指定一个非空字母序列;模式 '%s*' 指定一个可能为空格序列。因此,在上面的示例中,整个模式指定一个字母序列,后跟一个空格序列,后跟 `=´,再后跟空格加上另一个字母序列。两个字母序列的模式都用圆括号括起来,以便在匹配发生时捕获它们。find 函数始终首先返回匹配发生的索引(我们在前面的示例中将其存储在虚拟变量 _ 中),然后返回模式匹配期间进行的捕获。以下是一个类似的示例
    date = "17/7/1990"
    _, _, d, m, y = string.find(date, "(%d+)/(%d+)/(%d+)")
    print(d, m, y)  --> 17  7  1990

我们还可以在模式本身中使用捕获。在模式中,像 '%d' 这样的项(其中 d 是一个单独的数字)仅匹配 d-th 捕获的副本。作为一个典型的用法,假设您想在字符串中查找用单引号或双引号括起来的子字符串。您可以尝试使用 '["'].-["']' 这样的模式,即一个引号后跟任何内容,后跟另一个引号;但对于像 "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 的替换字符串中。与模式一样,替换字符串可能包含 '%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 <em>change</em> that.
另一个有用的示例是修剪字符串
    function trim (s)
      return (string.gsub(s, "^%s*(.-)%s*$", "%1"))
    end
请注意模式格式的明智使用。两个锚点(`^´ 和 `$´)确保我们获得整个字符串。因为 '.-' 尝试尽可能少地扩展,所以两个模式 '%s*' 匹配两端的全部空格。另请注意,由于 gsub 返回两个值,因此我们使用额外的括号来丢弃额外结果(计数)。

捕获值的最后一次使用可能是最强大的。我们可以使用一个函数作为其第三个参数来调用 string.gsub,而不是替换字符串。以这种方式调用时,string.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?
如果您不确定给定变量是否具有字符串值,则可以对其值应用 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!",在该命令之后,word 表将是
    {"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 编码,这是 HTTP 用于在 URL 中发送参数的编码。此编码将特殊字符(例如 `=´、`&´ 和 `+´)编码为 "%XX",其中 XX 是该字符的十六进制表示形式。然后,它将空格更改为 `+´。例如,它将字符串 "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 匹配所有以 `%´ 开头的两位十六进制数字,并调用匿名函数。该函数将十六进制数字转换为数字(tonumber,基数为 16),并返回相应字符(string.char)。例如,
    print(unescape("a%2Bb+%3D+c"))  --> a+b = c

要解码对 name=value,我们使用 gfind。由于名称和值都不能包含 `&´ 或 `=´,因此我们可以使用模式 '[^&=]+' 匹配它们

    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 的对,并且对于每对,迭代器将相应捕获(如匹配字符串中的括号所示)作为 namevalue 的值返回。循环体仅对两个字符串调用 unescape,并将对存储在 cgi 表中。

相应的编码也很容易编写。首先,我们编写 escape 函数;此函数将所有特殊字符编码为 `%´ 后跟十六进制中的字符 ASCII 代码(format 选项 "%02X" 使用 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
encode 函数遍历要编码的表,构建结果字符串
    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