文字探勘的諸多應用如情緒分析、文本分類,聽起來都很美好,但在分析資料以前,首先要有乾淨資料。舉例來說,若我們想分析歷屆台灣總統的演講稿,在事前我們可能要先做這些準備:保留講稿中「台灣」和「臺灣」其中一種用法、刪除原文中的換行符號、整理民國與西元日期格式、消除空格例如以前可能會出現挪抬等。
上述提到的小任務,全部都是字串處理的範疇。當然,你可能會想說,你手上的資料已經非常乾淨,根本沒必要走過這些步驟!其實,就算不是文字探勘、就算資料已經足夠乾淨,但字串處理的使用情境非常生活化,它仍然能夠在意想不到的地方幫上你。
在實際開始寫程式之前,我們先來看幾個日常任務:
&
函數,這個函數的意義就是字串連接(string concatenation)除了這些實例,還有更多看似不起眼、卻在工作中反覆出現的小任務,例如英文大小寫轉換、計算特定字串出現次數、切割文章與文字等,都屬於字串處理。而這些任務,全都可以交給stringr
套件解決。
在stringr官方介紹頁面中就提到,「在R語言裡,字串可能不那麼引人注目或者高調,但在許多資料清理和預處理的任務中,它們確實扮演了很大的角色,」同時,它將stringr
稱為用於常見字串操作,簡單且具有一致性的(consistent)「包裝」(wrapper)。為什麼說它是一種具有一致性的包裝呢?下方介紹說分明。
和dplyr
、tidyr
、tidytext
等套件一樣,stringr
同屬tidyverse
生態系,它不只功能多元,幾乎可以將字串處理任務一網打盡,更關鍵的是,stringr
在設計時就處理掉一個使用者的重要痛點:「函數名稱不好背。」
打開stringr
的官方說明頁面,你可以很快發現,它的函數幾乎都是用str_()
開頭,這也就是前面提的一致性。不管是哪種字串處理任務,利用邏輯統一的命名方式,包裝每個套件中的函數。
如果你曾接觸過base R就知道,R語言原生的字串處理函數非常不好記。舉例來說,base R有兩個函數grep()
和grepl()
,前者可以比對出符合特定模式(pattern)的字串,並告訴你是第幾個,後者則會給予TRUE
與FALSE
的回饋。
string <- c("text mining","mine")
grep(pattern = "mine", x = string)
## [1] 2
grepl(pattern = "mine", x = string)
## [1] FALSE TRUE
在stringr
套件中,就只有一個簡單的str_detect()
要記。
string <- c("text mining","mine")
str_detect(string, pattern = "mine")
## [1] FALSE TRUE
再舉一例,sub()
和gsub()
可以替換(replace)字串,前者只能替換字串中第一次出現者,後者則可以全數替換。
string2 <- c("text","text and text")
sub(pattern = "text", replacement = "data", x = string2)
## [1] "data" "data and text"
gsub(pattern = "text", replacement = "data", x = string2)
## [1] "data" "data and data"
只是,在記函數名稱的時候,常常會忘記到底要用sub()
還是gsub()
,人腦記憶體就消耗在這裡。在stringr
裡面,有效果相同的函數,但命名邏輯更加一致,且名稱又很直觀,它們分別是str_replace()
和str_replace_all()
。
string2 <- c("text","text and text")
str_replace(string = string2, pattern = "text", replacement = "data")
## [1] "data" "data and text"
str_replace_all(string = string2, pattern = "text", replacement = "data")
## [1] "data" "data and data"
不只是命名而已,我們也會發現,stringr
函數的參數(argument)順序永遠都一樣,先是想要處理的字串,接著則是想要比對的模式,接續才會是其他會用到的參數。stringr
就是靠著函數取名邏輯一致、名稱直觀好記、參數清晰、功能多樣,成功擄獲使用者芳心。
底下我們整理出stringr
中常用函數,以及它們能夠完成的任務。
我們來看stringr
當中有些什麼函數吧:
其中,有_all()
後綴的函數,使用時就會返回多個結果,來看以下實際例子:
str_replace(c("BTOB", "BTS", "BACKSTREET BOYS"), "B", "A")
## [1] "ATOB" "ATS" "AACKSTREET BOYS"
str_replace_all(c("BTOB", "BTS", "BACKSTREET BOYS"), "B", "A")
## [1] "ATOA" "ATS" "AACKSTREET AOYS"
此外,使用stringr
函數時還有一個重點:注意函數輸出的資料結構為何。以str_extract()
來說,因為只會擷取第一個符合模式的字串,因此都只會返回一個字串,下方例子中輸出格式為向量(vector);str_extract_all()
則會返回所有字串,因此輸出格式就會是列表(list)。
在以資料框(dataframe)為主的環境下處理資料時,若是在mutate()
使用相關函數時,就要注意函數運用結果,會不會讓欄位從單純的字串變成列表,這樣就會影響接下來dplyr
動詞還有其他tidyverse
函數的運用。
library(tidyverse)
tibble(name = c("BTOB", "BTS", "BACKSTREET BOYS")) %>%
mutate(extract = str_extract(name, "B.*?S"))
## # A tibble: 3 × 2
## name extract
## <chr> <chr>
## 1 BTOB <NA>
## 2 BTS BTS
## 3 BACKSTREET BOYS BACKS
tibble(name = c("BTOB", "BTS", "BACKSTREET BOYS")) %>%
mutate(extract = str_extract_all(name, "B.*?S"))
## # A tibble: 3 × 2
## name extract
## <chr> <list>
## 1 BTOB <chr [0]>
## 2 BTS <chr [1]>
## 3 BACKSTREET BOYS <chr [2]>
從這個例子中,就能看出str_extract()
產出的欄位為字串,str_extract_all()
產出的欄位為列表欄位。使用str_split()
時,也會遇到相似情況。
str_split(c("BTOB,BTS", "Apink,April,AOA"), ",")
## [[1]]
## [1] "BTOB" "BTS"
##
## [[2]]
## [1] "Apink" "April" "AOA"
tibble(name = c("BTOB,BTS", "Apink,April,AOA")) %>%
mutate(name2 = strsplit(name, ","))
## # A tibble: 2 × 2
## name name2
## <chr> <list>
## 1 BTOB,BTS <chr [2]>
## 2 Apink,April,AOA <chr [3]>
利用上述這些stringr()
中的函數,就可以解決非常多字串處理的問題了!