字串就是一段夾在引號之間的文字。
mystring <- "some text"
mystring
#> [1] "some text"
typeof(mystring)
#> [1] "character"
可惜的是,對熟悉其他語言的程式設計師來說,這些術語可能會讓人混淆。
雖然type是character,R 並不會區分單一字母(在其他幾種語言裡稱為char,但_在 R 裡不是_)和很長的字串。
兩者都是單一項目,而且可以任選單引號' '或雙引號" "來包住。
風格指南建議使用雙引號,除非你想在字串裡使用雙引號而不將它們跳脫。
str2 <- 'String with "quotes"' # convenience format
str2
#> [1] "String with \"quotes\"" # standard format
因此,character vector就是由字串構成的向量,其中每個字串都是單一元素。
s <- c("some", "strings")
s
#> [1] "some" "strings"
length(s)
#> [1] 2
length(s[1]) # the number of vector elements, not the string length
#> [1] 1
nchar(s[1]) # the number of characters in the string "some"
#> [1] 4
在上面的例子裡,length(s[1])等同於length("some"):它算的是_向量元素_的個數,而不是_字串長度_。
操作單一字串顯然是可行的,就像上面例子中的nchar()一樣,但需要用特定的函式,這些會在下面討論。
如上面所見,某些標點字元需要「跳脫」,也就是在前面加上反斜線\。
對大多數有經驗的程式設計師來說,這份清單並不令人意外,因為這個主題在各語言之間已經相當標準化了。
除了引號(如上所示),這份清單還包括換行\n、定位字元\t、反斜線\\,以及以\U或\u開頭的 Unicode 符號。
為了避免使用大量跳脫字元,可以在r"( )"裡定義原始字串:
r"(raw "string")"
#> [1] "raw \"string\""
對於包含括號( )的原始字串,還有許多其他語法選項。
任何目前仍受支援的 R 版本都會以 UTF-8 編碼字串,因此世界上大多數語言都能支援。
"आधुनिक मानक हिन्दी"
#> [1] "आधुनिक मानक हिन्दी"
此外,許多 R 函式會考量 locale,並嘗試遵循當地的慣例來處理字串排序、大小寫轉換、日期與時間等等。
本課程大綱的範例主要以美式英文為主:一來這是常見的預設值,二來課程大綱的作者剛好都住在美國。
地區設定是個複雜的主題,請自行搜尋適合你自身情況的相關文件。
在基礎 R 裡,大多數事情都_做得到_,但許多程式設計師覺得字串操作的支援相當有限,而且令人困惑。
幸好,tidyverse套件直覺得多、彈性得多,也更能自我一致。
還是有很多沿用舊式字串函式的既有程式碼。 Exercism 上大多數的社群解答也包括在內。
此外,處理小型任務時,使用內建函式往往比匯入龐大的函式庫來得實用。 R 軌道的維護者在撰寫範例解答、用來驗證我們 GitHub 儲存庫 上的練習時,傾向採用這種做法。
因此,對這些舊做法有些認識仍然值得學習。
下面的函式都以最簡單的形式呈現,但大多也有選用引數。 詳情請見文件。
nchar回傳字串中的字元數。
nchar("string")
#> [1] 6
nchar("Hrōðgār") # a person in Beowulf, written in Old English
#> [1] 7
「字元」究竟是什麼意思,這個(很複雜!)主題超出了我們的範圍。 只要注意上面例子裡的幾個重點:
\0結尾(那樣會讓長度多 1)。要注意傳給nchar的內容是什麼。
對於非character的值,結果仍可能難以預測,不過這在近期的 R 版本中已經有所改善。
grep在字串向量裡搜尋某個模式,回傳有相符項目的索引。
grep("Jav", c("Java", "Python", "Javascript"))
#> [1] 1 3
paste 與 paste0
把任意數量的字串合併成單一字串。
paste可以選擇指定分隔符號:預設是空格。
paste0(v)等於paste(v, sep = "")。
paste("bits", "of", "a", "string")
#> [1] "bits of a string"
paste0("bits", "of", "a", "string")
#> [1] "bitsofastring"
sprintf若想更精細地控制字串的組合方式,R 從 C 複製了sprintf函式(後來也有好幾種語言跟進:維基百科列出了大約 30 種)。
r <- 5.3
pi <- 3.14159
sprintf("A circle of radius %.1f has area %.2f", r, pi * r^2)
#> [1] "A circle of radius 5.3 has area 88.25"
trimws開頭和/或結尾的空白可以用trimws()移除:
s <- " messy string "
trimws(s)
#> [1] "messy string"
有個which = "left"(或"right")參數,可以只修剪其中一端。
substr作用於單一字串,回傳兩個(含)界限之間的子字串。
substr("Exercism", 5, 7)
#> [1] "cis"
strsplit依分隔字串把一個字串分割成 R 的陣列。 分隔字串是必填的(沒有預設值),但可以是空字串。
strsplit("R, Python, Julia", ", ")
[[1]]
#> [1] "R" "Python" "Julia"
strsplit("Exercism", "")
[[1]]
#> [1] "E" "x" "e" "r" "c" "i" "s" "m"
陣列會在另一篇概念裡介紹。
如果你還沒讀到課程大綱的那個部分,現在只要知道 unlist() 函式(在這個情況下)能把陣列轉成向量就好。
unlist(strsplit("Exercism", ""))
#> [1] "E" "x" "e" "r" "c" "i" "s" "m"
regexpr 與 regexpr
是「regular expression」的縮寫,這些函式會在字串裡搜尋某個模式出現的位置,回傳一份(可能)有用的資料陣列。 這裡提到它們只是為了完整起見:還有更好的選擇!
regexpr只回傳第一次出現的位置,gregexpr回傳所有出現的位置("g"代表 global 搜尋)。
正如名稱所示,模式可以是正規表示式,這會在後續的概念中介紹。
為什麼這些舊的字串函式看起來像 Unix shell 指令?
雖然 R 誕生於 1993 年,它的設計卻大致上要能與 S 語言向後相容。
S 語言是在貝爾實驗室設計的,於 1976 年首次問世。 貝爾實驗室也打造了 Unix(從 1969 年開始)和 C 語言(1972 年)。
真是一支來自那個久遠年代的優秀團隊! 我們只能假設他們彼此交談、交換過想法。
如果可以寬容一點,我們會說舊式的字串函式並不是 R 能提供的最好選擇(也有人抱持比較不寬容的看法)。
為了改善這個情況,stringr 函式庫從 2009 年就開始開發,現在是 Tidyverse 系列的核心成員之一。
用library(stringr)或library(tidyverse)將它匯入你的命名空間。
一些主要特色包括:
str_前綴,並盡量讓用途一目了然。和所有 Tidyverse 套件一樣,stringr有良好的文件,通常由套件作者親自撰寫。
stringr裡有數十個函式,而且它們常常有各式各樣的選用引數。
一開始可能會讓人有點吃不消,所以文件是你的好朋友。
想想你要輸入什麼(單一字串?向量?還是更複雜的東西?),以及想得到什麼輸出。
只要找到正確的函式和正確的引數,你想做的事大多都有簡單的做法。
使用管線|>來連接函式的做法,會在課程大綱的其他部分更詳細地討論。
目前,知道以下幾點就很實用:
下面的例子示範了這個語法。 個別函式會在稍後介紹。
library(stringr)
"Monday-25-February" |>
str_split("-") |>
unlist() |> # [1] "Monday" "25" "February"
str_sub(1, 3) |> # [1] "Mon" "25" "Feb"
str_to_upper() |> # [1] "MON" "25" "FEB"
str_flatten(collapse = ", ")
#> [1] "MON, 25, FEB"
str_length 計算碼點(類似nchars)。
其他各種函式則處理空白,包括 str_trim() 和 str_squish()。
在任何資料科學專案一開始,這往往是至關重要的清理工作。
# The first 3 kernels in JuPyteR notebooks:
str_length(c("Julia", "Python", "R")) #> [1] 1 6 5
#> [1] 5 6 1
str_pad(c("Julia", "Python", "R"), 8, "right") # add spaces if necessary
#> [1] "Julia " "Python " "R "
str_trim("Julia Python R ") # remove leading & trailing whitespace
#> [1] "Julia Python R"
str_squish("Julia Python R ") # trim, collapse multiple spaces
#> [1] "Julia Python R"
函式 str_sub(string, start, end) 會回傳從start到end(含)的子字串,而 str_sub_all() 則可以處理多個字串。
彈性很大。
start和end預設分別是第一個和最後一個字元。
它們可以是索引向量。
和向量索引不同的是(這點很討喜),負值會從字串尾端往回算,跟 Python 一樣。
s <- "abcdefgh"
str_sub(s, 2, 4)
#> [1] "bcd"
str_sub(s, 5)
#> [1] "efgh"
str_sub(s, -3, -2)
#> [1] "fg"
str_sub(s, c(1, 4), c(2, 5))
#> [1] "ab" "de"
str_sub(s, c(1, 4, 6), c(2, 5, -1))
#> [1] "ab" "de" "fgh"
s |> str_sub(2, 4) # these functions are designed for pipes
#> [1] "bcd"
# str_sub_all takes vector input, gives list output
str_sub_all(c(s, "ijklmnop"), c(1, 4, 6), c(2, 5, -1))
[[1]]
#> [1] "ab" "de" "fgh"
[[2]]
#> [1] "ij" "lm" "nop"
str_flatten 會把字元向量轉換成單一字串。
collapse引數會插入每個元素之間,預設是""。
c("R", "Julia", "Python") |> str_flatten()
#> [1] "RJuliaPython"
c("R", "Julia", "Python") |> str_flatten(collapse = " - ")
#> [1] "R - Julia - Python"
str_c 會把多個字元向量轉換成單一字元向量,必要時會循環使用元素。
str_c(LETTERS[1:8], 1:8, sep = ":")
#> [1] "A:1" "B:2" "C:3" "D:4" "E:5" "F:6" "G:7" "H:8"
str_glue 為 R 帶來簡單的字串插值。
大括號{ }裡的運算式會被求值,並以文字表示取代。
如果提供多個運算式字串,它們也會像str_flatten一樣串接起來。
字串也可以選擇修剪開頭/結尾的空白。
r <- 5.3
pi <- 3.14159
str_glue("A circle of radius {r} has area {pi * r^2}")
A circle of radius 5.3 has area 88.2472631
雖然str_glue很有彈性(也相當複雜),但底層的 glue 函式庫提供了更廣泛的可能性。
R 很常見的一種用途,是把雜亂的資料整理成一致的格式以便分析。 為了幫忙做到這點,有幾種把文字分割成子字串的方法。
str_split_1 最簡單,它把單一字串依指定的模式(沒有預設模式)分割成一個向量。
s <- "R Julia Python"
str_split_1(s, " ")
#> [1] "R" "Julia" "Python"
str_split_i 接受一個字元向量和一個索引n,回傳每個輸入字串第n個子字串所成的向量。
s2 <- c(s, "C Kotlin, F#") #> [1] "R Julia Python" "C Kotlin, F#"
str_split_i(s2, " ", 2) # 2nd element of each
#> [1] "Julia" "Kotlin,"
str_split 接受一個字元向量,回傳一個由向量構成的陣列。
我們的課程大綱裡有一篇陣列概念,裡頭說明了輸出的形式。
str_split(s2, " ")
[[1]]
#> [1] "R" "Julia" "Python"
[[2]]
#> [1] "C" "Kotlin," "F#"
還有 str_split_fixed,它回傳一個矩陣。
矩陣會在之後的概念裡介紹。
要比對整個字串時,str_equal() 可以使用各種「相等」的定義。
最簡單的情況下,預設區分大小寫,但可以更改。
地區設定和 Unicode 會帶來更多可能性。
str_equal("Odin", "odin")
#> [1] FALSE
str_equal("Odin", "odin", ignore_case = TRUE)
#> [1] TRUE
有些語言(主要是歐洲語言)會區分大寫和小寫字母。
stringr有一組函式可以在兩者之間轉換,並依地區設定採用合適的方式:
str_to_lower()str_to_upper()str_to_title():把每個字的第一個字母轉成大寫str_to_sentence():把句子的第一個字母轉成大寫str_to_upper("elixir") # defaults to locale = "en"
#> [1] "ELIXIR"
str_to_upper("ελληνικά") # infers Greek
#> [1] "ΕΛΛΗΝΙΚΆ"
str_to_title("the cat sat on the mat")
#> [1] "The Cat Sat On The Mat"
str_to_sentence("the cat sat on the mat")
#> [1] "The cat sat on the mat"
要依模式比對來替換子字串,str_replace() 會改掉第一次出現的模式,str_replace_all() 則會改掉所有出現的位置。
要依字元索引來替換子字串,str_sub() 有一種賦值形式。
s <- "abcde"
str_sub(s, 3, 4) <- "yz"
s
#> [1] "abyze"
str_dup 會產生重複項,str_unique 則會移除重複項。
str_dup預設沒有分隔符號,但可以指定一個。
str_dup("xyz", 4, sep = ", ")
#> [1] "xyz, xyz, xyz, xyz"
str_unique是 unique() 的延伸。
兩者都會移除向量裡的重複項目,但stringr的函式還能處理大小寫區分和各種依地區設定而定的比對。
inp <- c("some", "strings", "Some", "Strings")
str_unique(inp)
#> [1] "some" "strings" "Some" "Strings"
str_unique(inp, ignore_case = TRUE)
#> [1] "some" "strings"