【Lua 筆記】字串(String) - part 8

由於有款遊戲叫做 CSO(Counter-Strike Online),內建模式創世者模式(Studio)新增使用 Lua 及其遊戲的 API,所以突發奇想製作這個筆記。

這個筆記會在一開始先著重純粹的程式設計自學,在最後的章節才會與 CSO 遊戲 API 進行應用。

※本文篇幅較長,十分複雜,請小心閱讀,請務必收藏!!※

字串(String)

字串(String)是由許多字元(Character)所組成的。

字元(Character):中文字、英文字母(a-z)、底線等等任何符號皆是一種字元。

字串可以用以下三種方式表示:

  • 成對單引號括起來(’String’)
  • 成對雙引號括起來(”String”)
  • [[ ]] 裡面放字串 -> 多行字串

第三種方式舉例:

1
2
3
4
5
6
a = [[
Hi, this is a string
aaaaaaaa
]]

print(a)

輸出結果:

1
2
Hi, this is a string
aaaaaaaa

取得字串長度的另一種方式

string.len(裡面放字串),實際上這個方式與用 # 這個運算子的方式並沒有什麼區別,但 string.len() 的可讀性會比用 # 更高。

以下是用兩種方式的比較:

1
2
3
4
5
6
a = [[
Hi, this is a string
aaaaaaaa
]]

print(string.len(a)) --> 30
1
2
3
4
5
6
a = [[
Hi, this is a string
aaaaaaaa
]]

