第一版是為 Lua 5.0 編寫的。雖然在很大程度上仍然適用於後續版本,但有一些差異。
第四版針對 Lua 5.3,可在 Amazon 和其他書店購買。
購買書籍,您還可以 支援 Lua 專案


20.4 – 業界訣竅

模式比對是處理字串的強大工具。您只要呼叫 string.gsubfind 幾次,就能執行許多複雜的運算。不過,與任何能力一樣,您必須小心使用。

模式比對並非適當剖析器的替代方案。對於快速且粗略的程式,您可以在原始碼上執行有用的處理,但很難建立品質良好的產品。一個很好的範例是,考量我們用來比對 C 程式中註解的模式:'/%*.-%*/'。如果您的程式有一個包含 "/*" 的字串,您將會得到錯誤的結果

    test = [[char s[] = "a /* here";  /* a tricky string */]]
    print(string.gsub(test, "/%*.-%*/", "<COMMENT>"))
      --> char s[] = "a <COMMENT>
包含此類內容的字串很少見,而且對於您自己的使用而言,該模式可能會發揮作用。但是,您不能販售有此類缺陷的程式。

通常,模式比對對 Lua 程式而言已經夠有效率了:Pentium 333MHz(依據今日標準而言並非快速的機器)花不到十分之一秒就能比對出一個包含 200K 個字元(30K 個字)的文字中的所有字詞。但是,您可以採取預防措施。您應該盡可能讓模式具體;鬆散的模式比具體的模式慢。一個極端的範例是 '(.-)%$',用於取得字串中直到第一個美元符號為止的所有文字。如果主旨字串有一個美元符號,一切都很好;但是,假設字串不包含任何美元符號。演算法會先嘗試從字串的第一個位置開始比對模式。它會瀏覽整個字串,尋找美元符號。當字串結束時,模式會失敗於字串的第一個位置。然後,演算法會從字串的第二個位置開始再次執行整個搜尋,只會發現模式也不在此處比對;以此類推。這將花費二次時間,對於一個包含 200K 個字元的字串,在 Pentium 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