在利用文字探勘技術,完成讓人看完眼睛為之一亮的分析之前,我們要先確保資料集的品質足夠優異,否則只會應驗時常聽到的名言:「garbage in, garbage out」。
若我們以網路媒體文章,或者擷取社群媒體與論壇上的內容,當成文字探勘標的,一定要清理/洗原始資料(data cleaning/cleansing),之後才會開始分析。大家常說資料分析師的工作中,清理資料時間占8成、分析資料時間占2成。
就我的經驗來說,平常若打交道的以數值資料為主,確實是這個比例;但如果是文字資料,恐怕比例會更極端到清資料就要花上9成時間。在清理文字資料的過程中,一定會用上一個神兵利器:「正規表達式」(regular expression,簡稱為 regex 或 regexp)。底下,我們就來介紹正規表達式的用途與用法。
什麼時候會用上正規表達式呢?舉例來說,若想檢查一段英文字串,是否包含英文母音”aeiou”,我們可以利用stringr
裡面的函數str_detect()
確認。
str_detect()
函數裡面,會放入想要比對的字串,對應參數為string
,以及想要比對的字串模式,對應參數為pattern
。
library(tidyverse)
## ── Attaching core tidyverse packages ────────────────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.1.2 ✔ readr 2.1.4
## ✔ forcats 1.0.0 ✔ stringr 1.5.0
## ✔ ggplot2 3.4.2 ✔ tibble 3.2.1
## ✔ lubridate 1.9.2 ✔ tidyr 1.3.0
## ✔ purrr 1.0.1
## ── Conflicts ──────────────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
str_detect(string = c("Sistar", "BTS", "AOA", "BTOB", "Apink"),
pattern = "a")
## [1] TRUE FALSE FALSE FALSE FALSE
str_detect(string = c("Sistar", "BTS", "AOA", "BTOB", "Apink"),
pattern = "e")
## [1] FALSE FALSE FALSE FALSE FALSE
我們可以想像,在pattern
當中,逐一放入a、e、i、o、u、A、E、I、O、U,接著再將結果利用|
(代表「或」)確認,到底哪些韓團名稱之中有母音。
白話一點解釋,就是確認這些名字當中,「有沒有a」、「有沒有e」、…一路往下比對,最後再選出「有a」或「有e」或「有i」…。
這裡面的「有沒有a」就是一種想找的字串模式。不過,比起一個一個比對,利用正規表達式,可以更快完成上述任務。來看下面的案例。
str_detect(string = c("Sistar", "BTS", "AOA", "BTOB", "f(x)", "SS501"),
pattern = "a|e|i|o|u|A|E|I|O|U")
## [1] TRUE FALSE TRUE TRUE FALSE FALSE
str_detect(string = c("Sistar", "BTS", "AOA", "BTOB", "f(x)", "SS501"),
pattern = "[aeiouAIEOU]")
## [1] TRUE FALSE TRUE TRUE FALSE FALSE
無論是"a|e|i|o|u|A|E|I|O|U"
,或者"[aeiouAIEOU]"
,雙引號當中字串與符號的拼接,就是「正規表達式」的具體實例。
套個比喻,正規表達式像是一種搜尋文字的公式。當我們想從文章中找出符合特定模式的一段文字,就可以拿正規表達式,作為找出該模式的搜尋規則。
在上面尋找母音的範例中,|
代表「或」,放在[]
裡面的字母全部都會用來比對,因此兩者可以達成相同效果。其實,在利用str_c()
函數時:
str_c(string = c("Sistar", "BTS", "AOA", "BTOB", "f(x)", "SS501"), pattern = "a")
這當中的"a"
,同樣也是字串模式,正規表達式則是更進一步,利用提前約定好的符號,增添搜尋規則的豐富程度。
這些提前約定好的符號,在英文中被稱作metacharacter,對應中文翻譯有「元字符/元」、「中繼字符/元」、「保留字符/元」、「特殊字符/元」等,我習慣用保留字元稱呼,接下來都會直接使用這個說法。
上面用過的|
、底下會介紹的*
,甚至是括號(
與)
,都屬於保留字元的範疇。
當我們在模式中寫到這些保留字元時,程式能夠立刻知道,我不是想尋找|
這個符號,而是想利用「或」的功能。如果我們真的想在字串中尋找|
符號,則要另外在前面加上兩條反斜線「轉義」(escaping)。
# 想找 S 或 B
str_detect(string = c("SS501", "BTS|BTOB"),
pattern = "S|B")
## [1] TRUE TRUE
# 想找 | 符號
str_detect(string = c("SS501", "BTS|BTOB"),
pattern = "\\|")
## [1] FALSE TRUE
第二個例子中,我們就是用\\
消除|
「或」的意義,讓電腦知道我們不是要利用「或」的功能,而是真的想尋找|
符號是否出現於文中。
不過,你可能會想說,為什麼要用兩條反斜線,才能轉義?
原因在於,當\
加上某些英文字母,本來就有特殊意義,例如\t
代表定位字元(tab)、\n
代表換行,這些結合\
與字母的特殊序列,被稱為character escapes,對應中文為「逃脫字元」。
我們先來看在R語言中使用換行\n
符號時,「印出」結果會是什麼長相。平常我們都會用print()
函數,或者直接呈現某個R語言物件的內容。但在這邊我們會用cat()
函數,差別在哪呢?
利用print()
(還有直接印出字串物件)時,它會保留字串的實際值,因為它是用來輸出R語言物件(data
object)。
print("abc\nabc")
## [1] "abc\nabc"
相對來說,cat()
函數則是用可讀格式(readable format)將幾個字串連接(concatenate)在一起後輸出。
cat("abc\nabc")
## abc
## abc
如果加上\
逃脫,就不會換行了。
cat("abc\\nabc")
## abc\nabc
因為逃脫字元本身就含有一個\
,因此,在R語言當中,想要完成轉義,需要第二個反斜線。
在上面的例子中,我們看到\n
真的讓”abc”與”abc”之間換行;但轉義後\n
失去換行的意義,直接印出。所以前面想要找|
符號,沒有要利用它的「或」功能,就要加上\\
在前面才能處理。後面會列出更多逃脫字元及應用。
稍微總結這個小節的重點:正規表達式是由想搜尋的字元和保留字元拼湊而成的文字搜尋公式,是字串處理必學的重要利器,也是確保文字探勘擁有好品質的重要基礎。
底下我們就從最基礎的正規表達式語法開始介紹。
我們利用str_detect()
函數,還有包含3個韓團在內的字串c("BTOB", "BTS", "CNBLUE")
,用來說明保留字元,掌握它就能發揮正規表達式的一半功力了!
# 想找包含 BT 在內的字串
str_detect(string = c("BTOB", "BTS", "CNBLUE"),
pattern = "BT")
## [1] TRUE TRUE FALSE
我們先來認識正規表達式當中有哪些保留字元吧。
. ^ $ * + ? [ ] { } | \ ( )
這一串的每個符號都是保留字元,也就是說,它們各自擁有特別的功能。
.
能夠配對「換行字元」以外任意字元,一個.
代表一個字元。# 想找包含BT.在內的字串,.可以是任意字元
str_detect(string = c("BTOB", "BTS", "CNBLUE"),
pattern = "BT.")
## [1] TRUE TRUE FALSE
# 想找 BT.B,沒有緊接著B就會配對失敗
str_detect(string = c("BTOB", "BTS", "CNBLUE"),
pattern = "BT.B")
## [1] TRUE FALSE FALSE
# 換行 \n 比對失敗,但 tab 比對成功
str_detect(string = c("BT\nOB", "BT\tS", "CNBLUE"),
pattern = "BT.")
## [1] FALSE TRUE FALSE
^
能夠配對字串起始位置的字元,也就是比對開頭的字元,要放在用來比對的字元前面。# 想找 B 開頭的字串
str_detect(string = c("BTOB", "BTS", "CNBLUE"),
pattern = "^B")
## [1] TRUE TRUE FALSE
$
能夠配對字串結束位置的字元,也就是比對末端的字元,要放在用來比對的字元後面。# 想找 B 開頭的字串
str_detect(string = c("BTOB", "BTS", "CNBLUE"),
pattern = "B$")
## [1] TRUE FALSE FALSE
|
代表「或者」,能比對左邊的規則或右邊的規則;另外&
不是一個保留字元喔,因為如果想找abc
連續3個字母,只要直接列在規則中;如果想找同時包含ab
和yz
,我們會另外用代表「合樣」(lookahead以及lookbehind)概念的方法判斷,因為相對複雜,後面會另闢小節說明。# 想找包含 O 的字串,或者用 E 結尾的字串
str_detect(string = c("BTOB", "BTS", "CNBLUE"),
pattern = "O|E$")
## [1] TRUE FALSE TRUE
[]
代表字元的集合,它可以納入多個字元,比對時會全部考慮進來;裡面還能放進許多程式語言都廣泛採用的表達方式,例如用[0-9]
代表數字、[a-z]
代表小寫英文字母,下個小節會再詳細介紹。# 想找有 A, O, E 在內的字串
str_detect(string = c("BTOB", "BTS", "CNBLUE"),
pattern = "[AOE]")
## [1] TRUE FALSE TRUE
# 想找有大寫英文字母的字串
str_detect(string = c("BTOB", "BTS", "CNBLUE"),
pattern = "[A-Z]")
## [1] TRUE TRUE TRUE
()
讓字元以「組」為單位比對;它還有其他進階用法,後面會另外介紹。# 想找包含 abab 或 acab 在內的字串
str_detect(string = c("ab", "abab", "acab", "abaa"),
pattern = "(ab|ac)(ab)")
## [1] FALSE TRUE TRUE FALSE
{}
用來表示次數或者範圍,可以用來設定它前面規則比對出現次數的下限(至少出現幾次)與上限(最多出現幾次)。在R語言裡面可以只寫下限,但不能只寫上限喔!# 想找包含 s/S 連續出現 1 - 2 次的字串
str_detect(string = c("SS501", "ss501", "SSS", "Apink"),
pattern = "[Ss]{1,2}")
## [1] TRUE TRUE TRUE FALSE
# 想找包含 s/S 連續出現至少 3 次的字串
str_detect(string = c("SS501", "ss501", "SSS", "Apink"),
pattern = "[Ss]{3,}")
## [1] FALSE FALSE TRUE FALSE
?
用來表示它前面的規則出現一次或零次,也就是說,?
前面的字元或字元組是可有可無。# 想找包含 BTOS 或 BTS 在內的字串
# 可以有O(出現1次)也可以沒有O(出現0次)
str_detect(string = c("BTOB", "BTS", "BTSB"),
pattern = "BTO?S")
## [1] FALSE TRUE TRUE
*
用來表示它前面的規則出現多次或零次,這邊的多次包含剛好一次。我們改用str_extract_all()
函數說明,它可以從字串中取出符合規則的一小段字串。
# 想找包含 BT or BTO or BTS在內的字串
# BTOOS 最能說明「出現多次」的意義
str_extract_all(string = c("BTOB", "BTS", "BTSB", "BTC", "BTOOS"),
pattern = "BT[OS]*")
## [[1]]
## [1] "BTO"
##
## [[2]]
## [1] "BTS"
##
## [[3]]
## [1] "BTS"
##
## [[4]]
## [1] "BT"
##
## [[5]]
## [1] "BTOOS"
+
用來表示它前面的規則出現一次或多次,也就是說,+
前面的字元或字元組至少出現一次。# 想找包含 BTO 或 BTS 在內的字串
# BTC 不符合所以沒有取出任何字串
str_extract_all(string = c("BTOB", "BTS", "BTSB", "BTC"),
pattern = "BT[OS]+")
## [[1]]
## [1] "BTO"
##
## [[2]]
## [1] "BTS"
##
## [[3]]
## [1] "BTS"
##
## [[4]]
## character(0)
\
代表轉義,前面提過在R語言中要有兩道斜線才能轉義。# 想找包含 | 或者 包含 . 在內的字串
# 第一個 | 有轉義,第二個 | 沒有因此代表「或」
str_detect(string = c("B|B", "B.", "B", "."),
pattern = "\\||\\.")
## [1] TRUE TRUE FALSE TRUE
整理上面的規則,我們可以簡單分類:
^
和$
?
、*
、+
、{}
|
和[]
.
\
和組別()
將規則結合起來,正規表達式就能發揮很強大的功能!
前面提過,正規表達式中\
能夠「轉義」,替字元跳脫出其字面意義。在R語言裡面,應用正規表達式時使用\
相對麻煩,為什麼呢?
因為,不只正規表達式中的\
具有特殊意義,R語言本來就會使用\
轉義。我們來讀一份文字檔,在讀取之前,先用截圖看長相。
我們可以看到裡面有換行,「音樂」和「時間」之間也有tab。把文字匯入R語言當中,又會長什麼樣子呢?
example <- read_file("data/regex-example.txt")
先直接印出來看,可以看到用\n
代表換行、\t
代表tab。
example
## [1] "現在我有冰淇淋。\n\n我很喜歡冰淇淋,但是,速度與激情九,速度與...速度與激情九,我最~喜歡!\n\n所以現在是音樂\t時間!\n準備,1 2 3"
也可以直接看實際在文件中會呈現的長相。
cat(example)
## 現在我有冰淇淋。
##
## 我很喜歡冰淇淋,但是,速度與激情九,速度與...速度與激情九,我最~喜歡!
##
## 所以現在是音樂 時間!
## 準備,1 2 3
我們先來看R語言中常見的跳脫字元:
我們來看這些符號在文件中會長什麼樣子。
不過,只有特定系統或環境輸出\a
時會發出鈴聲,但我現在用的 MAC 電腦不會,所以底下會跳過。
# 換行字元
cat("你\n好")
## 你
## 好
# 定位字元
cat("你\t好")
## 你 好
下一個是回車字元。回車(carriage return)字元的功能是讓滑鼠游標移到現在這行的開始位置,不會跳到下一行。
# 回車字元
cat("你\r好")
## 你好
執行上面這段程式碼時,會先印出"你"
,接著\r
會回到開頭,再接著印出"好"
。因此輸出結果會是"好"
,因為"好"
蓋過"你"
。
接下來則是退格字元,它會讓游標移動到前一個位置,所以輸出後一個字元時,會蓋過前面的字元,所以"你"
就被覆蓋過去。
# 退格字元
cat("我想說你\b好")
## 我想說你好
上面都是有特殊意義的跳脫字元,我們接著來看其他和R語言中和標點符號有關的例子。
平常要表達字串時,我們會用雙引號""
或單引號''
將字串包在其中。不過,如果想在字串中使用雙引號或單引號,例如Charles' car
或者Jane said, "..."
怎麼辦?
一般來說,我們想在字串中使用單引號,就會在賦值時使用雙引號;同理,想在字串中使用雙引號,就會在賦值時使用單引號。
cat("Charles' car")
## Charles' car
cat('Jane said, "..."')
## Jane said, "..."
但總有些時候,我們不能這麼做,像是Jane said, "Charle's car..."
。
cat("Jane said, "Charles' car..."")
## Error: <text>:1:18: unexpected symbol
## 1: cat("Jane said, "Charles
## ^
上面出現錯誤,因為R語言以為"Charle
的雙引號代表字串結束,沒想到那其實是在引述Jane的話。這時,\
就能派上用場!
cat("Jane said, \"Charles' car...\"")
## Jane said, "Charles' car..."
cat("你\\好")
## 你\好
cat("你\"好")
## 你"好
cat("你\'好")
## 你'好
上面都沒問題,但接下來就會出錯囉!
cat("你"好")
## Error: <text>:1:8: unexpected symbol
## 1: cat("你"好
## ^
因為沒有使用轉義,電腦以為到你就結束了,沒想到後面還有一個好,這樣看起來覺得缺了一個雙引號,因此回報錯誤。
這樣是不是有比較理解R語言中的逃脫字元了呢?
最後,我們再來釐清一件事情。在上面的例子中,我們只要使用\'
就能讓'
逃脫掉它原先在R語言中的功能,也就是賦值之用。
事實上,無論是'
或者"
,都是R語言中的基本函數!你可以在R Console裡輸入?base::Quotes
,R Studio右下角的Help頁面,就會跳出介紹。
這些是在R語言當中的轉義。但是,在正規表達式中,同樣會使用\
轉義。
舉例來說,前面我們提過|
代表「或」,所以如果我想找字串中有沒有|
符號,不能直接用|
符號,要不然電腦會以為我們要利用「或」功能,得在前面加上\
才行。
但是這樣還不夠!因為就像上面剛提到的,R語言中\
本身就有轉義的意思,所以我們要把這個代表轉義的\
再轉義掉。如果我想找|
符號,就得在前面加上兩次\
,變成\\|
,才能讓R語言知道,原來我就是要找`|。
這樣的說明,有沒有比較清楚R語言中的轉義,以及正規表達式的轉義了呢?
介紹[]
時有提到,在包含R語言在內的眾多程式語言之中,可以利用類似[a-z]
的表示方式,比對特定字元的集合。"[a-z]"
代表「從A到Z的所有大寫英文字母」,它一定比在[]
列出26個字母省時。
這個寫法被稱為「字元集」(character
set/class)。雖然[]
屬於保留字元的一部分,但因為延伸應用很多所以特別介紹。常見的字元集有這些:
# 想找包含 a 或任意數字在內的字串
str_detect(string = c("SS501", "ss501", "apink", "Apink"),
pattern = "a|[0-9]")
## [1] TRUE TRUE TRUE FALSE
# 跟上面意義相同
str_detect(string = c("SS501", "ss501", "apink", "Apink"),
pattern = "a|[:digit:]")
## [1] TRUE TRUE TRUE FALSE
# 想找包含空格或定位字元在內的字串
str_detect(string = c(" ", "\t", "a", "\f"),
pattern = "[:blank:]")
## [1] TRUE TRUE FALSE FALSE
# 跟上面意義相同
str_detect(string = c(" ", "\t", "a", "\f"),
pattern = "[ \t]")
## [1] TRUE TRUE FALSE FALSE
前面學過^
可以用來比對字串開頭,但如果放在[]
裡面,它就變成取否定,也就是「不要…」的意思。
# 想找「包含數字」以外字元的字串,代表數字以外都算
str_detect(string = c("SS501", "501", "apink", "Apink"),
pattern = "[^0-9]")
## [1] TRUE FALSE TRUE TRUE
# 想找「包含數字、大寫英文字母」以外字元的字串
str_detect(string = c("SS501", "501", "apink", "Apink"),
pattern = "[^0-9A-Z]")
## [1] FALSE FALSE TRUE TRUE
# 想找「包含數字、大寫A-R」以外字元的字串
str_detect(string = c("SS501", "501", "apink", "Apink"),
pattern = "[^0-9A-R]")
## [1] TRUE FALSE TRUE TRUE
有了[]
這個武器後,我們後續可以結合其他特殊字元,提升正規表達式的豐富程度。
除了[]
可以表示字元集,還有這幾個正規表達式中的逃脫字元,也有類似用法:\d, \D, \s, \S, \w, \W
。
\d
的意義和[0-9]
相同\D
取\d
的反義,和[^0-9]
相同\w
能比對任意字母、數字或底線(undescroe),和[a-zA-Z0-9_]
相同\W
取\w
的反義,和[^a-zA-Z0-9_]
相同\s
能比對任意空白字元,和[:space:]
相同\S
取\s
的反義,和[^[:space:]]
相同str_detect(c("0", "\f", "\n", " "), "[:space:]")
## [1] FALSE TRUE TRUE TRUE
str_detect(c("0", "\f", "\n", " "), "[^[:space:]]")
## [1] TRUE FALSE FALSE FALSE