print(#a) --> 30

也可以用 utf8.len 函数:

1
2
3
4
5
6
7
8
9
local myString = "Hello, 世界!"

-- utf8.len()
local length1 = utf8.len(myString)
print(length1) -- 输出 10

-- string.len()
local length2 = string.len(myString)
print(length2) -- 输出 14

註:utf8 是一種編碼,主要針對 Unicode 進行編碼,詳細具體內容筆者推薦看這篇文章:UTF-8

可以看出當我們字串是中文字時,使用 string.len()(註:使用 # 的效果同屬於 string.len())便會使字串長度輸出不準確,這是因為中文字編碼的關係。

string.len() 跟 # 回傳的會是 ASCII 編碼字串的長度,而 utf8.len() 包含中文字與英文。

所以歸結出一個結論:

  • 一般英文字母(ASCII)用 string.len() 或 #
  • 中文字與英文字母混合用 utf8.len()

跳脫字元(Escape Character)

跳脫字元(Escape Character),又稱轉義字元(陸譯)、逃逸字元、逸出字元。是一種能夠針對字串進行特殊操作的字元,例如原本不能在字串裡面顯示單雙引號的字元,所以透過反斜線(\)結合單雙引號形成一個跳脫字元,就能夠變成一種能夠出現在字串內的字元。

話說這麼多,跳脫字元其實本身還是一個字元(廢話XD),總之他就是一種特殊的字元,能讓字串的操作變得更加多元、方便。

所以要使用跳脫字元之前,必須要有一個反斜線存在(\)。

以下有個表格是關於跳脫字元的使用(表格來源:Lua 字符串 | 菜鸟教程):

跳脫字元意義ASCII碼(十進位)
\a響鈴(BEL:bell)007
\b退格(BS:BackSpace),將目前位置移到前一列(換言之,就是刪除鍵)008
\f換頁(FF:Form Feed),將目前位置移到下頁開頭。012
\n換行(LF:Line Feed),將目前位置移到下一行開頭。010
\r回車(CR:Carriage Return),將目前位置移到本行開頭013
\t水平製表(HT:Horizontal Tab)(跳到下一個 TAB 位置,也就是按一下 tab 縮排)009
\v垂直製表(VT:Vertical Tab)011
\代表一個反斜線字元’’\’092
\’代表一個單引號字元039
\”代表一個雙引號字元034
\0空字元(NULL)000
\ddd1 到 3 位八進位數所代表的任意字元三位八進位
\xhh1 到 2 位十六進位所代表的任意字元二位十六進位

這個表格呢,等我們真的有需要用到它的功能時,再來看就好了,硬要說一個常用的話,或許就是 \n 了吧。

然後讓我們來練習一下:)

1
2
3
4
5
6
print("str\ning")
print("\"string\"")
print("strin\0g")
print("st\bring")
print("s\ftring")
print("strin\rg")

輸出結果:

1
2
3
4
5
6
7
8
str
ing
"string"
string
sring
s
tring
gtrin

不曉得各位有沒有發現,Lua 的 print() 好像會自動幫我們換行欸?如果我不想換行怎麼辦?沒關係,以下這個函數就能夠實現不換行的機制:io.write()

string 的方法(Method)

方法(Method)是什麼?這個問題,我們需要牽涉到程式設計當中,一個很重要的概念,稱之為類(Class),通常有 class 的語言,在程式設計上都被稱之為物件導向程式設計(Object-oriented programming)。

而方法其實是一種函數,屬於 class 的一部分,也就是說一個 class 當中可以有很多方法,這些方法都具有不同的功能,目前只要先理解這樣就好。

以下表格來源:Lua 字符串 | 菜鸟教程

Number方法及用途
1string.upper(argument) : 字串全部轉為大寫字母。
2string.lower(argument) : 字串全部轉為小寫字母。
3string.gsub(mainString,findString,replaceString,num) : 在字串中替換。mainString 為要操作的字串,findString 為被替換的字元,replaceString 要替換的字元,num 替換次數(可以忽略,則全部替換)。
4string.find(str, substr, [init, [plain]]) : 在一個指定的目標字串 str 中搜尋指定的內容 substr,如果找到了一個符合的子字串,就會回傳這個子字串的起始索引和結束索引,不存在則回傳 nil。init 指定了搜尋的起始位置,預設為 1,可以是一個負數,表示從後往前數的字元數。plain 表示是否使用簡單模式,預設為 false、true,只做簡單的查找子字串的操作,false 表示使用使用正規模式比對。
5string.reverse(arg) : 字串反轉
6string.format(…) : 回傳一個類似 printf(C 語言 print 函數,表示格式化)的格式化字串
7string.char(arg) 和 string.byte(arg[,int]) : char 將整數數字轉成字元並連接,byte 轉換字元為整數值(可以指定某個字元,預設第一個字元)。
8string.len(arg) : 計算字串長度。
9string.rep(string, n) : 回傳字串 string 的 n 個拷貝
10.. : 連接兩個子字串
11string.gmatch(str, pattern) : 回傳一個迭代器函數,每次呼叫這個函數,回傳一個在字串 str 找到的下一個符合 pattern 描述的子字串。如果參數 pattern 所描述的字串沒有找到,迭代函數回傳 nil。
12string.match(str, pattern, init) : string.match() 只尋找來源字串 str 中的第一個配對。參數 init 為可選,指定搜尋過程的起點,預設為 1。在成功配對時,函數將回傳配對運算式中的所有捕獲結果;如果沒有設置捕獲標記,則回傳整個配對字串。當沒有成功的配對時,回傳 nil。

:::spoiler 點開看以上方法的詳細範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
-- 定義一個字串
local str = "Hello, Lua!"

-- 1. 將字串轉為大寫
local upperStr = string.upper(str)
print("Uppercase: " .. upperStr)

-- 2. 將字串轉為小寫
local lowerStr = string.lower(str)
print("Lowercase: " .. lowerStr)

-- 3. 替換字串中的字元
local replacedStr = string.gsub(str, "Lua", "World")
print("Replaced: " .. replacedStr)

-- 4. 搜尋字串中的子字串
local startIdx, endIdx = string.find(str, "Lua")
if startIdx then
print("Found 'Lua' at: " .. startIdx .. " to " .. endIdx)
else
print("'Lua' not found")
end

-- 5. 反轉字串
local reversedStr = string.reverse(str)
print("Reversed: " .. reversedStr)

-- 6. 格式化字串
local formattedStr = string.format("Formatted: %s", str)
print(formattedStr)

-- 7. 將整數轉為字元,並將字元轉為整數
local charStr = string.char(76, 117, 97) -- 'Lua'
print("Char: " .. charStr)
local byteVal = string.byte(str, 1)
print("Byte of first character: " .. byteVal)

-- 8. 計算字串長度
local strLen = string.len(str)
print("Length: " .. strLen)

-- 9. 回傳字串的多個拷貝
local repeatedStr = string.rep(str, 2)
print("Repeated: " .. repeatedStr)

-- 10. 連接兩個子字串
local concatenatedStr = str .. " Let's learn Lua!"
print("Concatenated: " .. concatenatedStr)

-- 11. 使用迭代器函數匹配字串
for word in string.gmatch(str, "%a+") do
print("Word: " .. word)
end

-- 12. 匹配字串中的第一個配對
local matchedStr = string.match(str, "L%a+")
print("Matched: " .. matchedStr)

輸出結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Uppercase: HELLO, LUA!
Lowercase: hello, lua!
Replaced: Hello, World!
Found 'Lua' at: 8 to 10
Reversed: !auL ,olleH
Formatted: Hello, Lua!
Char: Lua
Byte of first character: 72
Length: 11
Repeated: Hello, Lua!Hello, Lua!
Concatenated: Hello, Lua! Let's learn Lua!
Word: Hello
Word: Lua
Matched: Lua

:::

字串格式化(String-Formating)

字串格式化是一個相當重要的語法之一,幾乎是每個程式語言都不可或缺的東西。實際應用有:進度條、醫院掛號顯示號碼、字串+顯示目前數字等等。

總之讓字串格式化之後,就能有許多操作、運算的空間,能讓原本不能動的字串,變成活生生的字串。

Lua 提供了 string.format() 函數來產生具有特定格式的字串,函數的第一個參數是格式, 之後是對應格式中每個代號的各種資料。

以下是格式化字串的 format specifiers(格式化規範符號),來源自 Lua 字符串 | 菜鸟教程

  • %c - 接受一個數字,並將其轉換為 ASCII 碼表中對應的字元(將ASCII碼轉成字元)
  • %d、%i - 接受一個數字並將其轉換為有號的整數格式
  • %o - 接受一個數字並將其轉換為八進位數格式
  • %u - 接受一個數字並將其轉換為無號整數格式
  • %x - 接受一個數字並將其轉換為十六進位數格式,使用小寫字母
  • %X - 接受一個數字並將其轉換為十六進位數格式,使用大寫字母
  • %e - 接受一個數字並將其轉換為科學記號格式,使用小寫字母 e
  • %E - 接受一個數字並將其轉換為科學記號格式,使用大寫字母 E
  • %f - 接受一個數字並將其轉換為浮點數格式
  • %g(%G) - 接受一個數字並將其轉換為 %e(%E,對應 %G)及 %f 中較短的一種格式
  • %q - 接受一個字串並將其轉換為可安全被 Lua 編譯器讀入的格式
  • %s - 接受一個字串並按照給定的參數格式化該字串

:::info
註:%u 被稱為是無號整數,是因為 C 語言有資料型態為 unsigned int、unsigned char 等等。unsigned(u) 就是沒有負數的意思,只有自然數。
:::

為進一步細化格式,可以在 % 號後新增參數,參數將以如下的順序讀入:

  1. 符號:一個 + 號表示其後的數字 format specifiers 會讓正數顯示正號。預設只有負數顯示符號。
  2. 占位修飾符號:一個 0,在後面指定了字串寬度時佔位用。不填時的預設占位修飾符號是空格。
  3. 對齊標識:在指定了字串寬度時,預設為向右對齊,增加 - 號可以改為向左對齊。
  4. 寬度數值:寬度數值用於指定輸出內容的最小寬度。如果實際內容的寬度小於指定的寬度數值,則會使用占位修飾符號(預設為空格)進行填充,以達到指定的寬度。(如:%05d、%5d)
  5. 小數位數 / 字串裁切:在寬度數值後增加的小數部分 n,若後接 f(浮點數 format specifiers,如 %6.3f)則設定該浮點數的小數只保留 n 位,若後接 s(字串 format specifiers,如 %5.3s)則設定該字串只顯示前 n 位。

以下是個範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
local number = 123.456
local integer = 42
local str = "Hello, Lua!"

-- 1. 符號:顯示正號
print(string.format("%+d", integer)) -- +42

-- 2. 占位修飾符號:使用 0 進行填充
print(string.format("%05d", integer)) -- 00042

-- 3. 對齊標識:向左對齊
print(string.format("%-5d", integer)) -- 42 (後面有三個空格)

-- 4. 寬度數值:指定最小寬度
print(string.format("%10s", str)) -- Hello, Lua!

-- 5. 小數位數 / 字串裁切
-- 小數位數:保留 2 位小數
print(string.format("%.2f", number)) -- 123.46

-- 字串裁切:只顯示前 5 位
print(string.format("%.5s", str)) -- Hello

-- 綜合範例:同時使用多個參數
print(string.format("%+010.2f", number)) -- +0000123.46
print(string.format("%-10.5s", str)) -- Hello (後面有五個空格)

輸出結果:

1
2
3
4
5
6
7
8
+42
00042
42
Hello, Lua!
123.46
Hello
+000123.46
Hello

再來一個練習範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
local number = 12345
local float_number = 123.456
local str = "Hello, Lua!"

print(string.format("%%c: %c", number)) -- 將數字轉換為 ASCII 字元
print(string.format("%%d: %d", number)) -- 將數字轉換為有號整數
print(string.format("%%i: %i", number)) -- 將數字轉換為有號整數
print(string.format("%%o: %o", number)) -- 將數字轉換為八進位數
print(string.format("%%u: %u", number)) -- 將數字轉換為無號整數
print(string.format("%%x: %x", number)) -- 將數字轉換為十六進位數(小寫)
print(string.format("%%X: %X", number)) -- 將數字轉換為十六進位數(大寫)
print(string.format("%%e: %e", float_number)) -- 將數字轉換為科學記號(小寫 e)
print(string.format("%%E: %E", float_number)) -- 將數字轉換為科學記號(大寫 E)
print(string.format("%%f: %f", float_number)) -- 將數字轉換為浮點數
print(string.format("%%g: %g", float_number)) -- 將數字轉換為 %e 或 %f 中較短的一種格式
print(string.format("%%G: %G", float_number)) -- 將數字轉換為 %E 或 %f 中較短的一種格式
print(string.format("%%q: %q", str)) -- 將字串轉換為可被 Lua 編譯器讀入的格式
print(string.format("%%s: %s", str)) -- 將字串按照給定的參數格式化

-- 複合型格式化
print(string.format("數字 %d 的八進位表示為 %o", number, number))
print(string.format("數字 %d 的十六進位表示為 %x", number, number))
print(string.format("浮點數 %f 的科學記號表示為 %e", float_number, float_number))

輸出結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
%c: 9
%d: 12345
%i: 12345
%o: 30071
%u: 12345
%x: 3039
%X: 3039
%e: 1.234560e+02
%E: 1.234560E+02
%f: 123.456000
%g: 123.456
%G: 123.456
%q: "Hello, Lua!"
%s: Hello, Lua!
數字 12345 的八進位表示為 30071
數字 12345 的十六進位表示為 3039
浮點數 123.456000 的科學記號表示為 1.234560e+02

字串匹配模式

Lua 中的匹配模式直接用常規的字串來描述。它用於模式匹配函數 string.findstring.gmatchstring.gsubstring.match

以下是一個範例,呈現 dd/mm/yyyy 格式的日期(來自:Lua 字符串 | 菜鸟教程):

1
2
3
s = "Deadline is 30/05/1999, firm"
date = "%d%d/%d%d/%d%d%d%d"
print(string.sub(s, string.find(s, date))) --> 30/05/1999

輸出結果:

1
30/05/1999

有關於 Lua 字元類別種類,可看以下條列:

  • .(點):與任何字元配對
  • %a:與任何字母配對
  • %c:與任何控制符配對(例如 \n)
  • %d:與任何數字配對
  • %l:與任何小寫字母配對
  • %p:與任何標點(punctuation)配對
  • %s:與空白字元配對
  • %u:與任何大寫字母配對
  • %w:與任何字母 / 數字配對
  • %x:與任何十六進制數配對
  • %z:與任何代表 0 的字元配對
  • %x(此處 x 是非字母非數字字元):與字元 x 配對。主要用來處理運算式中有功能的字元(^$()%.[]*+-?)的配對問題,例如 %% 與 % 配對。
  • [數個字元類別]:與任何 [] 中包含的字元類別配對。例如 [%w] 與任何字母 / 數字,或底線符號()配對。
  • 數個字元類別:與任何”不”包含在 [] 中的字元類別配對。例如 %s 與任何非空白字元配對。

當上述的字元類別以大寫書寫時,表示與非此字元類別的任何字元配對。例如,%S 表示與任何非空白字元配對。例如,’%A’非字母的字元:

1
2
> print(string.gsub("hello, up-down!", "%A", "."))
hello..up.down. 4

註:有一個 > 表示程式碼是在 Shell 底下執行。

接下來,同樣也是做練習啦,不練習不會進步~:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
local test_string = "Hello, World! 1234\n"

local function test_pattern(pattern, description)
if string.match(test_string, pattern) then
print(description .. ": 匹配成功")
else
print(description .. ": 匹配失敗")
end
end

test_pattern(".", "與任何字元配對")
test_pattern("%a", "與任何字母配對")
test_pattern("%c", "與任何控制符配對")
test_pattern("%d", "與任何數字配對")
test_pattern("%l", "與任何小寫字母配對")
test_pattern("%p", "與任何標點配對")
test_pattern("%s", "與空白字元配對")
test_pattern("%u", "與任何大寫字母配對")
test_pattern("%w", "與任何字母 / 數字配對")
test_pattern("%x", "與任何十六進制數配對")
test_pattern("%z", "與任何代表 0 的字元配對")
test_pattern("%%", "與字元 % 配對")
test_pattern("[%w_]", "與任何字母 / 數字或底線符號配對")
test_pattern("[^%s]", "與任何非空白字元配對")

test_pattern("%a+", "與一個或多個字母配對")
test_pattern("%d+", "與一個或多個數字配對")
test_pattern("%s+", "與一個或多個空白字元配對")
test_pattern("%u%l+", "與一個大寫字母後跟隨一個或多個小寫字母配對")

print("測試字串: " .. test_string)

輸出結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
與任何字元配對: 匹配成功
與任何字母配對: 匹配成功
與任何控制符配對: 匹配成功
與任何數字配對: 匹配成功
與任何小寫字母配對: 匹配成功
與任何標點配對: 匹配成功
與空白字元配對: 匹配成功
與任何大寫字母配對: 匹配成功
與任何字母 / 數字配對: 匹配成功
與任何十六進制數配對: 匹配成功
與任何代表 0 的字元配對: 匹配失敗
與字元 % 配對: 匹配失敗
與任何字母 / 數字或底線符號配對: 匹配成功
與任何非空白字元配對: 匹配成功
與一個或多個字母配對: 匹配成功
與一個或多個數字配對: 匹配成功
與一個或多個空白字元配對: 匹配成功
與一個大寫字母後跟隨一個或多個小寫字母配對: 匹配成功
測試字串: Hello, World! 1234

在模式匹配中有一些特殊字元,他們有特殊的意義,Lua中的特殊字元如下:

( ) . % + - * ? [ ^ $

‘%’ 用作特殊字元的跳脫字元,因此 ‘%.’ 匹配點;’%%’ 匹配字元 ‘%’。跳脫字元 ‘%’ 不僅可以用來跳脫特殊字元,還可以用於所有的非字母的字元。

以下節錄自:Lua 字符串 | 菜鸟教程

模式條目可以是:

  • 單一字元類別符合該類別中任意單一字元;
  • 單一字元類別跟一個 ‘*’,將匹配 0 或多個該類別的字元。這個條目總是匹配盡可能長的字串;
  • 單一字元類別跟一個 ‘+’,將匹配 1 或更多該類別的字元。這個條目總是匹配盡可能長的字串;
  • 單一字元類別跟一個 ‘-‘,將匹配 0 或更多該類別的字元。和 ‘*’ 不同,這個條目總是匹配盡可能短的字串;
  • 單一字元類別跟一個 ‘?’,將匹配 0 或一個該類別的字元。只要有可能,它就會匹配一個;
  • %n,這裡的 n 可以從 1 到 9;這個條目匹配一個等於 n 號捕獲物(後面會描述)的子字串。
  • %bxy,這裡的 x 和 y 是兩個明確的字元;這個條目匹配以 x 開始 y 結束,並且其中 x 和 y 保持平衡的字串。意思是,如果從左到右讀這個字串,對每次讀到一個 x 就 +1,讀到一個 y 就 -1,最終結束處的那個 y 是第一個記數到 0 的 y。舉個例子,條目 %b() 可以匹配到括號平衡的運算式。
  • %f[set],邊境模式;這個條目會匹配到一個位於 set 內某個字元之前的一個空字串,且這個位置的前一個字元不屬於 set。集合 set 的意思如前面所述。配對出的那個空字串之開始和結束點的計算就看成該處有個字元 ‘\0’ 一樣。

以下是一個範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
-- 測試字串 1
local test_str = "abc123xyz"

-- 單一字元類別匹配
local pattern1 = "[a-z]" -- 匹配任意小寫字母
print(test_str:match(pattern1)) -- a

-- 單一字元類別跟 '*'
local pattern2 = "[a-z]*" -- 匹配0或多個小寫字母
print(test_str:match(pattern2)) -- abc

-- 單一字元類別跟 '+'
local pattern3 = "[a-z]+" -- 匹配1或多個小寫字母
print(test_str:match(pattern3)) -- abc

-- 單一字元類別跟 '-'
local pattern4 = "[a-z]-" -- 匹配0或多個小寫字母(盡可能短)
print(test_str:match(pattern4)) -- a

-- 單一字元類別跟 '?'
local pattern5 = "[a-z]?" -- 匹配0或1個小寫字母
print(test_str:match(pattern5)) -- a

-- %n 捕獲物匹配
local pattern6 = "(%d)(%a)%1" -- 匹配數字後跟字母,再跟相同的數字
local match1, match2 = test_str:match(pattern6)
print(match1, match2) -- nil nil (因為沒有符合的模式)

-- %bxy 平衡匹配
local balanced_str = "a(b(c)d)e"
local pattern7 = "%b()" -- 匹配平衡的括號
print(balanced_str:match(pattern7)) -- (b(c)d)

-- %f[set] 邊境模式匹配
local pattern8 = "%f[%d]" -- 匹配位於數字前的空字串
local start_pos = test_str:find(pattern8)
print(start_pos) -- 4 (因為 '1' 是第一個數字)

-- 測試字串 2
local test_str2 = "hello123world"

-- 單一字元類別匹配
local pattern9 = "[a-z]" -- 匹配任意小寫字母
print(test_str2:match(pattern9)) -- h

-- 單一字元類別跟 '*'
local pattern10 = "[a-z]*" -- 匹配0或多個小寫字母
print(test_str2:match(pattern10)) -- hello

-- 單一字元類別跟 '+'
local pattern11 = "[a-z]+" -- 匹配1或多個小寫字母
print(test_str2:match(pattern11)) -- hello

-- 單一字元類別跟 '-'
local pattern12 = "[a-z]-" -- 匹配0或多個小寫字母(盡可能短)
print(test_str2:match(pattern12)) -- h

-- 單一字元類別跟 '?'
local pattern13 = "[a-z]?" -- 匹配0或1個小寫字母
print(test_str2:match(pattern13)) -- h

-- %n 捕獲物匹配
local pattern14 = "(%d)(%a)%1" -- 匹配數字後跟字母,再跟相同的數字
local match3, match4 = test_str2:match(pattern14)
print(match3, match4) -- nil nil (因為沒有符合的模式)

-- %bxy 平衡匹配
local balanced_str2 = "a(b(c)d)e"
local pattern15 = "%b()" -- 匹配平衡的括號
print(balanced_str2:match(pattern15)) -- (b(c)d)

-- %f[set] 邊境模式匹配
local pattern16 = "%f[%d]" -- 匹配位於數字前的空字串
local start_pos2 = test_str2:find(pattern16)
print(start_pos2) -- 6 (因為 '1' 是第一個數字)

:::warning
在 Lua 中,冒號 (:) 是一種語法糖(Syntactic sugar,暫且別管這啥)。

str:match(“%d”) 等價於 string.match(str, “%d”)。冒號語法自動將 str 作為第一個參數傳遞給 string.match 函數。(總之有語法糖就能夠使程式碼變得更加簡潔有力)
:::

:::info
[a-z] 是一個字元類別(character class),表示匹配任意一個從 a 到 z 的小寫字母。這種寫法稱為範圍(range),用來指定一組連續的字元。

也有衍生以下這些寫法:

  • [a-z0-9]:匹配任意一個小寫字母或數字。
  • [aeiou]:匹配任意一個母音字母(a、e、i、o、u)。
    :::

:::info
在 %n 捕獲物匹配的地方,小括號 () 用於捕獲匹配到的內容。這裡的 (%d) 表示捕獲一個數字,並將其儲存為第一個捕獲組。

(%a):這裡的 (%a) 表示捕獲一個字母,並將其儲存為第二個捕獲組。

%1:反向引用(backreference),表示匹配第一個捕獲組的內容。在這個模式中,%1 會匹配之前捕獲的數字。

所以 (%d)(%a)%1 是一個包含捕獲組(capture groups)和反向引用(backreference)的模式。

(%d)(%a)%1 可作以下三點總結:

  • 匹配一個數字,並將其捕獲為第一個捕獲組。
  • 匹配一個字母,並將其捕獲為第二個捕獲組。
  • 最後匹配與第一個捕獲組相同的數字。

而至於都沒匹配到,看一下字串:”hello123world”

第一個匹配到數字 1,但第二個不是字母。所以只可能是 3、w,但是 3、w、o 匹配第三個 o 並不是與 3 同樣的數字(因為反向引用 %1 必須與第一個捕獲組相同數字),所以 nil。
:::

:::info
%bxy 平衡(平衡:指的是成對的符號,包括括號)匹配的部分:

local pattern15 = "%b()":x、y 都是括號,所以 %b() 用於匹配平衡的小括號對。

在字串 "a(b(c)d)e" 中,第一個平衡的括號對是 "(b(c)d)",它從第二個字元開始,到第八個字元結束。需要注意的是,這個匹配包括了括號內的所有內容,包括巢狀的括號對 "(c)"
:::

:::info
%f[set] 邊境模式匹配(frontier pattern)的部分:

%f[set] 會匹配一個位置,這個位置的前一個字元不在指定的集合 set 中,而位置本身的字元在集合中。集合 set 是由一組字元組成的,可以用一系列的字元或者字元類別(如 %d 表示數字,%s 表示空白字符等)來指定。

1
2
3
4
5
6
7
-- 測試字串 2
local test_str2 = "hello123world"

-- %f[set] 邊境模式匹配
local pattern16 = "%f[%d]" -- 匹配位於數字前的空字串
local start_pos2 = test_str2:find(pattern16)
print(start_pos2) -- 6 (因為 '1' 是第一個數字)

在這個範例當中,%f[%d] 用來匹配位於數字前的空字串位置。表示它會尋找一個位置,這個位置的前一個字元不是數字,而位置本身的字元是數字。對於識別字串中數字的開始位置非常有用。

如果 test_str2 是 “Hello 123”,則 %f[%d] 會匹配到 “ “ 和 “1” 之間的位置,因為 “ “ 不是數字,而 “1” 是數字。由於 Lua 的字串索引從 1 開始,”1” 在字符串中的位置是 7,所以 start_pos2 的值會是 7,表示 %f[%d] 匹配到的位置。

講這麼多複雜的還有舉例,總之,%f[set] 的 set 主要是找第一個出現的字元。
:::

總結

:::info
字串表示方式

  • 單引號:’String’
  • 雙引號:”String”
  • 多行字串:[[ 多行字串 ]]
    :::

:::info
取得字串長度

  • string.len(字串):取得字串長度
  • #字串:取得字串長度
  • utf8.len(字串):取得包含中文字的字串長度
    :::

:::info
跳脫字元

  • 使用反斜線 \ 來表示特殊字元,如 \n 表示換行,\” 表示雙引號等。
    :::

:::info
字串方法(Method)

  • string.upper(字串):轉為大寫
  • string.lower(字串):轉為小寫
  • string.gsub(字串, 查找字串, 替換字串, [次數]):替換字串
  • string.find(字串, 子字串, [起始位置], [簡單模式]):查找子字串
  • string.reverse(字串):反轉字串
  • string.format(格式, …):格式化字串
  • string.char(數字):將數字轉為字元
  • string.byte(字串, [位置]):將字元轉為數字
  • string.len(字串):計算字串長度
  • string.rep(字串, 次數):重複字串
  • ..:連接兩個字串
  • string.gmatch(字串, 模式):返回迭代器函數
  • string.match(字串, 模式, [起始位置]):匹配字串
    :::

:::info
字串格式化

  • 使用 string.format() 進行格式化,支持多種格式化規範符號,如 %d、%f、%s 等。
    :::

:::info
字串匹配模式

  • 使用 string.find、string.gmatch、string.gsub、string.match 進行模式匹配。
  • 支援多種字元類別,如 %a(字母)、%d(數字)、%s(空白字元)等。
  • 支援捕獲組和反向引用,如 (%d)(%a)%1。

常用模式條目

  • [a-z]:匹配任意小寫字母
  • [a-z]*:匹配0或多個小寫字母
  • [a-z]+:匹配1或多個小寫字母
  • [a-z]-:匹配0或多個小寫字母(盡可能短)
  • [a-z]?:匹配0或1個小寫字母
  • %b():匹配平衡的括號
  • %f[set]:匹配位於 set 內某個字元之前的空字串
    :::

參考資料

lua-users wiki: Frontier Pattern

Lua pattern matching - Series of same character - Stack Overflow

【30天Lua重拾筆記08】基礎1: 類型 - 字串 | 又LAG隨性筆記

簡單記錄一下 lua 中 string.gmatch 的用法 | Medium

Lua 字符串 | 菜鸟教程