《數據科學中的R語言》之字元串處理入門

原文地址:r4ds.had.co.nz/strings.

作者:Hadley Wickham

---

文章目錄:

  • 1 介紹
    • 1.1 前提條件
  • 2 字元串基本操作
    • 2.1 字元串長度
    • 2.2 合併字元串
    • 2.3 分割字元串
    • 2.4 字元串本地化處理
    • 2.5 小練習
  • 3 用正則表達式匹配字元串
    • 3.1 基本匹配操作
    • 3.2 錨點
    • 3.3 字元類型和多選符
    • 3.4 重複匹配
    • 3.5 分組和反向引用
  • 4 相關工具
    • 4.1 檢測匹配結果
    • 4.2 小練習
    • 4.3 提取匹配結果
    • 4.4 分組匹配
    • 4.5 替換匹配結果
    • 4.6 切分
    • 4.7 尋找匹配位置
  • 5 其他模式類型
    • 5.1 小練習
  • 6 正則表達式的其他用途
  • 7 stringi庫
    • 7.1 小練習

---

1 介紹

這篇文章將為你介紹如何使用R語言來處理字元串。你學會字元串的基本操作和如何創建字元串,但這章的重點會在正則表達式上。由於字元串通常包含未經處理、格式混亂的數據,所以正則表達式會在此發揮很大的作用。正則表達式是種簡明的語言,用於描述字元串中的組織模式。當你第一次看到正則表達式時,他們就像躺在你鍵盤上的貓一樣無從下手,但當你逐步理解時,你就明白了其意義。

1.1 前提條件

這篇文章將以stringr包作為字元串處理的重心。由於我們通常不總會處理到文本數據,所以stringr包不是tidyverse包的核心組成部分,所以我們需要額外導入這個包。

library(tidyverse)nlibrary(stringr)n

2 字元串基本操作

你可以通過單引號或雙引號來創建字元串。不同於其他語言,這兩種方法並不會造成不同的結果。但我推薦使用雙引號"",除非你想要創建包含有多個雙引號的字元串。

string1 <- "This is a string"nstring2 <- If I want to include a "quote" inside a string, I use single quotesn

如果你忘記加上第二個雙引號(或單引號)就回車,在第二行行首會出現+,在這裡我們叫它續行符:

> "This is a string without a closing quoten+ n+ n+ HELP IM STUCKn

如果發生了這樣的情況,可以按Esc來結束這次輸入,也可以在結尾輸入第二個雙引號(或單引號)來完成輸入。

要想在字元串輸入單引號或雙引號而不引起錯誤的話,你可以使用來為其「轉義」:

double_quote <- """ # or "nsingle_quote <- # or ""n

這意味著,若是你想要輸入反斜線符號"",則需要輸入兩次:""。

但要當心,字元串的列印表示和其自身並不一樣,因為列印表示會顯示出轉義符。如果想要看到其真正的樣子,要用writeLines():

x <- c(""", "")nxn#> [1] """ ""nwriteLines(x)n#> "n#> n

這類特殊字元並不算多。最常用的便是"n",用來表示換行符(newline),還有"t",表示製表符(tab),你也可以通過輸入?"或?""來查看完整的轉義字元列表。有時你也會看到類似"μ"的字元,這是種輸入非英文字元的方法,適用於所有平台:

x <- "μ"nxn#> [1] "μ"n

多個字元串通常存在一個字元向量中,你可以通過c()來創建:

c("one", "two", "three")n#> [1] "one" "two" "three"n

2.1 字元串長度

R語言中本身自帶有多個函數來處理字元串,但我們在此不使用它們,因為它們用起來不方便且難記。在此我們會使用stringr包提供的函數。這些函數的名字相較前者更為直觀,而且都以str_開頭。例如,str_length()表示字元串中的字元數量:

str_length(c("a", "R for data science", NA))n#> [1] 1 18 NAn

如果你使用RStudio的話,str_前綴會更為好用,因為當你輸入str_時會觸發其自動完成功能,讓你能看到所有stringr包中的函數:

2.2 合併字元串

要想合併兩個及以上的字元串,就要用str_c():

str_c("x", "y")n#> [1] "xy"nstr_c("x", "y", "z")n#> [1] "xyz"n

使用seq參數來控制其合併後字元串之間的字元:

str_c("x", "y", sep = ", ")n#> [1] "x, y"n

就如R語言中的其他函數一樣,缺失值處理起來很麻煩。如果你想讓缺失值輸出為"NA",可以使用str_replace_na():

x <- c("abc", NA)nstr_c("|-", x, "-|")n#> [1] "|-abc-|" NAnstr_c("|-", str_replace_na(x), "-|")n#> [1] "|-abc-|" "|-NA-|"n

如上所示,str_c()的輸出結果也是向量,若是參數中含有固定字元串和向量,則會將固定字元串分別和各個向量結合:

tr_c("prefix-", c("a", "b", "c"), "-suffix")n#> [1] "prefix-a-suffix" "prefix-b-suffix" "prefix-c-suffix"n

而字元大小為0的對象則會被丟棄。這個特性在和if一同使用時會尤其有用:

name <- "Hadley"ntime_of_day <- "morning"nbirthday <- FALSEnnstr_c(n"Good ", time_of_day, " ", name,nif (birthday) " and HAPPY BIRTHDAY",n"."n)n#> [1] "Good morning Hadley."n

要是想要將一個字元串向量結合成單個字元串,使用collapse參數:

str_c(c("x", "y", "z"), collapse = ", ")n#> [1] "x, y, z"n

2.3 分割字元串

你可以通過使用str_sub()來分割字元串。就如字元串一樣,str_sub()也通過接受起點和終點的參數來確定切片的位置:

