正規表達式是一種非常靈活的字串「模式比對」方式,它使用一套專為此目的設計的領域特定語言(DSL)。
就像其他幾種程式語言一樣,Julia 並不打算自己實作正規表達式函式庫。 它改為包裝廣受使用的 PCRE2 函式庫,因此提供了與(例如)Gleam 完全相同、並與 JavaScript 非常相似的正規表達式語法。
這份 Julia 課程大綱假設你已經熟悉基本的正規表達式語法。 我們只會聚焦在 Julia 特有的功能上。
以下列出一些能幫你複習正規表達式知識的資源。
Julia 的正規表達式介面在手冊中有說明。
Julia 的正規表達式就只是一個字串,只是在開頭的"前面多了r。
所有基本功能都屬於標準函式庫的一部分。
事實上,Strings概念中介紹過的許多函式,本身就設計用來進行正規表達式搜尋,例如occursin()。
julia> re = r"test$"
r"test$"
julia> typeof(re)
Regex
# Does a string end with "test"?
julia> occursin(re, "this is a test")
true
julia> occursin(re, "these are tests")
false
修飾字元可以接在結尾引號的後面,例如i代表忽略大小寫的比對。
julia> occursin(r"test", "Testing")
false
julia> occursin(r"test"i, "Testing")
true
我們通常想知道究竟是「什麼」符合。做法是在正規表達式裡用括號加入擷取群組,再使用match()函式。
julia> m = match(r"(\d+g) .* (\d+ml)", "dissolve 25g sugar in 200ml water")
RegexMatch("25g sugar in 200ml", 1="25g", 2="200ml")
julia> m.captures
2-element Vector{Union{Nothing, SubString{String}}}:
"25g"
"200ml"
# how many matches?
julia> length(m.captures)
2
# what matched?
julia> m[1], m[2]
("25g", "200ml")
# Starting positions of the matches (character index)
julia> m.offsets
2-element Vector{Int64}:
10
23
當然,比對也可能失敗。
這時結果會是特殊值Nothing,而不是RegexMatch,所以要準備好檢查這種情況。
# failed match
m = match(r"(not here)", "dissolve 25g sugar in 200ml water")
julia> typeof(m)
Nothing
julia> isnothing(m)
true
雖然match預設從字串開頭開始比對,我們也可以指定位移量n,忽略前n個字元。
# capture first match
julia> m = match(r"(\wat)", "cat, sat, mat")
RegexMatch("cat", 1="cat")
# ignore first 5 characters, then match
julia> m = match(r"(\wat)", "cat, sat, mat", 5)
RegexMatch("sat", 1="sat")
在 Julia 中,match()只會找出目標字串裡的「第一個」比對:不像某些其他語言那樣有全域修飾字元。
取而代之的是eachmatch(),它會回傳一個比對的迭代器。
它是惰性求值的,所以你可能需要將它轉成你想要的格式。
julia> matches = eachmatch(r"(\wat)", "cat, sat, mat")
Base.RegexMatchIterator{String}(r"(\wat)", "cat, sat, mat", false)
# convert to vector
julia> collect(matches)
3-element Vector{RegexMatch}:
RegexMatch("cat", 1="cat")
RegexMatch("sat", 1="sat")
RegexMatch("mat", 1="mat")
# convert with comprehension
julia> [m.match for m in matches]
3-element Vector{SubString{String}}:
"cat"
"sat"
"mat"
# broadcast an anonymous function
julia> (m -> m.match).(matches)
3-element Vector{SubString{String}}:
"cat"
"sat"
"mat"
使用正規表達式的常見原因之一,就是把比對到的內容換成另一個字串。
replace()函式在Strings概念中已經討論過,當時是用字串常值來搜尋。
同一個函式也能發揮正規表達式比對的全部威力。
julia> replace("some string", r"[aeiou]" => "*")
"s*m* str*ng"
julia> replace("first second", r"(\w+) (?<agroup>\w+)" => s"\g<agroup> \1")
"second first"
上面的第二個例子展示了編號擷取群組和具名擷取群組都能用在取代內容中,而且是在s" "字串裡。
更多細節請見手冊:這個主題總是逼著大多數程式設計師一再回頭翻文件!
你受雇於一間正在開發聊天機器人的公司,擔任正規表達式專家。
目前它還處於非常基礎的開發階段,因此你的任務是運用正規表達式,提升聊天機器人理解與生成自然語言的能力。
為了學習的目的,請使用正規表達式來解這些任務。
我們知道有時確實有其他可行的做法(甚至可能更有效率!)
聊天機器人不只聰明,還是個忠誠的助手。
使用者想向聊天機器人提問時,必須把「Chatbot」這個字放在指令的第一個位置。
這個關鍵字是大寫還是小寫都無所謂,重點在於它出現的位置。
實作is_valid_command()函式,幫助聊天機器人辨認使用者是否在下指令。
julia> is_valid_command("Chatbot, play a song from the 80's.")
True
julia> is_valid_command("Hey Chatbot, where is the closest pharmacy?")
False
julia> is_valid_command("CHATBOT, do you have a solution for this challenge?")
True
聊天機器人很難理解人類如何用表情符號表達情緒。
當聊天機器人收到使用者的訊息時,每個表情符號都會以「emoji」加上它的 id 編號來表示。
實作remove_emoji()方法,讓它接收一個字串,並移除整則訊息中所有表情符號的加密內容。
不含表情符號加密文字的行,應該原樣回傳。
只要移除表情符號的字串就好,不要試著調整空白。
julia> remove_emoji("I love playing videogames emoji3465 it's one of my hobbies")
"I love playing videogames it's one of my hobbies"
由於聊天機器人的功能可以下載到行動應用程式上,使用者可能會在對話中輸入電話號碼。
問題在於,聊天機器人只能讀取並驗證特定格式的號碼。
如果號碼有效(符合正規表達式所指定的字元序列),聊天機器人會回覆一則訊息,感謝使用者並確認號碼。如果號碼無效,函式會告知使用者這個電話號碼無效。
預期的格式是:(+##) ###-###-###
julia> check_phone_number("(+34) 659-771-594")
"Thanks! You can now download me to your phone."
julia> check_phone_number("659-771-594")
"Oops, it seems like I can't reach out to 659-771-594"
聊天機器人是一套非常好奇的軟體。雖然它可以在網路上搜尋特定主題,但它喜歡問使用者有沒有什麼不錯的網站或 URL,好去找相關資訊。
對話範例:
實作getURL()函式,它要能回傳一個只包含每個網站連結的陣列。
julia> getURL("I learned a lot from exercism.org")
["exercism.org"]
為了儲存所有曾與它對話過的使用者資料,聊天機器人可以從使用者的個人檔案取得全名,格式像這樣:「smith, john」。
這樣一來,我們希望聊天機器人非常有禮貌,並留下好印象。
撰寫nice_to_meet_you()函式,它接收一個包含使用者全名的字串,並回傳字串**「Nice to meet you, John Smith」**
julia> str = "Smith, John"
julia> nice_to_meet_you(str)
"Nice to meet you, John Smith"