字串

字串 在 R

2 個練習

關於 字串

背景

字串就是一段夾在引號之間的文字。

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套件直覺得多、彈性得多,也更能自我一致。

基礎 R

還是有很多沿用舊式字串函式的既有程式碼。 Exercism 上大多數的社群解答也包括在內。

此外,處理小型任務時,使用內建函式往往比匯入龐大的函式庫來得實用。 R 軌道的維護者在撰寫範例解答、用來驗證我們 GitHub 儲存庫 上的練習時,傾向採用這種做法。

因此,對這些舊做法有些認識仍然值得學習。

下面的函式都以最簡單的形式呈現,但大多也有選用引數。 詳情請見文件。

nchar

回傳字串中的字元數。

nchar("string")
#> [1] 6

nchar("Hrōðgār") # a person in Beowulf, written in Old English
#> [1] 7

「字元」究竟是什麼意思,這個(很複雜!)主題超出了我們的範圍。 只要注意上面例子裡的幾個重點:

  • 雖然內部是用 C 實作的,R 的字串並不是以\0結尾(那樣會讓長度多 1)。
  • 多位元組的 Unicode 字元會如你所預期(或所希望)地處理。「Hrōðgār」有 7 個字元(也稱作 runes 或碼點),但儲存時需要 10 個位元組。

要注意傳給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 搜尋)。

正如名稱所示,模式可以是正規表示式,這會在後續的概念中介紹。

Advanced

為什麼這些舊的字串函式看起來像 Unix shell 指令?

雖然 R 誕生於 1993 年,它的設計卻大致上要能與 S 語言向後相容。

S 語言是在貝爾實驗室設計的,於 1976 年首次問世。 貝爾實驗室也打造了 Unix(從 1969 年開始)和 C 語言(1972 年)。

真是一支來自那個久遠年代的優秀團隊! 我們只能假設他們彼此交談、交換過想法。

Tidyverse

如果可以寬容一點,我們會說舊式的字串函式並不是 R 能提供的最好選擇(也有人抱持比較不寬容的看法)。

為了改善這個情況,stringr 函式庫從 2009 年就開始開發,現在是 Tidyverse 系列的核心成員之一。

用library(stringr)或library(tidyverse)將它匯入你的命名空間。

一些主要特色包括:

  • 所有函式名稱都有str_前綴,並盡量讓用途一目了然。
  • 引數的順序盡量保持一致,把輸入資料放在第一個引數。
  • 輸入可以從單一字串到複雜的資料結構。函式會盡量配合,做出最合理的處理。

和所有 Tidyverse 套件一樣,stringr有良好的文件,通常由套件作者親自撰寫。

stringr裡有數十個函式,而且它們常常有各式各樣的選用引數。 一開始可能會讓人有點吃不消,所以文件是你的好朋友。

想想你要輸入什麼(單一字串?向量?還是更複雜的東西?),以及想得到什麼輸出。

只要找到正確的函式和正確的引數,你想做的事大多都有簡單的做法。

岔題:管線快速入門

使用管線|>來連接函式的做法,會在課程大綱的其他部分更詳細地討論。

目前,知道以下幾點就很實用:

  • 管線左邊的結果會成為右邊函式的第一個引數(這裡的「右邊」不計選用的換行)。
  • 其餘的引數可以一併寫在函式呼叫裡。
  • 在 R 裡,我們一定要把管線接向函式_呼叫_,而不只是_名稱_,所以括號永遠不可省略(這點和某些其他語言不同)。

下面的例子示範了這個語法。 個別函式會在稍後介紹。

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"
Advanced

stringr函式庫是以較低階函式構成的 stringi 函式庫為基礎。

如果你有上面還沒提到的特定需求,那麼:

  1. 查閱 stringr 的文件,它涵蓋的選項比我們在這份(已經很長的)概念文件裡能提到的還多。
  2. 查閱 stringi 的文件,它描述了更多功能。
透過 GitHub 編輯 連結會在新視窗或分頁中開啟

學習 字串

練習已鎖定

再解鎖 1 個練習,就能練習 字串