x <- c("Apple", "Banana", "Pear")nstr_sub(x, 1, 3)n#> [1] "App" "Ban" "Pea"n# negative numbers count backwards from endnstr_sub(x, -3, -1)n#> [1] "ple" "ana" "ear"n

需要注意的是,str_sub()在字元串比切片始末點短時也不會出錯,因為它會返回儘可能多的字元:

str_sub("a", 1, 5)n#> [1] "a"n

你也可以使用賦值形式來修改字元串:

str_sub(x, 1, 1) <- str_to_lower(str_sub(x, 1, 1))nxn#> [1] "apple" "banana" "pear"n

2.4 字元串本地化處理

之前我曾說過使用str_to_lower()來讓字母變為小寫。你也可以用str_to_upper()或str_to_title()來做類似的處理。然而,改變大小寫事實上卻比想像的複雜,因為不同語言有不同的大小寫規則和形式。你可以通過定義其國別來應用相關規則:

# 土耳其語中有兩種i: 有點的和沒有點的n# 他們的大小寫也有不一樣的規則:nstr_to_upper(c("i", "?"))n#> [1] "I" "I"nstr_to_upper(c("i", "?"), locale = "tr")n#> [1] "?" "I"n

本地化國別參數由ISO 639語言編碼標準決定,其參數通常是兩個或三個字母的縮寫。如果你不知道你自己語言的參數代碼,參閱維基上的列表。如果你為該參數留空,則默認使用當前機子的語言本地化。

另一個受本地化影響較大的操作是分類。R語言自帶的order()和sort()函數基於當前本地化來分類字元串。如果你想要讓分類在不同電腦間有更好的表現,就使用str_sort()和str_order()吧,這兩個函數也帶有locale參數:

x <- c("apple", "eggplant", "banana")nnstr_sort(x, locale = "en") # Englishn#> [1] "apple" "banana" "eggplant"nnstr_sort(x, locale = "haw") # Hawaiiann#> [1] "apple" "eggplant" "banana"n

2.5 小練習

  1. 有些不使用stringr包的代碼中,通常會有paste()和paste0()。它們之間有什麼不同?它們相當於stringr包中的什麼函數呢?這些函數在處理缺失值時又有什麼不同呢?
  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時它會怎樣。

3 用正則表達式匹配字元串

正則表達式是個十分簡明的語言,其讓你能描述字元串中的組成模式。你需要一些時間來理解正則表達式,但一旦你能看懂它們,你會發現它們有用極了。

在這裡,我們將使用str_view()和str_view_all()來學習正則表達式。這兩個函數輸入一個字元向量和一個正則表達式,輸出給你匹配結果。我們會從最簡單的正則表達式開始,慢慢過渡到更複雜的表達式。一旦你學會模式匹配方法,你會學會如何應用這些點子到大量stringr函數中。

3.1 基本匹配操作

最簡單的模式匹配是匹配對應一模一樣的字元串:

x <- c("apple", "banana", "pear")nstr_view(x, "an")n

接下來學習.的用法,它能匹配任何字元(除了換行符):

str_view(x, ".a.")n

但若是"."能匹配任何字元,它怎麼才能匹配自己,即"."字元呢?你只需要輸入轉義字元來告訴正則表達式你想匹配它就好了。就如字元串一樣,正則表達式也使用反斜線來轉義特殊字元。所以要想匹配".",你只需要使用.就好了。然而這也會造成麻煩。我們使用字元串來表現正則表達式,而在字元串里也是個轉義符號。所以若是要創建正則表達式的.,我們需要輸入字元串"."。

# To create the regular expression, we need ndot <- "."nn# But the expression itself only contains one:nwriteLines(dot)n#> .nn# And this tells R to look for an explicit .nstr_view(c("abc", "a.c", "bef"), "a.c")n

如果在正則表達式里用作轉義字元,那要匹配反斜線要怎麼做呢?你還是需要讓其轉義,創建正則表達式。要想使用正則表達式,你需要個字元串,這裡還需要轉義。所以,你想要匹配反斜線的話,你要輸入""————你需要四個反斜線才能匹配到一個真正的反斜線!

x <- "ab"nwriteLines(x)n#> abnnstr_view(x, "")n

3.1.1 小練習

  1. 解釋為什麼這些字元串不能匹配到一個:"", "", ""。
  2. 如何匹配"?
  3. ......這個正則表達式會匹配到什麼?如何用一個字元串表現它?

3.2 錨點

默認情況下,正則表達式會匹配任何位置的字元串。使用錨點對正則表達式十分有用,那樣正則表達式就能確定要匹配字元串首端或是末端的特定字元串。

你可以使用:

  • ^來匹配字元串的首端
  • $來匹配字元串的末端

x <- c("apple", "banana", "pear")nstr_view(x, "^a")n

str_view(x, "a$")n

要想記住這兩個符號,試試我從Evan Misshula學到的記憶法:

如果你一開始就擁有了力量(^),那你到最後會收穫金錢($)。

要想讓正則表達式只匹配完整的一個字元串,使用^和$來匹配:

x <- c("apple pie", "apple", "apple cake")nstr_view(x, "apple")n

str_view(x, "^apple$")n

你也可以使用b來匹配兩邊有邊界的單詞。在R里我不經常用到它,但我在使用RStudio時會用來查找某個函數是否是其他函數的組成部分。例如,我會搜索bsumb來避免匹配到summarise, summary, rowsum這些函數。

3.2.1 小練習

  1. 如何匹配"$^$"這個字元串?
  2. 根據string::words中的語料庫,創建正則表達式來分別查找符合下列條件的單詞:
    1. 以"y"開頭
    2. 以"x"結束。
    3. 正好是三個字母組成。(拒絕使用str_length(),這是作弊!)
    4. 帶有七個以上的字母。

      由於這個列表比較長,你或許想要使用str_view()中的match參數來只顯示匹配到的或是未匹配到的單詞。

