http://yphuang.github.io/blog/2016/03/15/regular-expression-and-strings-processing-in-R/
0.動機:為什么學習字符串處理
傳統的統計學教育幾乎沒有告訴過我們,如何進行文本的統計建模分析。然而,我們日常生活中接觸到的大部分數據都是以文本的形式存在。文本分析與挖掘在業界中也有着非常廣泛的應用。
由於文本數據大多屬於非結構化的數據,要想對文本數據進行傳統的統計模型分析,必須要經過層層的數據清洗與整理。
今天我們要介紹的『正則表達式及R字符串處理』就是用來干這一種臟活累活的。
與建立酷炫的模型比起來,數據的清洗與整理似乎是一種低檔次的工作。如果把建立模型類比於高級廚師的工作,那么,數據清洗無疑是類似切菜洗碗打掃衛生的活兒。然而想要成為資深的『數據玩家』,這種看似低檔次的工作是必不可少的,並且,這種工作極有可能將占據你整個建模流程的80%的時間。
如果我們能夠掌握高效的數據清洗工具,那么我們將擁有更多的時間來進行模型選擇和參數調整,使得我們的模型更加合理有效。
此外,對於不需要進行文本建模分析的同學,掌握文本處理的工具也將對減輕你的工作負擔大有益處。下面,我舉幾個我自身經歷的『文本處理工具讓生活更美好』的例子:
-
R輔助郵件查閱及核對:R字符串處理應用之郵件考勤自動化
-
R輔助SAS處理大量數據:深入理解SAS之批量數據導入
-
R爬蟲獲取研究數據:這個將在后續的講座作為一個專題進行介紹。
可見,我們可以用到文本處理工具的場景還是非常多的,如批量文件名修改、批量字符替換、大量郵件或html文件處理等。
下面,我將通過一個例子,展示R字符串處理的大致功能。接着,介紹正則表達式的基礎概念。然后,介紹R字符串處理中一個非常好用的拓展包stringr
,並接着介紹一些文件編碼處理相關的函數。最后,通過一兩個案例展示字符串處理的真實應用場景。
1.A toy example ——初步認識R中的字符串處理
為了先給大家一個關於R字符串處理的大體認識,我們使用R中自帶的一個數據集USArrests
進行函數功能演示。
先看看數據集的結構。
# take a glimpse head(USArrests)
字符串子集提取:獲得州的簡稱
# 獲得州名 states = rownames(USArrests) # 方法一:substr() substr(x = states, start = 1, stop = 4) # 方法二:abbreviate() abbreviate(states,minlength = 5)
字符統計:獲得名字最長的州名
# get number of characters in each state name state_chars <- nchar(states) # hist hist(nchar(states),main = "Histogram", xlab = "number of charaters in US State names") # longest state's name states[which(state_chars == max(state_chars))]
注意:nchar()與length()的區別
字符串匹配:含某些字母的州名
# get states names with 'w' grep(pattern = "w", x = states, value = TRUE) ########################### # get states names with 'W' OR 'w' ## Method 1: grep(pattern = "[wW]", x = states, value = TRUE) ## Method 2: grep(pattern = "w", x = tolower(states),value = TRUE) ## Method 3: grep(pattern = "W", x = toupper(states), value = TRUE) ## Method 4: grep(pattern = "w",x = states, ignore.case = TRUE, value = TRUE)
字符統計:某些字母個數統計
library(stringr) # total number of a's str_count(states,"a") ################## # number of vowels # vector of vowels vowels <- c("a","e","i","o","u") # vector for storing results num_vowels <- vector(mode = "integer",length = 5) # calculate for(i in seq_along(vowels)){ num_aux <- str_count(tolower(states),vowels[i]) num_vowels[i]<-sum(num_aux) } # add names names(num_vowels)<-vowels # total number of vowels num_vowels # barplot barplot(num_vowels, main = "number of vowels in USA States names")
2.正則表達式
正則表達式是對字符串類型數據進行匹配判斷,提取等操作的一套邏輯公式。
處理字符串類型數據方面,高效的工具有Perl和Python。如果我們只是偶爾接觸文本處理任務,則學習Perl無疑成本太高;如果常用Python,則可以利用成熟的正則表達式模塊:re
庫;如果常用R,則使用Hadley大神開發的stringr
包則已經能夠游刃有余。
下面,我們先簡要介紹重要並通用的正則表達式規則。接着,總結一下stringr
包中需要輸入正則表達式參數的字符處理函數。
元字符(Metacharacters)
大部分的字母和所有的數字都是匹配他們自身的正則表達式。然而,在正則表達式的語法規定中,有12個字符被保留用作特殊用途。他們分別是:
[ ] \ ^ $ . | ? * + ( )
如果我們直接進行對這些特殊字符進行匹配,是不能匹配成功的。正確理解他們的作用與用法,至關重要。
library(stringr) metaChar = c("$","*","+",".","?","[","^","{","|","(","\\") grep(pattern="$", x=metaChar, value=TRUE) grep(pattern="\\", x=metaChar, value=TRUE) grep(pattern="(", x=metaChar, value=TRUE) gsub(pattern="|", replacement=".", "gsub|uses|regular|expressions") strsplit(x="strsplit.aslo.uses.regular.expressions", split=".")
它們的作用如下:
[ ]
:括號內的任意字符將被匹配;
# example grep(pattern = "[wW]", x = states, value = TRUE)
\
:具有兩個作用:- 1.對元字符進行轉義(后續會有介紹)
- 2.一些以
\
開頭的特殊序列表達了一些字符串組
strsplit(x="strsplit.aslo.uses.regular.expressions", split=".") # compare strsplit(x="strsplit.aslo.uses.regular.expressions", split="\\.") ################ # function 2: library(stringr) str_extract_all(string = "my cridit card number: 34901358932236",pattern = "\\d")
^
:匹配字符串的開始.將^
置於character class的首位表達的意思是取反義。如[^5]
表示匹配除了”5”以外的任何字符。
# function 1 test_vector<-c("123","456","321") library(stringr) str_extract_all(test_vector,"3") str_extract_all(test_vector,"^3") # function 2 str_extract_all(test_vector,"[^3]")
$
:匹配字符串的結束。但將它置於character class內則消除了它的特殊含義。如[akm$]
將匹配’a’,’k’,’m’或者’$’.
# function 1 test_vector<-c("123","456$","321") library(stringr) str_extract_all(test_vector,"3$") # function 2 str_extract_all(test_vector,"[3$]")
.
:匹配除換行符以外的任意字符。
str_extract_all(string = c("regular.expressions\n","\n"), pattern ="\\.")
|
:或者
test_vector2<-c("AlphaGo實在厲害!","alphago是啥","阿爾法狗是一條很凶猛的狗。") str_extract_all(string = test_vector2, pattern ="AlphaGo|阿爾法狗")
?
:前面的字符(組)是可有可無的,並且最多被匹配一次
str_extract_all(string = c("abc","ac","bc"),pattern = "ab?c")
*
:前面的字符(組)將被匹配零次或多次
str_extract_all(string = c("abababab","abc","ac"),pattern = "(ab)*")
+
:前面的字符(組)將被匹配一次或多次
str_extract_all(string = c("abababab","abc","ac"),pattern = "(ab)+")
( )
:表示一個字符組,括號內的字符串將作為一個整體被匹配。
str_extract_all(string = c("ababc","ac","cde"),pattern = "(ab)?c") str_extract_all(string = c("abc","ac","cde"),pattern = "ab?c")
重復
代碼 | 含義說明 |
---|---|
? |
重復零次或一次 |
* |
重復零次或多次 |
+ |
重復一次或多次 |
{n} |
重復n次 |
{n,} |
重復n次或更多次 |
{n,m} |
重復n次到m次 |
str_extract_all(string = c("abababab","ababc","ababababc"),pattern = "(ab){2,3}")
轉義
如果我們想查找元字符本身,如”?”和”*“,我們需要提前告訴編譯系統,取消這些字符的特殊含義。這個時候,就需要用到轉義字符\
,即使用\?
和\*
.當然,如果我們要找的是\
,則使用\\
進行匹配。
strsplit(x="strsplit.aslo.uses.regular.expressions", split=".") # compare strsplit(x="strsplit.aslo.uses.regular.expressions", split="\\.")
注:R中的轉義字符則是雙斜杠:\\
R中預定義的字符組
代碼 | 含義說明 |
---|---|
[:digit:] |
數字:0-9 |
[:lower:] |
小寫字母:a-z |
[:upper:] |
大寫字母:A-Z |
[:alpha:] |
字母:a-z及A-Z |
[:alnum:] |
所有字母及數字 |
[:punct:] |
標點符號,如. , ; 等 |
[:graph:] |
Graphical characters,即[:alnum:]和[:punct:] |
[:blank:] |
空字符,即:Space和Tab |
[:space:] |
Space,Tab,newline,及其他space characters |
[:print:] |
可打印的字符,即:[:alnum:],[:punct:]和[:space:] |
library(stringr) str_extract_all(string = "my cridit card number: 34901358932236",pattern = "\\d")
代表字符組的特殊符號
代碼 | 含義說明 |
---|---|
\w |
字符串,等價於[:alnum:] |
\W |
非字符串,等價於[^[:alnum:]] |
\s |
空格字符,等價於[:blank:] |
\S |
非空格字符,等價於[^[:blank:]] |
\d |
數字,等價於[:digit:] |
\D |
非數字,等價於[^[:digit:]] |
\b |
Word edge(單詞開頭或結束的位置) |
\B |
No Word edge(非單詞開頭或結束的位置) |
\< |
Word beginning(單詞開頭的位置) |
\> |
Word end(單詞結束的位置) |
3.stringr
字符串處理函數對比學習
stringr
包中的重要函數
函數 | 功能說明 | R Base中對應函數 |
---|---|---|
使用正則表達式的函數 | ||
str_extract() |
提取首個匹配模式的字符 | regmatches() |
str_extract_all() |
提取所有匹配模式的字符 | regmatches() |
str_locate() |
返回首個匹配模式的字符的位置 | regexpr() |
str_locate_all() |
返回所有匹配模式的字符的位置 | gregexpr() |
str_replace() |
替換首個匹配模式 | sub() |
str_replace_all() |
替換所有匹配模式 | gsub() |
str_split() |
按照模式分割字符串 | strsplit() |
str_split_fixed() |
按照模式將字符串分割成指定個數 | - |
str_detect() |
檢測字符是否存在某些指定模式 | grepl() |
str_count() |
返回指定模式出現的次數 | - |
其他重要函數 | ||
str_sub() |
提取指定位置的字符 | regmatches() |
str_dup() |
丟棄指定位置的字符 | - |
str_length() |
返回字符的長度 | nchar() |
str_pad() |
填補字符 | - |
str_trim() |
丟棄填充,如去掉字符前后的空格 | - |
str_c() |
連接字符 | paste(),paste0() |
可見,stringr
包中的字符處理函數更豐富和完整,並且更容易記憶。
文本文件的讀寫
這里的文本文件指的是非表格式的文件,如純文本文件,html文件。文本文件的讀取可以使用readLines()
和scan()
函數。一般需要通過encoding =
參數設置文件內容的編碼方式。
#假設當前路徑有一個文件為`file.txt` text <- readLines("file.txt", encoding = "UTF-8") #默認設置,每個單詞作為字符向量的一個元素 scan("file.txt", what = character(0),encoding = "UTF-8") #設置成每一行文本作為向量的一個元素,這類似於readLines scan("file.txt", what = character(0), sep = "\n",encoding = "UTF-8") #設置成每一句文本作為向量的一個元素 scan("file.txt", what = character(0), sep = ".",encoding = "UTF-8")
文本文件的寫出可以使用cat()
和writeLines()
函數。
# 假設要保存當前環境中的R變量text # sep參數指定要保存向量里的元素的分割符號。 cat(text, file = "file.txt", sep = "\n") writeLines(text, con = "file.txt", sep = "\n", useBytes = F)
字符統計及字符翻譯
x<- c("I love R","I'm fascinated by Statisitcs") ################## ## 字符統計 # nchar nchar(x) # str_count library(stringr) str_count(x,pattern = "") str_length(x) ###################### DNA <- "AgCTaaGGGcctTagct" ## 字符翻譯:大小寫轉換 tolower(DNA) toupper(DNA) ## 字符翻譯:符號替換(逐個替換) # chartr chartr("Tt", "Uu", DNA) #將T鹼基替換成U鹼基 # 注意:與str_replace()的區別 library(stringr) str_replace_all(string = DNA,pattern = "T",replacement = "U") %>% str_replace_all(string = .,pattern = "t",replacement = "u")
字符串連接
# paste paste("control",1:3,sep = "_") # str_c() library(stringr) str_c("control",1:3,sep = "_")
字符串拆分
# strsplit text <- "I love R.\nI'm fascinated by Statisitcs." cat(text) strsplit(text,split = " ") strsplit(text,split = "\\s") # str_split library(stringr) str_split(text,pattern = "\\s")
字符串查詢
字符串的查詢或者搜索應用了正則表達式的匹配來完成任務. R Base 包含的字符串查詢相關的函數有grep(),grepl(),regexpr(),gregexpr()和regexec()等。
################################# ## 包含匹配 # grep x<- c("I love R","I'm fascinated by Statisitcs","I") grep(pattern = "love",x = x) grep(pattern = "love",x = x,value = TRUE) grepl(pattern = "love",x = x) # str_detect str_detect(string = x, pattern = "love") ################################# # # match,完全匹配, 常用的 %in% 由match()定義 match(x = "I",table = x) "I'm" %in% x
字符串替換
sub()和gsub()能夠提供匹配替換的功能,但其替換的實質是先創建一個對象,然后對原始對象進行重新賦值,最后結果好像是“替換”了一樣。
sub()和gsub()的區別在於,前者只替換第一次匹配的字串(請注意輸出結果中world的首字母),而后者會替換掉所有匹配的字串。
也可以使用substr和substring對指定位置進行替換。
##################################### ## 匹配替換 test_vector3<-c("Without the vowels,We can still read the word.") # sub sub(pattern = "[aeiou]",replacement = "-",x = test_vector3) # gsub gsub(pattern = "[aeiou]",replacement = "-",x = test_vector3) # str_replace_all str_replace_all(string = test_vector3,pattern = "[aeiou]", replacement = "-") ########################################## ## 指定位置替換
字符串提取
常用到的提取函數有substr()和substring(),它們都是靠位置來進行提取的,它們自身並不適用正則表達式,但是它們可以結合正則表達式函數regexpr(),gregexpr()和regexec()等可以方便地從文本中提取所需信息。
stringr
包中的函數str_sub
和str_dup
可以通過位置提取,而str_extract
和str_match
可以通過正則表達式提取。
substr("abcdef", start = 2, stop = 4) substring("abcdef", first = 1:6, last = 2:7) str_sub("abcdef",start = 2, end = 4) str_sub("abcdef",start = 1:6, end = 1:6) ################################ text_weibo<- c("#圍棋人機大戰# 【人工智能攻克圍棋 AlphaGo三比零完勝李世石】","谷歌人工智能AlphaGo與韓國棋手李世石今日進行了第三場較量","最終AlphaGo戰勝李世石,連續取得三場勝利。接下來兩場將淪為李世石的“榮譽之戰。") # str_match_all,返回的列表中的元素為矩陣 str_match_all(text_weibo,pattern = "#.+#") str_match_all(text_weibo, pattern = "[a-zA-Z]+") # str_extract_all,返回的列表中的元素為向量 str_extract_all(text_weibo,pattern = "#.+#") str_extract_all(text_weibo, pattern = "[a-zA-Z]+")
字符串定制輸出
這個內容有點類似於字符串的連接。R中相應的函數為strtrim(),用於將字符串修剪到特定的顯示寬度。stringr
中相應的函數為:str_pad().
strtrim()會根據width參數提供的數字來修剪字符串,若width提供的數字大於字符串的字符數的話,則該字符串會保持原樣,不會增加空格之類的東西,若小於,則刪除部分字符。而str_pad()則相反。
strtrim(c("abcde", "abcde", "abcde"),width = c(1, 5, 10)) str_pad(string = c("abcde", "abcde", "abcde"),width = c(1, 5, 10),side = "right")
strwrap()會把字符串當成一個段落來處理(不管段落中是否有換行),按照段落的格式進行縮進和分行,返回結果就是一行行的字符串。
而str_wrap()不對文本直接切割成向量,而是在文本內容中插入了縮進或分行的標識符。
string <- "Each character string in the input is first split into\n paragraphs (or lines containing whitespace only). The paragraphs are then formatted by breaking lines at word boundaries." strwrap(x = string, width = 30) #str_wrap str_wrap(string = string,width = 30) cat(str_wrap(string = string, width = 30))
4.字符編碼相關的重要函數
windows下處理字符串類型數據最頭疼的無疑是編碼問題了。這里介紹幾個編碼轉換相關的函數。
函數 | 功能說明 |
---|---|
iconv() |
轉換編碼格式 |
Encoding() |
查看編碼格式;或者指定編碼格式 |
tau::is.locale() |
tests if the components of a vector of character are in the encoding of the current locale |
tau::is.ascii() |
|
tau::is.utf8() |
tests if the components of a vector of character are true UTF-8 strings |
雖然查看編碼方式已經有Encoding()
函數,但是這個函數往往在很多時候都不靈,經常返回惱人的“Unknow”。而火狐瀏覽器進行網頁文本編碼識別的一個 c++ 庫universalchardet ,可以識別的編碼種類較多。文鋒寫了一個相應的R包接口,專用於文件編碼方式檢測,具體請參考:checkenc - 自動文本編碼識別
devtools::install_github("qinwf/checkenc") library(checkenc) checkenc("2016-03-10-regular-expression-and-strings-processing-in-R.html") Encoding("2016-03-10-regular-expression-and-strings-processing-in-R.html")
5.應用案例
最后,給大家展示一個小小的爬蟲案例:爬取豆瓣T250中的電影信息進行分析。這里出於練習的目的刻意使用了字符串處理函數,在實際的爬蟲中,有更方便快捷的實現方式。
本案例改編自肖凱老師的博客在R語言中使用正則表達式,原博客使用R Base中的函數進行處理字符串,這里已經全部更改為stringr
中的函數進行處理。
library(stringr) library(dplyr) url <-'http://movie.douban.com/top250?format=text' # 獲取網頁原代碼,以行的形式存放在web變量中 setInternet2() web <- readLines(url,encoding="UTF-8") # 找到包含電影名稱的行 name<-str_extract_all(string = web, pattern = '<span class="title">.+</span>') movie.names_line <- unlist(name) # 用正則表達式來提取電影名 movie.names <- str_extract(string = movie.names_line, pattern = ">[^&].+<") %>% str_replace_all(string = ., pattern = ">|<",replacement = "") movie.names<- na.omit(movie.names) # 獲取評價人數 Rating<- str_extract_all(string = web,pattern = '<span>[:digit:]+人評價</span>') Rating.num_line<-unlist(Rating) Rating.num<- str_extract(string = Rating.num_line, pattern = "[:digit:]+") %>% as.numeric(.) #獲取評價分數 Score_line<-str_extract_all(string = web, pattern = '<span class="rating_num" property="v:average">[\\d\\.]+</span>') Score_line<- unlist(Score_line) Score<- str_extract(string = Score_line, pattern = '\\d\\.\\d') %>% as.numeric(.) # 數據合並 MovieData<- data.frame(MovieName = movie.names, RatingNum = Rating.num,