第14章 字元串
原文地址:R for Data Science
英語水平有限,歡迎指正錯誤!
14.1 概述
這一章介紹在R中處理字元串。你將會學習關於字元串的工作原理以及如何創建字元串的基本知識,本章的重點是用字元串表示的正則表達式和簡單的正則表達式。因為字元串通常包含非結構化或半結構化數據,正則表達式是一種用於描述字元串模式的簡潔語言,所以正則表達式很有用。第一次看到正則表達式時,你會覺得這是一堆亂碼,但是隨著理解加深,你很快就能理解其中的意義。
註:一個正則表達式通常被稱為一個模式(pattern),為用來描述或者匹配一系列匹配某個句法規則的字元串。
14.1.1 前期準備
本章將會重點關注用於字元串處理的Stringr包。Stringr包不是核心tidyverse包中的一部分,因為你並非總是有文本數據,所以我們需要載入這個包。
library(tidyverse)library(stringr)
註:tidyverse包是一組共享通用數據表示和「API」設計的包,可以查看所有Hadley的R包的文檔。
14.2 字元串的基礎知識
單引號『』或雙引號「」用於創建字元串,與其他編程語言不同,在R中用單引號或雙引號創建字元串並沒有區別,建議使用雙引號「」,除非所創建的字元串里包含雙引號時才使用單引號『』創建字元串。
string1 <- "This is a string"string2 <- If I want to include a "quote" inside a string, I use single quotes
如果只用了引號的一邊而忘記打引號的另一邊,會返回+及後面有字元串提示錯誤:
> "This is a string without a closing quote+ + + HELP IM STUCK
若出現了這種情況,請按Escape鍵並重試!
字元串中如包含單引號和雙引號,用將其進行「轉義」,即通過(轉義符)告訴R字元串中的引號不是用於創建字元串,而是作為字元:
double_quote <- """ # or "single_quote <- # or ""
如果輸入的字元串包含字元反斜杠,需要雙寫反斜杠""將其轉義。
注意列印輸出字元串與字元串本身不同,因為列印輸出字元串顯示轉義符,要查看字元串的原始內容,使用函數writeLines():
x <- c(""", "")x#> [1] """ ""writeLines(x)#> "#>
還有一些其他的特殊字元,最常用的有"
"(創建新行)," "(製表符),你可以通過以下方式來查看關於"使用幫助的完整列表:? " 或?" "。你有時會看到像"u00b5"的字元串,這是一種在所有平台上輸入非英語字元的方式:
x <- "u00b5"x#> [1] "μ"
一個字元向量通常存儲多個字元串向量, c()可以用於創建字元向量:
c("one", "two", "three")#> [1] "one" "two" "three"
14.2.1 字元串長度
R本身中自帶很多字元處理函數,但一般不用這些函數,因為這些函數的名字不規則很難記住。我們將會使用stringr包中的函數,這些函數的名字更加直觀,包中所有函數的名字都以str_為開頭,例如str_length()計算字元向量中每個字元串的字元數量:
str_length(c("a", "R for data science", NA))#> [1] 1 18 NA
使用RStudio時,這個通用前綴str_會很有用,因為輸入str_就會自動彈出stringr包中的所有函數供你查看:
14.2.2 連接字元串
函數str_c()用於連接兩個或以上字元串:
str_c("x", "y")#> [1] "xy"str_c("x", "y", "z")#> [1] "xyz"
使用參數sep選擇字元串之間的分隔符:
str_c("x", "y", sep = ", ")#> [1] "x, y"
函數str_c()跟R中大部分其他函數一樣,缺失值不是字元,如果想輸出字元"NA",使用函數str_replace_na():
x <- c("abc", NA)str_c("|-", x, "-|")#> [1] "|-abc-|" NAstr_c("|-", str_replace_na(x), "-|")#> [1] "|-abc-|" "|-NA-|"
如上所示,函數str_c()會被矢量化,會自動循環重複短向量使其與最長向量的長度相同:
str_c("prefix-", c("a", "b", "c"), "-suffix")#> [1] "prefix-a-suffix" "prefix-b-suffix" "prefix-c-suffix"
長度為0的對象會默認忽略,當str_c()與if連用時會很有用:
name <- "Hadley"time_of_day <- "morning"birthday <- FALSEstr_c( "Good ", time_of_day, " ", name, if (birthday) " and HAPPY BIRTHDAY", ".")#> [1] "Good morning Hadley."
將字元向量中的字元串連接成一個字元串使用參數collapse:
str_c(c("x", "y", "z"), collapse = ", ")#> [1] "x, y, z"
14.2.3 提取字元串子集
函數str_sub()用於提取字元串子集,函數 str_sub()中的參數start和end指定字元串子集提取開始和結束的位置:
x <- c("Apple", "Banana", "Pear")str_sub(x, 1, 3)#> [1] "App" "Ban" "Pea"# negative numbers count backwards from endstr_sub(x, -3, -1)#> [1] "ple" "ana" "ear"
注意當字元串長度比所提取子集長度短時,函數str_sub()不會顯示錯誤,會儘可能返回最多的值:
str_sub("a", 1, 5)#> [1] "a"
函數str_sub()可通過賦值的方式來修改字元串:
str_sub(x, 1, 1) <- str_to_lower(str_sub(x, 1, 1))x#> [1] "apple" "banana" "pear"
14.2.4 區域設置
註:不同系統、平台、與軟體有不同的區域設置處理方式和不同的設置範圍,但是一般區域設置最少也會包括語言和地區。
在上述內容中,用str_to_lower()將文本改為小寫形式,用str_to_upper()或str_to_title()可以改變字元串的形式。因為不同的語言改變其形式有不同的規則,所以對比其初始形式改變其形式很複雜,通過指定區域設置來選擇改寫的規則集:
# 土耳其語i有兩種大寫形式:有點和沒有點# 根據不同規則選擇其大寫形式:str_to_upper(c("i", "?"))#> [1] "I" "I"str_to_upper(c("i", "?"), locale = "tr")#> [1] "?" "I"
區域設置為ISO 639定義的語言代碼,以兩個或三個字母的縮寫形式表示,如果你不清楚當前使用語言的代碼,維基百科有一個詳盡的列表供你查看。如不指定區域設置,函數將會使用當前操作系統的區域設置。
區域設置會影響另一個重要操作——排序,R基礎函數order()和sort()使用當前區域設置將文字進行排序。使用函數str_sort()和str_sort()並指定參數locale可在不同的計算機中強制使用與當前區域設置不同的語言及排序規則對字元進行排序:
x <- c("apple", "eggplant", "banana")str_sort(x, locale = "en") # English#> [1] "apple" "banana" "eggplant"str_sort(x, locale = "haw") # Hawaiian#> [1] "apple" "eggplant" "banana"
14.2.5 練習
1、 在不使用stringr包的代碼中,會經常看到paste()和paste0()。這兩個函數的功能有什麼區別?stringr包中哪兩個函數功能與這兩個函數相同?這些函數處理缺失值NA時有什麼不同?
2、 描述函數str_c()中參數sep和collapse的區別。
3、 當字元串含有偶數個字元,如何使用函數str_length()和函數str_sub()來提取字元串中間的字元?
4、 函數str_wrap()的功能是什麼?你可能會在什麼情況下使用它?
5、 函數str_trim()的功能是什麼?與函數str_trim()功能相反的函數是什麼?
6、 編寫一個函數將向量(例如c("a", "b","c"))轉化為字元串a, b, and c。仔細思考如果給定一個長度為0、1或2的向量,這個函數應該會如何處理?
14.3 用正則表達式進行匹配
正則表達式是一種很簡潔的語言,用於描述字元串的模式。你一看到正則表達式就會覺得頭暈,但你一旦理解了正則表達式,你會發現它們很有用。
為了學習正則表達式,我們將會使用函數str_view()和函數str_view_all()。這兩個函數有兩個參數,一個字元向量和一個正則表達式,函數返回的值會顯示他們是如何匹配的。我們從非常簡單的正則表達式開始,接著慢慢深入。一旦你掌握了兩者之間的匹配方式,你會學習到如何將stringr包中各種函數應用於實際情況。
14.3.1 基本匹配
最簡單的模式是匹配準確的字元串:
x <- c("apple", "banana", "pear")str_view(x, "an")
稍複雜的模式是 .,匹配任意字元(換行除外):
str_view(x, ".a.")
但如果.匹配任意字元,那麼如何匹配字元.呢?你需要在正則表達式中使用轉義字元表示.是匹配模式,而不是使用.的特殊用法(匹配任意字元)。與字元串相同,正則表達式使用反斜杠作為轉義符,將具有特殊用法的符號進行轉義,因此正則表達式.匹配字元.,同時也產生了另一個問題,我們使用字元串來表示正則表達式,而在字元串中也作為一個轉義符號,所以需要用字元串"\."來創建正則表達式.。
# 需要用\來創建表示正則表達式的字元串dot <- "\."# 但正則表達式本身只包含一個:writeLines(dot)#> .# 下列表達式尋找 .str_view(c("abc", "a.c", "bef"), "a\.c")
如果在正則表達式中使用作為轉義字元,那麼怎樣匹配字元?你需要創建一個正則表達式\,為了創建表示這個正則表達式的字元串需要使用將轉義的字元串,這就意味著你需要輸入"\"匹配字元——你需要四個反斜杠的字元串匹配一個反斜杠。
x <- "a\b"writeLines(x)#> astr_view(x, "\")
在本書中,正則表達式表示為.和用於表示正則表達式的字元串表示為"\."。
註:將下一個字元標記為一個特殊字元(FileFormat Escape)、或一個原義字元(Identity Escape)、或一個向後引用(backreferences)、或一個八進位轉義符。例如,「n」匹配字元「n」。「
」匹配一個換行符。序列「\」匹配「」而「(」則匹配「(」。
14.3.1.1 練習
1、 請解釋字元串""、""和"\"不匹配的原因?
2、 如何匹配序列" ?
3、 正則表達式......匹配什麼模式?如何用字元串表示它?
14.3.2 指定位置
在默認情況下,正則表達式將會匹配字元串任意部分,在正則表達式中用符號指定在字元串的開頭和結尾進行匹配:
? ^匹配字元串開頭
? $匹配字元串結尾
x <- c("apple", "banana", "pear")str_view(x, "^a")
str_view(x, "a$")
你可以嘗試我從Evan Misshula學習到的記憶方法來記住這兩個符號的含義:當你開始擁有權力(^),你最終會得到金錢($)。
在正則表達式同時使用^和$強制正則表達式僅僅匹配完整字元串:
x <- c("apple pie", "apple", "apple cake")str_view(x, "apple")
str_view(x, "^apple$")
通過限制所匹配單詞的首尾,我不經常在R中用,但是當在RStudio中想搜索的函數名字是其他函數名字的一部分,我有時會用。例如,我會用sum來查找sum而避免匹配其他函數,如summarise、summary、rowsum等等。
14.3.2.1 練習
1、如何匹配字元串"$^$"?
2、在stringr::words中給出了常用單詞的語料庫,創建正則表達式以找出下列單詞:
(1)以「y」開頭
(2)以「x」結尾
(3)正好有3個字母(不要作弊使用str_length()!)
(4)有7個或以上字母
因為這個列表很長,你可能想用函數str_view()的參數match來顯示匹配或者未匹配的單詞。
14.3.3 字元類型及替代模式
有一些特殊模式可匹配1個以上字元,你已經學習了可以匹配任意字元換行符除外的特殊模式 . ,還有其他4個可以用於匹配多個字元的工具:
d:匹配任意數字
s:匹配任意空格(例如:空格、製表符、換行符)
[abc] :匹配a、b或c
[^abc]:匹配除a、b或c以外的所有字元
記住要創建一個包含d或s的正則表達式,你需要在字元串中將轉義,因此要輸入"\d" 或 "\s"
使用豎直分隔符|來表示1個或以上可供選擇匹配的替代模式,例如,abc|d..f將會匹配"abc"或者"deaf"。
注意|的優先順序很低,因此abc|xyz匹配abc或xyz,而不是abcyz或abxyz。如果其優先順序對匹配模式造成混淆,可以像數學表達式一樣用圓括弧清楚表明匹配模式:
str_view(c("grey", "gray"), "gr(e|a)y")
14.3.3.1 練習
1.創建正則表達式找到下列所有詞:
(1)以母音開頭
(2)只包含輔音(提示:匹配不含母音的詞。)
(3)以ed結尾,但不包括以eed結尾
(4)以ing或ise結尾
2. 驗證經驗規則「在英語拼寫中i總是在e之前,除非前一個字母是c,在這種情況下e在i之後」
3.「u」總是跟在 「q」後面嗎?
4.創建一個正則表達式匹配以英式英語而非美式英語書寫的單詞
5.創建一個正則表達式匹配你所在國家最常見的手機號碼
14.3.4 重複
指定模式匹配次數:
?:0或1
+:1或1以上
*:0或0以上
x <- "1888 is the longest year in Roman numerals: MDCCCLXXXVIII"str_view(x, "CC?")
str_view(x, "CC+")
str_view(x, C[LX]+)
注意這些運算符的優先順序很高,因此輸入colou?r可匹配美式英語或英式英語的拼寫。大多數情況下需要使用圓括弧以免造成混淆,如bana(na)+。
精確指定匹配次數:
{n}:n次
{n,}:n或n以上
{,m}:最多m次
{n,m}:n至m的區間
str_view(x, "C{2}")
str_view(x, "C{2,}")
str_view(x, "C{2,3}")
默認情況下會儘可能匹配最長的字元串,可以在模式後使用?儘可能匹配最短的字元串。這是正則表達式的高級功能,知道這個會很有用:
str_view(x, C{2,3}?)
str_view(x, C[LX]+?)
14.3.4.1 練習
1.用 {m,n}寫出與?、 +、 *功能相同功能的形式
2.敘述下列正則表達式的匹配模式:(仔細閱讀下列是正則表達式還是用字元串來定義的正則表達式)
(1)^.*$
(2)"\{.+\}"
(3)d{4}-d{2}-d{2}
(4)"\\{4}"
3.創建正則表達式找到下列所有詞:
(1)以三個輔音開頭
(2)含連續三個或三個以上母音
(3)含連續兩個或兩個以上母音-輔音組合
(4)在Regex Crossword上完成初學者正則表達式填字遊戲。
14.3.5 分組和向後引用
在前面,你已經學習了用圓括弧來解決有歧義的複雜表達式。圓括弧還可以用於定義「組」,可通過指定組來向後引用,如1、 2等等。例如,下面的正則表達式找出有一對重複字母且表示水果的單詞:
str_view(fruit, "(..)\1", match = TRUE)
(與str_match()連用會很有用。)
14.3.5.1 練習
1.敘述下列表達式所匹配的模式
(1)(.)11
(2)"(.)(.)\2\1"
(3)(..)1
(4)"(.).\1.\1"
(5)"(.)(.)(.).*\3\2\1"
2.創建正則表達式匹配下列詞:
(1)以相同字元開頭和結尾
(2)包含一對重複的字母(例如:「church」中的「ch」 重複了兩次)
(3)包含重複一個字母至少三遍的詞(例如:「eleven」 有三個 「e」)
14.4 工具
現在你已經學習了正則表達式的基礎知識,接下來應該學習如何應用正則表達式解決實際問題。在這一部分你將會學習Stringr包中各種各樣的函數讓你:
1. 判斷哪個字元串匹配了模式
2. 找到匹配的位置
3. 提取匹配的內容
4. 用新內容替換匹配模式
5. 基於匹配模式分割字元串
在我們繼續之前要注意:正則表達式很有用,用單個正則表達式解決問題很容易,但是有些時候只用一個正則表達式解決問題很複雜,用Jamie Zawinski的話說:
有些人在面對問題時,他們會想「我可以用正則表達式解決這個問題。」現在他們面臨著兩個問題。
作為提醒,看看這個用於檢查電子郵箱地址是否有效的正則表達式:
(?:(?:
)?[ ])*(?:(?:(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|"(?:[^"
\]|\.|(?:(?:
)?[ ]))*"(?:(?:
)?[ ])*)(?:.(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|"(?:[^"
\]|\.|(?:(?:
)?[ ]))*"(?:(?:
)?[ ])*))*@(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*)(?:.(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*))*|(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|"(?:[^"
\]|\.|(?:(?:
)?[ ]))*"(?:(?:
)?[ ])*)*<(?:(?:
)?[ ])*(?:@(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*)(?:.(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*))*(?:,@(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*)(?:.(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*))*)*:(?:(?:
)?[ ])*)?(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|"(?:[^"
\]|\.|(?:(?:
)?[ ]))*"(?:(?:
)?[ ])*)(?:.(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|"(?:[^"
\]|\.|(?:(?:
)?[ ]))*"(?:(?:
)?[ ])*))*@(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*)(?:.(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*))*>(?:(?:
)?[ ])*)|(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|"(?:[^"
\]|\.|(?:(?:
)?[ ]))*"(?:(?:
)?[ ])*)*:(?:(?:
)?[ ])*(?:(?:(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|"(?:[^"
\]|\.|(?:(?:
)?[ ]))*"(?:(?:
)?[ ])*)(?:.(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|"(?:[^"
\]|\.|(?:(?:
)?[ ]))*"(?:(?:
)?[ ])*))*@(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*)(?:.(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*))*|(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|"(?:[^"
\]|\.|(?:(?:
)?[ ]))*"(?:(?:
)?[ ])*)*<(?:(?:
)?[ ])*(?:@(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*)(?:.(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*))*(?:,@(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*)(?:.(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*))*)*:(?:(?:
)?[ ])*)?(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|"(?[^"
\]|\.|(?:(?:
)?[ ]))*"(?:(?:
)?[ ])*)(?:.(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|"(?:[^"
\]|\.|(?:(?:
)?[ ]))*"(?:(?:
)?[ ])*))*@(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*)(?:.(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*))*>(?:(?:
)?[ ])*)(?:,s*(?:(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|"(?:[^"
\]|\.|(?:(?:
)?[ ]))*"(?:(?:
)?[ ])*)(?:.(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|"(?:[^"
\]|\.|(?:(?:
)?[ ]))*"(?:(?:
)?[ ])*))*@(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*)(?:.(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*))*|(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|"(?:[^"
\]|\.|(?:(?:
)?[ ]))*"(?:(?:
)?[ ])*)*<(?:(?:
)?[ ])*(?:@(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*)(?:.(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*))*(?:,@(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*)(?:.(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*))*)*:(?:(?:
)?[ ])*)?(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|"(?:[^"
\]|\.|(?:(?:
)?[ ]))*"(?:(?:
)?[ ])*)(?:.(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|"(?:[^"
\]|\.|(?:(?:
)?[ ]))*"(?:(?:
)?[ ])*))*@(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*)(?:.(?:(?:
)?[ ])*(?:[^()<>@,;:".[] 00- 31]+(?:(?:(?:
)?[ ])+||(?=[["()<>@,;:".[]]))|[([^[]
\]|\.)*](?:(?:
)?[ ])*))*>(?:(?:
)?[ ])*))*)?;s*)
這是一個有點病態的例子(因為電子郵箱地址實在太複雜了),但它運用在實際代碼中。有關更多詳細信息,請參閱Using a regular expression to validate an email address中的stackoverflow討論。
不要忘記在使用編程語言時還可以使用其他工具。相比起創建一個複雜的正則表達式,寫一系列簡單的正則表達式通常會更容易。如果你在嘗試創建單個正則表達式解決問題遇到了困難,重新想一想把問題拆解成更小的部分,用簡單的正則表達式一步一步來解決問題的每一部分。
14.4.1 檢測匹配
使用函數str_detect()檢測字元串是否匹配了模式,它會返回與輸入與字元向量字元串數量相同的邏輯值向量:
x <- c("apple", "banana", "pear")str_detect(x, "e")#> [1] TRUE FALSE TRUE
當用於數值型向量時,0相當於FALSE,1相當於TRUE。當你想查看長度很長的字元向量所匹配模式的數量時,使用函數sum()和mean():
# 有多少個以t開頭的常用單詞?sum(str_detect(words, "^t"))#> [1] 65# 以母音結尾的常用單詞佔多少比例?mean(str_detect(words, "[aeiou]$"))#> [1] 0.277
當匹配模式的邏輯條件很複雜時(例如匹配a或b但不匹配c除d外),將函數str_detect()和邏輯運算符組合起來通常會比創建單個正則表達式更簡單。例如,下面的兩種方式均是找到不包含母音的所有詞:
# 找到不包含母音的所有單詞no_vowels_1 <- !str_detect(words, "[aeiou]")# 找到僅由輔音(非母音)組成的所有單詞no_vowels_2 <- str_detect(words, "^[^aeiou]+$")identical(no_vowels_1, no_vowels_2)#> [1] TRUE
返回的結果是相同的,但第一種方式顯然更容易理解。如果正則表達式實在太複雜了,嘗試將其拆分成一個個小部分,給每一部分命名,然後與邏輯運算符組合起來。
函數str_detect()經常用於選取包含匹配模式的字元串,用邏輯子集與其組合或者直接用函數str_subset()進行選取:
words[str_detect(words, "x$")]#> [1] "box" "sex" "six" "tax"str_subset(words, "x$")#> [1] "box" "sex" "six" "tax"
如果字元串是數據集中的一列,可以用filter()代替:
df <- tibble( word = words, i = seq_along(word))df %>% filter(str_detect(words, "x$"))#> # A tibble: 4 × 2#> word i#> <chr> <int>#> 1 box 108#> 2 sex 747#> 3 six 772#> 4 tax 841
函數str_count()是函數str_detect()的變形,返回字元串中匹配模式的數量,而不是僅僅返回邏輯值:
x <- c("apple", "banana", "pear")str_count(x, "a")#> [1] 1 3 1# 每個詞平均有多少個母音?mean(str_count(words, "[aeiou]"))#> [1] 1.99
函數str_count()和函數mutate()一起使用:
df %>% mutate( vowels = str_count(word, "[aeiou]"), consonants = str_count(word, "[^aeiou]") )#> # A tibble: 980 × 4#> word i vowels consonants#> <chr> <int> <int> <int>#> 1 a 1 1 0#> 2 able 2 2 2#> 3 about 3 3 2#> 4 absolute 4 4 4#> 5 accept 5 2 4#> 6 account 6 3 4#> # ... with 974 more rows
注意匹配是不會重疊的。例如,"abababa"將會匹配多少次模式"aba"呢?正則表達式返回的值是2,而不是3:
str_count("abababa", "aba")#> [1] 2str_view_all("abababa", "aba")
注意函數str_view_all()的使用。你很快會學習到,很多stringr包函數都是配對的:一個函數使用一個匹配,另一個函數與所有的對象匹配,第二種函數都有後綴_all。
14.4.2 練習
1、 嘗試同時使用兩種方式,單個正則表達式和多個函數str_detect()的調用組合解決下列問題
(1)找到所有以x開頭或結尾的詞
(2)找到所有以母音開頭並以輔音結尾的詞
(3)有沒有包含至少一個不同母音的詞
2、 哪個詞包含數量最多的母音?哪個詞包含比例最高的輔音?(提示:兩個問題的共同點是什麼?)
14.4.3 提取匹配內容
使用函數str_extract()提取匹配模式對應的文本,我們需要一個更複雜的例子展示這個函數的使用。我將會使用 Harvard sentences,它是設計用來測試VOIP 系統,但是用來練習正則表達式也很有用。stringr包提供stringr::sentences:
length(sentences)#> [1] 720head(sentences)#> [1] "The birch canoe slid on the smooth planks." #> [2] "Glue the sheet to the dark blue background."#> [3] "Its easy to tell the depth of a well." #> [4] "These days a chicken leg is a rare dish." #> [5] "Rice is often served in round bowls." #> [6] "The juice of lemons makes fine punch."
假設我們想找到包含顏色詞語的所有句子,首先創建一個顏色名稱的字元向量,然後將其轉換成單個正則表達式:
colours <- c("red", "orange", "yellow", "green", "blue", "purple")colour_match <- str_c(colours, collapse = "|")colour_match#> [1] "red|orange|yellow|green|blue|purple"
現在可以選擇包含顏色詞語的句子,然後將句子里表示顏色的詞提取出來看看是哪些詞:
has_colour <- str_subset(sentences, colour_match)matches <- str_extract(has_colour, colour_match)head(matches)#> [1] "blue" "blue" "red" "red" "red" "blue"
注意函數str_extract()僅僅提取第一個匹配,通過首先顯示含有匹配數量大於1的所有句子最容易看到是哪些詞:
more <- sentences[str_count(sentences, colour_match) > 1]str_view_all(more, colour_match)
str_extract(more, colour_match)#> [1] "blue" "green" "orange"
這是stringr包中函數返回值的常見模式,在單一匹配時所返回的值是很簡單的數據結構。使用函數str_extract_all()返回一個包含所有匹配的列表:
str_extract_all(more, colour_match)#> [[1]]#> [1] "blue" "red" #> #> [[2]]#> [1] "green" "red" #> #> [[3]]#> [1] "orange" "red"
你會在列表和迭代中了解更多關於列表的知識。
如果使用參數simplify = TRUE,函數str_extract_all()將會返回一個矩陣,在矩陣中長度較短的匹配將會擴展至與長度最長匹配相同的長度:
str_extract_all(more, colour_match, simplify = TRUE)#> [,1] [,2] #> [1,] "blue" "red"#> [2,] "green" "red"#> [3,] "orange" "red"x <- c("a", "a b", "a b c")str_extract_all(x, "[a-z]", simplify = TRUE)#> [,1] [,2] [,3]#> [1,] "a" "" "" #> [2,] "a" "b" "" #> [3,] "a" "b" "c"
14.4.3.1 練習
1、在前面的例子中,你可能已經注意到了正則表達式匹配了不是表示顏色的「flickered」,修改正則表達式解決這個問題。
2、從Harvard sentences的數據中提取:
(1)每個句子的第一個詞
(2)所有以ing結尾的單詞
(3)所有複數的單詞
14.4.4 分組匹配
在本章節前面部分我們談到在匹配時使用圓括弧來表明優先順序和向後引用的內容,圓括弧也可以用來提取部分複雜匹配模式。例如,假定我們想從句子中提取名詞,我們會找在「a」和「the」後面的詞。在正則表達式定義單詞有點棘手,因此這裡採用一個簡單的近似:一個至少含有一個字元但不是空格的序列。
noun <- "(a|the) ([^ ]+)"has_noun <- sentences %>% str_subset(noun) %>% head(10)has_noun %>% str_extract(noun)#> [1] "the smooth" "the sheet" "the depth" "a chicken" "the parked"#> [6] "the sun" "the huge" "the ball" "the woman" "a helps"
函數str_extract()返回完整匹配的值,函數str_match()給出匹配每個單獨部分,它返回一個矩陣而不是一個字元向量,一列顯示完整匹配,後面每一列顯示每個部分的分組:
has_noun %>% str_match(noun)#> [,1] [,2] [,3] #> [1,] "the smooth" "the" "smooth" #> [2,] "the sheet" "the" "sheet" #> [3,] "the depth" "the" "depth" #> [4,] "a chicken" "a" "chicken"#> [5,] "the parked" "the" "parked" #> [6,] "the sun" "the" "sun" #> [7,] "the huge" "the" "huge" #> [8,] "the ball" "the" "ball" #> [9,] "the woman" "the" "woman" #> [10,] "a helps" "a" "helps"
(毫不意外我們關於檢測名詞的嘗試是很拙劣,也匹配到了像smooth和 parked的形容詞)
如果數據是在數據框中,使用函數tidyr::extract()會更簡單。它的工作原理與函數str_match()相似,但要求給匹配命名並會創建新的一列:
tibble(sentence = sentences) %>% tidyr::extract( sentence, c("article", "noun"), "(a|the) ([^ ]+)", remove = FALSE )#> # A tibble: 720 × 3#> sentence article noun#> * <chr> <chr> <chr>#> 1 The birch canoe slid on the smooth planks. the smooth#> 2 Glue the sheet to the dark blue background. the sheet#> 3 Its easy to tell the depth of a well. the depth#> 4 These days a chicken leg is a rare dish. a chicken#> 5 Rice is often served in round bowls. <NA> <NA>#> 6 The juice of lemons makes fine punch. <NA> <NA>#> # ... with 714 more rows
與函數str_extract()相同,如果想對每個字元串進行匹配,需要使用函數str_match_all()。
14.4.4.1 練習
1、找到所有數字(如「one」、 「two」、 「three」 等等)後面的詞,同時返回數字及其後面的詞。
2、找到所有縮寫。以省略號為界限,分割省略號前面和後面的內容。
14.4.5 替換匹配內容
函數str_replace()和函數str_replace_all()用新的字元串替換匹配。最簡單的使用方法是用一個固定的字元串替換匹配:
x <- c("apple", "pear", "banana")str_replace(x, "[aeiou]", "-")#> [1] "-pple" "p-ar" "b-nana"str_replace_all(x, "[aeiou]", "-")#> [1] "-ppl-" "p--r" "b-n-n-"
使用函數str_replace_all()可通過提供命名向量執行多個替換:
x <- c("1 house", "2 cars", "3 people")str_replace_all(x, c("1" = "one", "2" = "two", "3" = "three"))#> [1] "one house" "two cars" "three people"
你可以使用向後引用來插入匹配內容而不是用一個固定的字元串替換。在下面的代碼中翻轉了第二個和第三個詞的順序。
sentences %>% str_replace("([^ ]+) ([^ ]+) ([^ ]+)", "\1 \3 \2") %>% head(5)#> [1] "The canoe birch slid on the smooth planks." #> [2] "Glue sheet the to the dark blue background."#> [3] "Its to easy tell the depth of a well." #> [4] "These a days chicken leg is a rare dish." #> [5] "Rice often is served in round bowls."
14.4.5.1 練習
1、用反斜杠 替換字元串中所有斜線 /。
2、用函數replace_all()實現與函數str_to_lower()相同的功能。
3、調轉words中所有詞的第一個和最後一個字母。調轉後哪些字元串仍然是單詞?
14.4.6 分割字元串
使用函數str_split()來分割字元串,例如將句子分割成單詞:
sentences %>% head(5) %>% str_split(" ")#> [[1]]#> [1] "The" "birch" "canoe" "slid" "on" "the" "smooth" #> [8] "planks."#> #> [[2]]#> [1] "Glue" "the" "sheet" "to" "the" #> [6] "dark" "blue" "background."#> #> [[3]]#> [1] "Its" "easy" "to" "tell" "the" "depth" "of" "a" "well."#> #> [[4]]#> [1] "These" "days" "a" "chicken" "leg" "is" "a" #> [8] "rare" "dish." #> #> [[5]]#> [1] "Rice" "is" "often" "served" "in" "round" "bowls."
因為每個句子可能包含數量不同的片段,所以它會返回列表。如果向量中只含有一個字元串,查看分割後所得片段最簡單的方法是選取列表的第一個元素。
"a|b|c|d" %>% str_split("\|") %>% .[[1]]#> [1] "a" "b" "c" "d"
另外,如同stringr包中返回列表的其他函數,使用參數simplify= TRUE返回矩陣:
sentences %>% head(5) %>% str_split(" ", simplify = TRUE)#> [,1] [,2] [,3] [,4] [,5] [,6] [,7] #> [1,] "The" "birch" "canoe" "slid" "on" "the" "smooth"#> [2,] "Glue" "the" "sheet" "to" "the" "dark" "blue" #> [3,] "Its" "easy" "to" "tell" "the" "depth" "of" #> [4,] "These" "days" "a" "chicken" "leg" "is" "a" #> [5,] "Rice" "is" "often" "served" "in" "round" "bowls."#> [,8] [,9] #> [1,] "planks." "" #> [2,] "background." "" #> [3,] "a" "well."#> [4,] "rare" "dish."#> [5,] "" ""
同樣可以指定返回分割片段的最大數量:
fields <- c("Name: Hadley", "Country: NZ", "Age: 35")fields %>% str_split(": ", n = 2, simplify = TRUE)#> [,1] [,2] #> [1,] "Name" "Hadley"#> [2,] "Country" "NZ" #> [3,] "Age" "35"
相對於用模式分割字元串,函數boundary()可以指定以字元、行、句子和單詞分割字元串:
x <- "This is a sentence. This is another sentence."str_view_all(x, boundary("word"))
str_split(x, " ")[[1]]#> [1] "This" "is" "a" "sentence." "" "This" #> [7] "is" "another" "sentence."str_split(x, boundary("word"))[[1]]#> [1] "This" "is" "a" "sentence" "This" "is" #> [7] "another" "sentence"
14.4.6.1 練習
1、將字元串"apples,pears, and bananas"分割為單詞。
2、為什麼用boundary("word")將字元串分割成單詞比用「 」更好?
3、用一個空字元串("")作為模式進行分割會發生什麼?用文檔做個試驗看看會發生了什麼。
14.4.7 查找匹配
函數str_locate()和函數str_locate_all()會給出每個匹配開始和結束的位置。當沒有其他函數的功能能符合需要時,這兩個函數特別有用,用函數str_locate()來尋找匹配模式,函數str_sub()提取和/或修改他們。
14.5 其他類型的模式
如果使用字元串作為模式,將會自動調用函數regex()並且將字元串用於這個函數,這個函數的功能是將字元串轉換為正則表達式:
# The regular call:str_view(fruit, "nana")# Is shorthand forstr_view(fruit, regex("nana"))
使用函數regex()其他參數控制匹配的細節:
參數ignore_case = TRUE字元的大小寫形式均能匹配,常常使用當前區域設置。
bananas <- c("banana", "Banana", "BANANA")str_view(bananas, "banana")
str_view(bananas, regex("banana", ignore_case = TRUE))
參數multiline = TRUE指定^和$匹配每一行的開頭和結尾而不是完整字元串的開頭和結尾。
x <- "Line 1
Line 2
Line 3"str_extract_all(x, "^Line")[[1]]#> [1] "Line"str_extract_all(x, regex("^Line", multiline = TRUE))[[1]]#> [1] "Line" "Line" "Line"
參數comments = TRUE允許使用注釋和空格使複雜的正則字元串更容易理解。空格會被忽略,因為在#後面的內容是注釋會自動忽略。匹配文本空格需要將其轉義:"\ "
phone <- regex(" \(? # optional opening parens (\d{3}) # area code [)- ]? # optional closing parens, dash, or space (\d{3}) # another three numbers [ -]? # optional space or dash (\d{3}) # three more numbers ", comments = TRUE)str_match("514-791-8141", phone)#> [,1] [,2] [,3] [,4] #> [1,] "514-791-814" "514" "791" "814"
參數dotall = TRUE允許.匹配任何內容,包括
還有其他三個函數可以用於代替函數regex():
fixed():匹配指定的位元組序列。所有特殊的正則表達式被忽略,而且操作的優先順序很低,避免複雜的轉義,比正則表達式處理速度更快。下列微基準測試展示了一個簡單例子,函數fixed()處理速度是函數regex()的3倍:
microbenchmark::microbenchmark( fixed = str_detect(sentences, fixed("the")), regex = str_detect(sentences, "the"), times = 20)#> Unit: microseconds#> expr min lq mean median uq max neval#> fixed 157 164 228 170 272 603 20#> regex 588 611 664 635 672 1103 20
因為其他語言表達同一字元有多種不同的方式,所以對於非英語的數據使用函數fixed()是有問題的。例如,有兩種方式定義「á」:一種方式是使用單一字元;另一種方式是a加上語調:
a1 <- "u00e1"a2 <- "au0301"c(a1, a2)#> [1] "á" "a?"a1 == a2#> [1] FALSE
他們呈現相同的結果,但是因為他們的定義不同,fixed()不能匹配兩者,使用函數coll()按照字元最終的結果進行比較,而不是其定義:
str_detect(a1, fixed(a2))#> [1] FALSEstr_detect(a1, coll(a2))#> [1] TRUE
coll():使用標準文字排序規則比較字元串,在處理字元定義不匹配時很有用。注意函數coll()的參數locale能選擇比較字元的規則,不同地區語言不同使用不同的規則!
# 當處理字元形式不相同的情況時,你同樣需要知道文字比較的規則i <- c("I", "?", "i", "?")i#> [1] "I" "?" "i" "?"str_subset(i, coll("i", ignore_case = TRUE))#> [1] "I" "i"str_subset(i, coll("i", ignore_case = TRUE, locale = "tr"))#> [1] "?" "i"
函數fixed() 和函數regex()均有參數ignore_case,但都不能選擇區域設置,這兩個函數使用默認的區域設置,在使用stringi包時會看到更多下列的代碼。
stringi::stri_locale_info()#> $Language#> [1] "en"#> #> $Country#> [1] "US"#> #> $Variant#> [1] ""#> #> $Name#> [1] "en_US"
函數coll()的缺點是速度慢,因為判斷字元是否相同的規則比較複雜,所以相比函數regex()和函數fixed(),函數coll()的處理速度相對慢一些。
在函數str_split()中使用函數boundary()能指定匹配內容的類型,可與其他函數連用:
x <- "This is a sentence."str_view_all(x, boundary("word"))
str_extract_all(x, boundary("word"))#> [[1]]#> [1] "This" "is" "a" "sentence"
14.5.1 練習
1、如何用regex()和fixed()找到所有包含的字元串,比較兩種方式?
2、sentences中5個出現頻率最高的詞是什麼?
14.6 正則表達式的其他應用
R自帶的兩個函數同樣可以使用正則表達式:
函數apropos()搜索整個環境可用的所有對象,在不記得函數名字時會很有用。
apropos("replace")#> [1] "%+replace%" "replace" "replace_na" "str_replace" #> [5] "str_replace_all" "str_replace_na" "theme_replace"
函數dir()將會列出目錄中所有文件,參數pattern用正則表達式表示且僅僅返回含匹配模式的文件名。例如,你可以在當前目錄中找到所有RMarkdown文件:
head(dir(pattern = "\.Rmd$"))#> [1] "communicate-plots.Rmd" "communicate.Rmd" "datetimes.Rmd" #> [4] "EDA.Rmd" "explore.Rmd" "factors.Rmd"
(如果你更喜歡用文件擴展名,如*.Rmd,用函數glob2rx()可以將它們轉化成正則表達式)
14.7 stringi包
Stringr包是建立在stringi包的基礎上。當你學習stringr包後會很有用,因為它包含一組處理字元串最常用的函數,而stringi包更為綜合,它幾乎包含了你處理字元串可能用到的所有函數:stringi包有234個函數,stringr包有42個函數。
如果在使用stringr包處理字元串過程中遇到困難,不妨用stringi包。這兩個包很相似,因此能夠將所學習stringr包的知識運用在stringi包,這兩個包函數主要的區別是前綴,stringr包函數的前綴是str_,stringi包函數的前綴是stri_。
14.7.1 練習
1、在stringi包找到下列功能的函數:
(1)計算單詞數量
(2)找到重複字元串
(3)生成隨機文本
2、用函數stri_sort()進行排序時如何指定語言?
推薦閱讀:
※R語言數據可視化——顏色綜合運用與色彩方案共享
※學習與實踐筆記—第三講簡單數據處理
※[數據分析與可視化 25] R會議筆記:原理篇
※如何隨機地決定R程序的運行先後順序?