3.3 字元類型和多選符

正則表達式中還有很多特殊符號能夠匹配多個字元。比如前面已經出現的.,可以匹配除了換行符的任何字元。以下是四個這類符號:

  • d: 匹配任何整數.
  • s:匹配任何空白符(例如空格符,製表符,和換行符).
  • [abc]: 能匹配a, b, 或c.
  • [^abc]: 匹配除了a, b, 或c以外的任何字元.

記住,要想創建包含d或s的正則表達式,你要多寫一個,所以應該輸入"d"或"s"。

你也可以使用多選符來在一個或多個多選模式中選取字元。例如,abc|d..f將匹配"abc"或"deaf"。注意,|的優先權較低,所以abc|xyz將匹配abc或xyz而不是abcyz或者abxyz。就如一些數學符號一樣,若是優先權問題困擾著你,你可以使用括弧來讓其意義更為清楚:

str_view(c("grey", "gray"), "gr(e|a)y")n

3.3.1 小練習

  1. 使用正則表達式匹配以下條件的單詞:

    1. 由母音開頭。
    2. 只包含輔音字母的(提示:想想如何匹配"非"母音字母)。
    3. 以ed結尾,但不以eed結尾。
    4. 以ing或ise結尾
  2. 驗證一下構詞規則"i通常在e之前,除非在前面有c"
  3. "q"後面是否總跟著"u"?
  4. 寫出個正則表達式,匹配一個由英式英語而不是美式英語寫出的單詞。
  5. 寫出個正則表達式,匹配常見的電話號碼。

3.4 重複匹配

這一部分我們將學習控制匹配的次數:

  • ?: 0次或1次
  • +: 1次及以上
  • *: 0次及以上

x <- "1888 is the longest year in Roman numerals: MDCCCLXXXVIII"nstr_view(x, "CC?")n

str_view(x, "CC+")n

str_view(x, C[LX]+)n

注意,這種操作符的優先度較高,所以你可以用colou?r來匹配其英式形式和美式形式。這意味著,其多數情況下要輸入括弧來註明,例如bana(na)+。

你也可以標明需要匹配的次數:

  • {n}: 正好n次
  • {n,}: n次及以上
  • {,m}: 最多m次
  • {n,m}: n次和m次之間

str_view(x, "C{2}")n

str_view(x, "C{2,}")n

str_view(x, "C{2,3}")n

默認情況下,這些匹配是"貪婪匹配":他們將匹配儘可能長的字元串。當然你也能設置"懶惰匹配",匹配出儘可能短的字元,只需在後面加上?即可。這是正則表達式的高級特性,而且十分有用:

str_view(x, C{2,3}?)n

str_view(x, C[LX]+?)n

3.4.1 小練習

  1. 用{m,n}來表示?,+,*這三種匹配方式。
  2. 用自己的話說一下這些正則表達式的作用(仔細閱讀,判斷我是用一個正則表達式還是一個字元串來定義正則表達式):
      1. ^.*$
      2. "{.+}"
      3. d{4}-d{2}-d{2}
      4. "{4}"
  3. 寫出個正則表達式,找出符合以下條件的單詞:
    1. 以三個輔音字母開頭。
    2. 帶有連續三個原因。
    3. 帶有連續兩個及以上母音-輔音字母對。
  4. 去完成鏈接中的正則表達式練習

3.5 分組和反向引用

之前的部分,你學會如何使用括弧來給表達式消除歧義。括弧也能用於定義"分組",這些分組能讓你在之後使用1、2這樣的符號進行反向引用。例如,以下正則表達式會找到所有包含重複字母組合的水果單詞:

str_view(fruit, "(..)1", match = TRUE)n

3.5.1 小練習

  1. 說出以下表達式會匹配到什麼:

    1. (.)11
    2. "(.)(.)21"
    3. (..)1
    4. "(.).1.1"
    5. "(.)(.)(.).*321"
  2. 寫出符合以下匹配條件的正則表達式:
    1. 以相同字元開始和結束。
    2. 包含重複的字母對(例如,"church"包含兩個"ch")
    3. 包含一個重複了至少三次的字母(例如,"eleven"有三個"e")

4 相關工具

到這裡你已經學會了正則表達式的基本用法,是時候將它們用於解決實際問題了。這部分你將學會大量stringr包里的函數,它們能讓你:

  • 確定哪些字元串能夠被匹配
  • 找到匹配結果的位置
  • 提取匹配結果的內容
  • 替換匹配結果為其他內容
  • 基於匹配結果切分字元串

這裡要注意,因為正則表達式十分強大,想要用一個正則表達式來解決所有難題也很簡單。

就如Jamie Zawinski所說:

有些人面對一個難題時,會想「我知道,我會用正則表達式來解決」。於是,他們現在遇到了兩個難題。

作為個警示的例子,看看下面這個用來檢查email地址是否有效的正則表達式:

