iT邦幫忙

2023 iThome 鐵人賽

DAY 4
0
AI & Data

用R語言玩轉文字探勘系列 第 4

[Day 4] R語言與正規表達式: 基本概念

  • 分享至 

  • xImage
  •  

正規表達式

在利用文字探勘技術,完成讓人看完眼睛為之一亮的分析之前,我們要先確保資料集的品質足夠優異,否則只會應驗時常聽到的名言:「garbage in, garbage out」。

若我們以網路媒體文章,或者擷取社群媒體與論壇上的內容,當成文字探勘標的,一定要清理/洗原始資料(data cleaning/cleansing),之後才會開始分析。大家常說資料分析師的工作中,清理資料時間占8成、分析資料時間占2成。

就我的經驗來說,平常若打交道的以數值資料為主,確實是這個比例;但如果是文字資料,恐怕比例會更極端到清資料就要花上9成時間。在清理文字資料的過程中,一定會用上一個神兵利器:「正規表達式」(regular expression,簡稱為 regex 或 regexp)。底下,我們就來介紹正規表達式的用途與用法。

正規表達式介紹 - 利用R語言

什麼時候會用上正規表達式呢?舉例來說,若想檢查一段英文字串,是否包含英文母音”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失去換行的意義,直接印出。所以前面想要找|符號,沒有要利用它的「或」功能,就要加上\\在前面才能處理。後面會列出更多逃脫字元及應用。

稍微總結這個小節的重點:正規表達式是由想搜尋的字元和保留字元拼湊而成的文字搜尋公式,是字串處理必學的重要利器,也是確保文字探勘擁有好品質的重要基礎。

底下我們就從最基礎的正規表達式語法開始介紹。

正規表達式基礎 - 利用R語言

保留字元

我們利用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個字母,只要直接列在規則中;如果想找同時包含abyz,我們會另外用代表「合樣」(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

上一篇
[Day 3] 使用R語言的文字探勘框架 - quanteda
下一篇
[Day 5] R語言與正規表達式: 進階語法和實例
系列文
用R語言玩轉文字探勘30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言