第一版是為 Lua 5.0 編寫的。雖然在很大程度上仍然適用於後續版本,但有一些不同之處。
第四版針對 Lua 5.3,可在 Amazon 和其他書店購買。
購買本書,您也可以幫助 支援 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