(?:(?:rn)?[ t])*(?:(?:(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t]n)+|Z|(?=[["()<>@,;:".[]]))|"(?:[^"r]|.|(?:(?:rn)?[ t]))*"(?:(?:nrn)?[ t])*)(?:.(?:(?:rn)?[ t])*(?:[^()<>@,;:".[] 000-031]+(?:(?:(n?:rn)?[ t])+|Z|(?=[["()<>@,;:".[]]))|"(?:[^"r]|.|(?:(?:rn)?[ nt]))*"(?:(?:rn)?[ t])*))*@(?:(?:rn)?[ t])*(?:[^()<>@,;:".[] 000-0n31]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".[]]))|[([^[]r]|.)*n](?:(?:rn)?[ t])*)(?:.(?:(?:rn)?[ t])*(?:[^()<>@,;:".[] 000-031]+n(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".[]]))|[([^[]r]|.)*](?:n(?:rn)?[ t])*))*|(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])+|Zn|(?=[["()<>@,;:".[]]))|"(?:[^"r]|.|(?:(?:rn)?[ t]))*"(?:(?:rn)n?[ t])*)*<(?:(?:rn)?[ t])*(?:@(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:nrn)?[ t])+|Z|(?=[["()<>@,;:".[]]))|[([^[]r]|.)*](?:(?:rn)?[nt])*)(?:.(?:(?:rn)?[ t])*(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)n?[ t])+|Z|(?=[["()<>@,;:".[]]))|[([^[]r]|.)*](?:(?:rn)?[ t]n)*))*(?:,@(?:(?:rn)?[ t])*(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[nt])+|Z|(?=[["()<>@,;:".[]]))|[([^[]r]|.)*](?:(?:rn)?[ t])*n)(?:.(?:(?:rn)?[ t])*(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t]n)+|Z|(?=[["()<>@,;:".[]]))|[([^[]r]|.)*](?:(?:rn)?[ t])*))*)n*:(?:(?:rn)?[ t])*)?(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])+n|Z|(?=[["()<>@,;:".[]]))|"(?:[^"r]|.|(?:(?:rn)?[ t]))*"(?:(?:rnn)?[ t])*)(?:.(?:(?:rn)?[ t])*(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:nrn)?[ t])+|Z|(?=[["()<>@,;:".[]]))|"(?:[^"r]|.|(?:(?:rn)?[ tn]))*"(?:(?:rn)?[ t])*))*@(?:(?:rn)?[ t])*(?:[^()<>@,;:".[] 000-031n]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".[]]))|[([^[]r]|.)*](n?:(?:rn)?[ t])*)(?:.(?:(?:rn)?[ t])*(?:[^()<>@,;:".[] 000-031]+(?n:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".[]]))|[([^[]r]|.)*](?:(?n:rn)?[ t])*))*>(?:(?:rn)?[ t])*)|(?:[^()<>@,;:".[] 000-031]+(?:(?n:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".[]]))|"(?:[^"r]|.|(?:(?:rn)?n[ t]))*"(?:(?:rn)?[ t])*)*:(?:(?:rn)?[ t])*(?:(?:(?:[^()<>@,;:".[] n000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".[]]))|"(?:[^"r]|n.|(?:(?:rn)?[ t]))*"(?:(?:rn)?[ t])*)(?:.(?:(?:rn)?[ t])*(?:[^()<>n@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".[]]))|"n(?:[^"r]|.|(?:(?:rn)?[ t]))*"(?:(?:rn)?[ t])*))*@(?:(?:rn)?[ t]n)*(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:n".[]]))|[([^[]r]|.)*](?:(?:rn)?[ t])*)(?:.(?:(?:rn)?[ t])*(?n:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".[n]]))|[([^[]r]|.)*](?:(?:rn)?[ t])*))*|(?:[^()<>@,;:".[] 000-n031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".[]]))|"(?:[^"r]|.|(n?:(?:rn)?[ t]))*"(?:(?:rn)?[ t])*)*<(?:(?:rn)?[ t])*(?:@(?:[^()<>@,;n:".[] 000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".[]]))|[([n^[]r]|.)*](?:(?:rn)?[ t])*)(?:.(?:(?:rn)?[ t])*(?:[^()<>@,;:"n.[] 000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".[]]))|[([^[n]r]|.)*](?:(?:rn)?[ t])*))*(?:,@(?:(?:rn)?[ t])*(?:[^()<>@,;:".n[] 000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".[]]))|[([^[]nr]|.)*](?:(?:rn)?[ t])*)(?:.(?:(?:rn)?[ t])*(?:[^()<>@,;:".[] n000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".[]]))|[([^[]r]n|.)*](?:(?:rn)?[ t])*))*)*:(?:(?:rn)?[ t])*)?(?:[^()<>@,;:".[] 0n00-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".[]]))|"(?:[^"r]|n.|(?:(?:rn)?[ t]))*"(?:(?:rn)?[ t])*)(?:.(?:(?:rn)?[ t])*(?:[^()<>@,n;:".[] 000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".[]]))|"(?n:[^"r]|.|(?:(?:rn)?[ t]))*"(?:(?:rn)?[ t])*))*@(?:(?:rn)?[ t])*n(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".n[]]))|[([^[]r]|.)*](?:(?:rn)?[ t])*)(?:.(?:(?:rn)?[ t])*(?:[n^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".[]n]))|[([^[]r]|.)*](?:(?:rn)?[ t])*))*>(?:(?:rn)?[ t])*)(?:,s*(n?:(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:n".[]]))|"(?:[^"r]|.|(?:(?:rn)?[ t]))*"(?:(?:rn)?[ t])*)(?:.(?:(n?:rn)?[ t])*(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[n["()<>@,;:".[]]))|"(?:[^"r]|.|(?:(?:rn)?[ t]))*"(?:(?:rn)?[ tn])*))*@(?:(?:rn)?[ t])*(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ tn])+|Z|(?=[["()<>@,;:".[]]))|[([^[]r]|.)*](?:(?:rn)?[ t])*)(?n:.(?:(?:rn)?[ t])*(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])+|nZ|(?=[["()<>@,;:".[]]))|[([^[]r]|.)*](?:(?:rn)?[ t])*))*|(?:n[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".[n]]))|"(?:[^"r]|.|(?:(?:rn)?[ t]))*"(?:(?:rn)?[ t])*)*<(?:(?:rn)n?[ t])*(?:@(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["n()<>@,;:".[]]))|[([^[]r]|.)*](?:(?:rn)?[ t])*)(?:.(?:(?:rn)n?[ t])*(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>n@,;:".[]]))|[([^[]r]|.)*](?:(?:rn)?[ t])*))*(?:,@(?:(?:rn)?[nt])*(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,n;:".[]]))|[([^[]r]|.)*](?:(?:rn)?[ t])*)(?:.(?:(?:rn)?[ t]n)*(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:n".[]]))|[([^[]r]|.)*](?:(?:rn)?[ t])*))*)*:(?:(?:rn)?[ t])*)?n(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[["()<>@,;:".n[]]))|"(?:[^"r]|.|(?:(?:rn)?[ t]))*"(?:(?:rn)?[ t])*)(?:.(?:(?:nrn)?[ t])*(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])+|Z|(?=[[n"()<>@,;:".[]]))|"(?:[^"r]|.|(?:(?:rn)?[ t]))*"(?:(?:rn)?[ t])n*))*@(?:(?:rn)?[ t])*(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])n+|Z|(?=[["()<>@,;:".[]]))|[([^[]r]|.)*](?:(?:rn)?[ t])*)(?:n.(?:(?:rn)?[ t])*(?:[^()<>@,;:".[] 000-031]+(?:(?:(?:rn)?[ t])+|Zn|(?=[["()<>@,;:".[]]))|[([^[]r]|.)*](?:(?:rn)?[ t])*))*>(?:(n?:rn)?[ t])*))*)?;s*)n

這是個相當典型的例子(因為email地址通常驚人地複雜),但在實際代碼里經常被用到。你可以查看該StackOverflow鏈接以獲得更多細節。

別忘了你在使用編程語言,你任意使用其他工具。通常情況下,創建一系列簡單的正則表達式會比創建一整個複雜的正則表達式要簡單。如果你創建正則表達式過程中遇到困難,你可以停下想想,看是否能將問題簡化成簡單點的問題,在遇到下個問題前解決所有問題。

4.1 檢測匹配結果

要想知道一個字元向量是否符合一個模式,使用str_detect()函數。它會返回輸入向量一樣長度的邏輯向量:

x <- c("apple", "banana", "pear")nstr_detect(x, "e")n#> [1] TRUE FALSE TRUEn

記住,當你在數值型環境中使用邏輯向量時,FALSE的值為0,而TRUE的值為1。所以要是你想知道關於匹配結果的一些情況,sum()和mean()在此會很有用:

# 有多少單詞以t開頭?nsum(str_detect(words, "^t"))n#> [1] 65nn# 由母音結尾的單詞佔多大比例?nmean(str_detect(words, "[aeiou]$"))n#> [1] 0.277n

當你遇到複雜的邏輯匹配條件時(例如,要匹配a或b,但沒有d的話不匹配c),相較於使用正則表達式,使用多個str_detect()來調用邏輯運算符會處理起來更簡單。例如,下面是兩個匹配沒有母音字母單詞的方法:

# 找到所有至少包含一個母音字母的單詞,將其除外nno_vowels_1 <- !str_detect(words, "[aeiou]")nn# 找到所有隻包含輔音字母的單詞nno_vowels_2 <- str_detect(words, "^[^aeiou]+$")nnidentical(no_vowels_1, no_vowels_2)n#> [1] TRUEn

兩種方法的結果是一樣的,但我覺得第一種方法明顯更簡單明了。當你的正則表達式變得相當複雜時,試著將其分成一個個小部分,再給它們分別取名字,最後再用邏輯運算符來將它們結合起來。

通常我們使用str_detect()來選擇符合某種模式的元素。你可以將其運用於邏輯切片,或是較為方便的str_subset()分裝器:

words[str_detect(words, "x$")]n#> [1] "box" "sex" "six" "tax"nstr_subset(words, "x$")n#> [1] "box" "sex" "six" "tax"n

通常情況下,你的字元串屬於某個數據框中的一列,我們會使用filter()處理它:

df <- tibble(n word = words, n i = seq_along(word)n)ndf %>% n filter(str_detect(words, "x$"))n#> # A tibble: 4 × 2n#> word in#> <chr> <int>n#> 1 box 108n#> 2 sex 747n#> 3 six 772n#> 4 tax 841n

str_detect()的一個變體則是str_count(),這個函數不是簡單返回對或錯,而是告訴你在這一個字元串中有多少個符合條件的匹配結果:

x <- c("apple", "banana", "pear")nstr_count(x, "a")n#> [1] 1 3 1nn# On average, how many vowels per word?nmean(str_count(words, "[aeiou]"))n#> [1] 1.99n

和mutate()一起使用str_count()效果也很好:

df %>% n mutate(n vowels = str_count(word, "[aeiou]"),n consonants = str_count(word, "[^aeiou]")n)n#> # A tibble: 980 × 4n#> word i vowels consonantsn#> <chr> <int> <int> <int>n#> 1 a 1 1 0n#> 2 able 2 2 2n#> 3 about 3 3 2n#> 4 absolute 4 4 4n#> 5 accept 5 2 4n#> 6 account 6 3 4n#> # ... with 974 more rowsn

值得注意的是,這些匹配結果不會重疊。例如,在abababa這個字元串中,你覺得aba會被匹配到多少次呢?正則表達式告訴你是兩次而不是三次:

str_count("abababa", "aba")n#> [1] 2nstr_view_all("abababa", "aba")n

注意str_view_all()的用法。就如你剛知道的,很多stringr函數都是成對出現的:一個函數用於匹配單個結果,另一個匹配所有結果。而那後者則通常帶有後綴_all。

4.2 小練習

  1. 對於以下練習,試著使用單個正則表達式,和多個str_detect()調用來分別完成。
    1. 找到所有以x開頭或結尾的單詞。
    2. 找到所有以母音字母開頭和以輔音字母結束的單詞。
    3. 有沒有包含所有母音字母的單詞。
  2. 什麼單詞的母音字母數量最多?什麼單詞中母音字母所佔比例最大?(提示:這個問題中的分母是什麼呢?)

4.3 提取匹配結果

要想提取文本里匹配結果,可以使用str_extract()。這次,我將用個更為複雜的例子。我將用到Harvard sentences,這是用於檢測VOIP系統的一套文本,但在練習正則表達式時也一樣有用。它們內置於string::sentences里:

length(sentences)n#> [1] 720nhead(sentences)n#> [1] "The birch canoe slid on the smooth planks." n#> [2] "Glue the sheet to the dark blue background."n#> [3] "Its easy to tell the depth of a well." n#> [4] "These days a chicken leg is a rare dish." n#> [5] "Rice is often served in round bowls." n#> [6] "The juice of lemons makes fine punch."n

如果我們想找到所有包含顏色的句子,我們首先創建一個以顏色名字構成的向量,然後將其轉變為正則表達式:

colours <- c("red", "orange", "yellow", "green", "blue", "purple")ncolour_match <- str_c(colours, collapse = "|")ncolour_matchn#> [1] "red|orange|yellow|green|blue|purple"n

現在我們就能選出那些包含顏色詞的句子了,然後提取出來看看它們都是什麼:

has_colour <- str_subset(sentences, colour_match)nmatches <- str_extract(has_colour, colour_match)nhead(matches)n#> [1] "blue" "blue" "red" "red" "red" "blue"n

注意,str_extract()只會提取其首次匹配的結果。我們可以直接選取帶有一個匹配結果以上的所有句子:

more <- sentences[str_count(sentences, colour_match) > 1]nstr_view_all(more, colour_match)n

str_extract(more, colour_match)n#> [1] "blue" "green" "orange"n

這是stringr函數中的常見模式,因為只處理一次匹配能讓你使用更為簡單的數據結構。要想得到所有匹配結果,使用str_extract_all()。它將返回一個列表:

str_extract_all(more, colour_match)n#> [[1]]n#> [1] "blue" "red" n#> n#> [[2]]n#> [1] "green" "red" n#> n#> [[3]]n#> [1] "orange" "red"n

你將在lists部分和iteration部分了解到更多關於列表的信息。

如果你使用simplify = TRUE參數,str_extract_all()會返回一個矩陣:

str_extract_all(more, colour_match, simplify = TRUE)n#> [,1] [,2] n#> [1,] "blue" "red"n#> [2,] "green" "red"n#> [3,] "orange" "red"nnx <- c("a", "a b", "a b c")nstr_extract_all(x, "[a-z]", simplify = TRUE)n#> [,1] [,2] [,3]n#> [1,] "a" "" "" n#> [2,] "a" "b" "" n#> [3,] "a" "b" "c"n

4.3.1 小練習

  1. 在之前的例子中,你會發現那個正則表達式會匹配到"flickered",但它不是顏色詞。修改該正則表達式以修復這個問題。
  2. 從 Harvard sentences 數據中,提取:
    1. 每個句子的第一個單詞。
    2. 所有以ing結尾的單詞。
    3. 所有複數詞。

4.4 分組匹配

這篇文章前面些內容里,我們談到了使用括弧來明確優先順序和匹配時的反向引用。你也可以使用括弧來提取整個匹配結果的某些部分。例如,我們要想提取句子里的名詞。我們可以直接找出那些在"a"或"the"後面的單詞。想要在正則表達式中定義一個「單詞」的方式比較特別,所以在此我將使用個方法來匹配盡量符合條件的單詞:一串至少有一個非空格字元的字元串。

noun <- "(a|the) ([^ ]+)"nnhas_noun <- sentences %>%n str_subset(noun) %>%n head(10)nhas_noun %>% n str_extract(noun)n#> [1] "the smooth" "the sheet" "the depth" "a chicken" "the parked"n#> [6] "the sun" "the huge" "the ball" "the woman" "a helps"n

str_extract()會返回完整的匹配結果;而str_match()會返回每個獨立的組成部分,返回形式將是個矩陣而不是字元向量:

has_noun %>% n str_match(noun)n#> [,1] [,2] [,3] n#> [1,] "the smooth" "the" "smooth" n#> [2,] "the sheet" "the" "sheet" n#> [3,] "the depth" "the" "depth" n#> [4,] "a chicken" "a" "chicken"n#> [5,] "the parked" "the" "parked" n#> [6,] "the sun" "the" "sun" n#> [7,] "the huge" "the" "huge" n#> [8,] "the ball" "the" "ball" n#> [9,] "the woman" "the" "woman" n#> [10,] "a helps" "a" "helps"n

(很明顯,我們的方法仍有很大欠缺,它不僅匹配了名詞還匹配了smooth和parked這類形容詞。)

如果你的數據是tibble類型,通常使用tidyr::extract()來處理更簡單。它用起來和str_match()差不多,但要你為匹配結果命名:

tibble(sentence = sentences) %>% n tidyr::extract(n sentence, c("article", "noun"), "(a|the) ([^ ]+)", n remove = FALSEn)n#> # A tibble: 720 × 3n#> sentence article nounn#> * <chr> <chr> <chr>n#> 1 The birch canoe slid on the smooth planks. the smoothn#> 2 Glue the sheet to the dark blue background. the sheetn#> 3 Its easy to tell the depth of a well. the depthn#> 4 These days a chicken leg is a rare dish. a chickenn#> 5 Rice is often served in round bowls. <NA> <NA>n#> 6 The juice of lemons makes fine punch. <NA> <NA>n#> # ... with 714 more rowsn

如str_extract()一樣,如果你想要得到每個字元串的所有匹配結果,你要用str_match_all()來獲取。

4.4.1 小練習

  1. 找到所有緊跟著數字的單詞,例如在"one"或"two"之後的單詞。將這些數字連同單詞一起提取出來。
  2. 找到所有的縮略詞。將其以單引號為分界切片。

4.5 替換匹配結果

str_replace()和str_replace_all()可以讓你以其他字元串替換匹配結果。最簡單的用法是將匹配結果替換為某個確定的字元串:

x <- c("apple", "pear", "banana")nstr_replace(x, "[aeiou]", "-")n#> [1] "-pple" "p-ar" "b-nana"nstr_replace_all(x, "[aeiou]", "-")n#> [1] "-ppl-" "p--r" "b-n-n-"n

使用str_replace_all()還可以進行多重替換:

x <- c("1 house", "2 cars", "3 people")nstr_replace_all(x, c("1" = "one", "2" = "two", "3" = "three"))n#> [1] "one house" "two cars" "three people"n

不只是用寫好的字元串來替換,你還可以用反向引用的結果來作為替換值。下列代碼中我改變了匹配到的第二和第三個單詞的位置:

sentences %>% n str_replace("([^ ]+) ([^ ]+) ([^ ]+)", "1 3 2") %>% n head(5)n#> [1] "The canoe birch slid on the smooth planks." n#> [2] "Glue sheet the to the dark blue background."n#> [3] "Its to easy tell the depth of a well." n#> [4] "These a days chicken leg is a rare dish." n#> [5] "Rice often is served in round bowls."n

4.5.1 小練習

  1. 替換所有正斜線為反斜線。
  2. 用replace_all()來實現簡單的str_to_lower()的功能。
  3. 用words中的所有第一和最後一個字母。哪些被替換後的字元串仍是正確的單詞?

4.6 切分

用str_split()來將字元串切分為更小的部分。例如,我們可以將句子切分成單詞:

sentences %>%n head(5) %>% n str_split(" ")n#> [[1]]n#> [1] "The" "birch" "canoe" "slid" "on" "the" "smooth" n#> [8] "planks."n#> n#> [[2]]n#> [1] "Glue" "the" "sheet" "to" "the" n#> [6] "dark" "blue" "background."n#> n#> [[3]]n#> [1] "Its" "easy" "to" "tell" "the" "depth" "of" "a" "well."n#> n#> [[4]]n#> [1] "These" "days" "a" "chicken" "leg" "is" "a" n#> [8] "rare" "dish." n#> n#> [[5]]n#> [1] "Rice" "is" "often" "served" "in" "round" "bowls."n

因為每個部分的單詞字母都不一樣,這將返回一個列表。如果你在處理一個單字元長度的向量,最簡單的方法是提取列表中的第一個元素:

"a|b|c|d" %>% n str_split("|") %>% n.[[1]]n#> [1] "a" "b" "c" "d"n

否則,你可以使用simplify = TRUE返回矩陣:

sentences %>%n head(5) %>% n str_split(" ", simplify = TRUE)n#> [,1] [,2] [,3] [,4] [,5] [,6] [,7] n#> [1,] "The" "birch" "canoe" "slid" "on" "the" "smooth"n#> [2,] "Glue" "the" "sheet" "to" "the" "dark" "blue" n#> [3,] "Its" "easy" "to" "tell" "the" "depth" "of" n#> [4,] "These" "days" "a" "chicken" "leg" "is" "a" n#> [5,] "Rice" "is" "often" "served" "in" "round" "bowls."n#> [,8] [,9] n#> [1,] "planks." "" n#> [2,] "background." "" n#> [3,] "a" "well."n#> [4,] "rare" "dish."n#> [5,] "" ""n

你也可以控制列數的數量:

fields <- c("Name: Hadley", "Country: NZ", "Age: 35")nfields %>% str_split(": ", n = 2, simplify = TRUE)n#> [,1] [,2] n#> [1,] "Name" "Hadley"n#> [2,] "Country" "NZ" n#> [3,] "Age" "35"n

除了以模式來切分,你也可以以字元、行數、句子和單詞來切分:

x <- "This is a sentence. This is another sentence."nstr_view_all(x, boundary("word"))n

str_split(x, " ")[[1]]n#> [1] "This" "is" "a" "sentence." "" "This" n#> [7] "is" "another" "sentence."nstr_split(x, boundary("word"))[[1]]n#> [1] "This" "is" "a" "sentence" "This" "is" n#> [7] "another" "sentence"n

4.6.1 小練習

  1. 切分類似"apples, pears, and bananas"這樣的字元串為一個個單詞。
  2. 為什麼用boundary(word)會比用" "來切分更好?
  3. 以空字元("")切分有什麼用?試一下再閱讀文檔。

4.7 尋找匹配位置

str_locate()和str_locate_all()可以給你提供每次匹配的始末點。你可以使用str_locate()來找到匹配模式,用str_sub()來提取和/或修改他們。

5 其他模式類型

當使用的模式是字元串時,它將會自動包裝成一次調用給regex():

# The regular call:nstr_view(fruit, "nana")n# Is shorthand fornstr_view(fruit, regex("nana"))n

可以使用regex()的其他參數來控制匹配的細節:

  • ignore_case = TRUE可以無視大小寫形式來匹配。其大小寫會根據當前本地化設置而定:

bananas <- c("banana", "Banana", "BANANA")nstr_view(bananas, "banana")n

str_view(bananas, regex("banana", ignore_case = TRUE))n

  • multiline = TRUE可以讓^和$匹配每一行的始末位置,而不是整個字元串的石墨位置。

x <- "Line 1nLine 2nLine 3"nstr_extract_all(x, "^Line")[[1]]n#> [1] "Line"nstr_extract_all(x, regex("^Line", multiline = TRUE))[[1]]n#> [1] "Line" "Line" "Line"n

  • comments = TRUE可以讓你給複雜的正則表達式寫注釋。在這裡空格會被無視,#後的也不會計入運行代碼的部分。要想匹配一個空格,就要轉義它:" "

phone <- regex("n (? # optional opening parensn (d{3}) # area coden [)- ]? # optional closing parens, dash, or spacen (d{3}) # another three numbersn [ -]? # optional space or dashn (d{3}) # three more numbersn ", comments = TRUE)nnstr_match("514-791-8141", phone)n#> [,1] [,2] [,3] [,4] n#> [1,] "514-791-814" "514" "791" "814"n

  • dotall = TRUE將讓.匹配所有字元,包括n。

以下是可以代替部分regex()功能的其他函數:

  • fixed():能夠匹配準確的位元組序列。它會無視所有特別的正則表達式並在很低的水平上操作。這意味著你可以避免複雜的轉義操作並可以比正則表達式匹配得更快:

microbenchmark::microbenchmark(nfixed = str_detect(sentences, fixed("the")),n regex = str_detect(sentences, "the"),n times = 20n)n#> Unit: microsecondsn#> expr min lq mean median uq max nevaln#> fixed 157 164 228 170 272 603 20n#> regex 588 611 664 635 672 1103 20n

用fixed()處理非英文數據時要小心。因為通常會有很多種方法顯示相同的字元,所以處理起來會很麻煩。例如,定義「á」有兩種方法:用單獨的該字元表示或是用"a"加上一個音符符號來表示:

a1 <- "á"na2 <- "a?"nc(a1, a2)n#> [1] "á" "a?"na1 == a2n#> [1] FALSEn

這樣渲染出來的樣子是一致的,但是定義過程卻完全不同,所以fixed()在此無法得到匹配結果。然而使用coll()則可以按照人類對字元的對比規則來匹配:

str_detect(a1, fixed(a2))n#> [1] FALSEnstr_detect(a1, coll(a2))n#> [1] TRUEn

  • coll():使用標準排序規則來對比字元串。這在進行不分大小寫的匹配中十分有用。但要注意,coll()在比較字元時使用了locale參數來控制所使用的規則。

# 這意味著你還需要注意其規則的不同n# 在做部分大小寫的匹配時:ni <- c("I", "?", "i", "?")nin#> [1] "I" "?" "i" "?"nnstr_subset(i, coll("i", ignore_case = TRUE))n#> [1] "I" "i"nstr_subset(i, coll("i", ignore_case = TRUE, locale = "tr"))n#> [1] "?" "i"n

fixed()和regex()都接收ignore_case參數,但它們不允許你選擇本地化設置:它們都總會使用默認的本地化設置。你可以用以下代碼查看當前的默認設置:

stringi::stri_locale_info()n#> $Languagen#> [1] "en"n#> n#> $Countryn#> [1] "US"n#> n#> $Variantn#> [1] ""n#> n#> $Namen#> [1] "en_US"n

然而coll()的不足之處就是速度。因為其識別字元是否一致的規則十分複雜,coll()相對於regex()和fixed()運行速度會慢很多。

  • 如你所知,你可以用str_split()中的boundary()來匹配邊界。你也可以在其他函數使用這個參數:

x <- "This is a sentence."nstr_view_all(x, boundary("word"))n

str_extract_all(x, boundary("word"))n#> [[1]]n#> [1] "This" "is" "a" "sentence"n

5.1 小練習

  1. 用regex()和fixed()分別找到所有帶有的字元串。
  2. sentences數據中哪五個詞最常見?

6 正則表達式的其他用途

在R語言基本函數中有兩個十分有用的函數也用到了正則表達式:

  • apropos()會從全局環境中搜索所有可用對象。這在你無法記住函數名時特別有用。

apropos("replace")n#> [1] "%+replace%" "replace" "replace_na" "str_replace" n#> [5] "str_replace_all" "str_replace_na" "theme_replace"n

  • dir()將列出該目錄下的所有文件。而pattern參數則接收一個正則表達式,返回符合模式的文件名。例如,你可以這樣從當前目錄找到所有R Markdown文件:

head(dir(pattern = ".Rmd$"))n#> [1] "communicate-plots.Rmd" "communicate.Rmd" "datetimes.Rmd" n#> [4] "EDA.Rmd" "explore.Rmd" "factors.Rmd"n

7 stringi

stringr是在stringi的基礎上構建的。stringr在你學習R語言時會很有用,因為這個包只用了一小部分函數,卻解決了大多數字元串處理會遇到的問題。而stringi則是以功能的全面為目的。stringi包含了任何你需要的函數:stringi有232個函數,而stringr有43個函數。

如果你發現在用stringr包無法處理一些問題的話,你可以試試stringi中的函數。這兩個包的用法很相似,所以你可以很自然地從stringr過渡到stringi。二者最主要的區別在於前綴:str_和stri_。

7.1 小練習

  1. 找到stringi包中符合下列功能的函數:
    1. 用來統計單詞數量的函數。
    2. 用來找到重複字元串的函數。
    3. 生成隨機文本的函數。
  2. 在使用stri_sort()函數時,如何控制分類的語言?

註:本文由 Excelsior vcvc 翻譯自 Hadley Wickham. R for Data Science - string

推薦閱讀:

數據科學家的自我修養
機器學習入門必備的13張「小抄」(附下載)
edX課程預告:To Be a Data Scientist
Kaggle入門手冊

TAG:R编程语言 | 数据科学 | 字符串 |