左手用R右手Python系列13——字元串處理與正則表達式
學習數據分析,掌握一些靈巧的分析工具可以使得數據清洗效率事半功倍,比如在處理非結構化的文本數據時,如果能夠了解一下簡單的正則表達式,那麼你可以免去大量的冗餘代碼,效率那叫一個高。
正則表達式是一套微型的袖珍語言,非常強大,依靠一些特定的字母和符號作為匹配模式,靈活組合,可以匹配出任何我們需要的文本信息。
而且它不依賴任何軟體平台,沒有屬於自己的GUI,就像是流動的水一樣,可以支持絕大多數主流編程語言。
今天這一篇只給大家簡單介紹正則表達式基礎,涉及到一些常用的字元及符號含義,及其在R語言和Python中所支持的常用函數。
-------------------------------------------------------
R語言中有兩套支持正則表達式的函數,基礎函數和stringr包中的字元串處理函數系統。
因為兩套系統完成的需求差別不大,我個人用慣了基礎函數系統,同時對於一些基礎函數無法完成的需求,給出stringr中對應函數的解決方案,最後會給出基礎函數和stringr系統函數功能對照表,供大家參考。
R語言的基礎函數中,支持正則表達式的函數主要由以下幾個
- strsplit() #字元串分割函數
- grep/grepl() #字元串篩選函數
- sub/gsub() #字元串替換函數
- regexpr()/gregexpr() #返回目標字元串起始位置
- substr( )substring() #字元串截取函數
- str_extract() #返回匹配值
以上便是R語言中支持正則表達式的高頻應用函數,其中R語言基礎函數中缺少一個精確返回匹配模式結果的函數,但是stringr中彌補了這一缺陷,這裡僅詳解stringr的這一函數,其他函數感興趣可以查閱源文檔。
strsplit()
strsplit() 函數用於字元串分割,可以根據給定分隔符執行分割操作。
myword<-c("fff-888","hh-333","ff-666","ccc-666")nresult<-strsplit(myword,"-")n
strsplit函數分割之後,輸出一個與輸入對象等長的列表,如需提取分割後的兩列則需要自己構造循環。
mydata<-data.frame(word=myword,first=NA,second=NA)nfor(i in 1:length(myword)){nmydata$first[i]<-result[[i]][1]nmydata$second[i]<-result[[i]][2]n};mydatan
這樣就完成了批量字元串的分割與提取。
grep/grepl()
這是一組功能雷同的字元串篩選函數(前者可以輸出對應符合條件的記錄序號或者真實值,後者直接輸出布爾值),何為篩選,就是它只能把包含目標匹配模式的字元串對象篩選出來,但是呢,如果你需要繼續提取其中的目標字元串模式,則需進一步使用其他提取函數進行提取,所以實際上他只是過濾掉了那些不包含目標模式的字元串。
myword<-c("fff-888","hh-333","ff-666","ccc-666")ngrep("[a-z]{3}",myword,value=FALSE)ngrep("[a-z]{3}",myword,value=TRUE)ngrepl("[a-z]{3}",myword)n
以上需求匹配了含有三個小寫英文字母的記錄,分別返回了序號、記錄、布爾值,這三種方法都可以作為進一步篩選,進行行索引的合法輸入條件。
sub/gsub()
這是一組配對的字元串替換函數,用於清除輸入字元串中的若干對象或者替換成目標對象。
myword<-c("fff-888","hh-333","ff-666","ccc-666")n
比如我想把以上字元串向量中的橫杠清除掉,則可以寫成如下格式:
sub("-","",myword)n[1] "fff888" "hh333" "ff666" "ccc666"nsub("-","_",myword)n[1] "fff_888" "hh_333" "ff_666" "ccc_666"nsub("-","*",myword)n[1] "fff*888" "hh*333" "ff*666" "ccc*666"n
gsub是針對單個記錄有多個匹配模式時,可以執行全部替換。
myword<-c("fff-888-ccc","hh-333","ff-666","ccc-666")nsub("-","",myword)n[1] "fff888-ccc" "hh333" "ff666" "ccc666"ngsub("-","",myword)n[1] "fff888ccc" "hh333" "ff666" "ccc666"n
regexpr()/gregexpr()
這是一對返回目標字元串起始位置的匹配函數。
myword<-c("fff-8880000rrrr","hh-333ccccc","ff-666ooooo","ccc-666jjjjj")n
以上字元串如果想要獲知每個對象中的數字部分起始位置,則需要該函數進行位置鎖定。
regexpr("d{3,}",myword)n[1] 5 4 4 5nattr(,"match.length")n[1] 7 3 3 3nattr(,"useBytes")n[1] TRUEn
返回值是一個帶有屬性信息的原子型向量,我們可以看到目標數字在四個記錄中的開始位置分別是5,4,4,5,長度分別是7,3,3,3
gregexpr() 與regexpr的關係類比sub與gsub的關係,當記錄中出現多個匹配模式時,gregexpr值輸出第一個匹配模式的開始位置和長度,而regexpr則會輸出所有的匹配模式和長度。
myword<-c("fff-8880000rrrr7777","hh-333ccccc","ff-666ooooo","ccc-666jjjjj")nregexpr("d{3,}",myword)ngregexpr("d{3,}",myword)n
從輸出上來看,regexpr忽略了第一個記錄中最後的幾個數字,但是gregexpr成功捕獲並返回其開始位置和長度,但是也導致其輸出結果冗長繁瑣,一般不常用到。
substr( )/substring()
既然獲取到了目標字元串在原始記錄中的位置和長度,那麼提取它是分分鐘的事兒。
接下來就是substr()/substring()大顯神通的時候啦。
這兩個函數雖然完成的需求相同,但是其作用原理差異很大,substr()一次只能匹配一個字元串,所以對於向量而言需要構造循環,substring()則可以直接賦值其開始向量和結束向量,因而我們只需提前構造好開始於結束位置向量,直接傳遞參數給它就避免手動循環了。
myword<-c("fff-8880000rrrr","hh-333ccccc","ff-666ooooo","ccc-666jjjjj")nadd<-regexpr("d{3,}",myword)n[1] 5 4 4 5nattr(,"match.length")n[1] 7 3 3 3nattr(,"useBytes")n[1] TRUEn
使用substr方法提取:
result<-c()nfor(i in 1:length(myword)){nresult[i]<-substr(myword[i],add[i],add[i]+attr(add,"match.length")[i]-1)n};resultn[1] "8880000" "333" "666" "666" n
使用substring方法提取:
substring(myword,as.numeric(add),as.numeric(add)+attr(add,"match.length")-1)n[1] "8880000" "333" "666" "666"n
結果一模一樣,但是效率就差距很大了,很明顯substring具有矢量化避免顯式循環的優勢。
but,你難道不覺得以上兩步簡直是火坑嘛,不是說正則表達式無敵嘛,腫么會匹配個目標字元串這麼困難捏!
下面祭出大殺器!!!
str_extract(myword,"d{3,}") n[1] "8880000" "333" "666" "666" n
請瞪大你的汪眼仔細看清楚,一個str_extract函數等於regexpr+substr/substring,這等神器你敢信,主要是它的作者也很流弊,大名鼎鼎的哈德利威科姆,沒錯就是小編經常提到那位神一樣存在的ggplot2作者。關於stringr我今天只講了一個函數,主要是很多類型需求在基礎函數中基本都可以找到對照函數。
如果你想詳細的了解stringr包的話,請一定要仔細閱讀它的官方文檔。
http://www.cnblogs.com/nxld/p/6062950.htmln
這裡有一篇總結的還算良心的R語言基礎字元串處理函數與stringr包函數的對比。
下圖是R語言中基礎字元串處理函數(支持正則表達式)與stringr內 函數的對照圖。你可以選擇一套適合記憶用著順序的去熟練運用(很多都是同名+str_前綴組成的),但是,無論你用哪套,str_extract()函數一定要給我記清清楚嘍,除非你跟我說你喜歡用基礎函數寫更多冗長的代碼,那樣的話你自便。
-------------
Python:
-----------
每次寫R VS Pyhton系列,我都內心特別忐忑,因為我知道有很多Pyhton大佬在看我的公眾號,害怕自己丟臉的,畢竟自己才學不到四個月的python。但是害怕丟人就不能進步了,所以還是堅持寫了哈哈~_~。
Python中的正則表達式函數相對集中,沒有那麼分散,我覺的最主要的原因是很多不應該由正則或者說沒必要殺雞用宰牛刀的字元串處理需求都已經內置成很多對象的方法中去了,而os庫僅僅保留了那些最為強大的幾組核心字元串處理函數,而且Pyhton作為面向對象的高級編程語言,其對正則表達式的支持度很高,很多正則的原生方法都保留了下來,比如字元串包裝,匹配分組等(在R中你是做不到的,R對正則的支持真的很有限)。
python為了解決轉義符「」的困擾問題,使用r作為字元前綴,直接繞過了轉義難題,我們可以大膽的使用原生正則表示方法。(R中沒有解決呢,遇到多重轉義不懵逼那都是大俠)。
re模塊給出了常用的幾個支持正則匹配的字元串處理函數。
#以下為匹配所用函數
- re.split(pattern, string[, maxsplit]) #字元串拆分
- re.match(pattern, string[, flags]) #字元串匹配(只從開頭匹配模式)
- re.search(pattern, string[, flags]) #不限制位置(任何位置開始匹配)
- re.findall(pattern, string[, flags]) #查找
- re.sub(pattern, repl, string[, count]) #替換
re.split(pattern, string[, maxsplit])
拆分函數與R語言中strsplit函數作用相同,按照某種特定規則進行字元串拆分。
import renmyword=["fff-888","hh-333","ff-666","ccc-666"]n
name=[];value=[]nfor i in myword:n name.append(re.split("-",i)[0])n value.append(re.split("-",i)[1])nmyresult={"name":name,"value":value}n{name: [fff, hh, ff, ccc], value: [888, 333, 666, 666]}n
以上過程成功的將myword中所有字元串按照「-」分成了兩列的字典。
re.match()/re.search()
這是一對匹配目標字元串的函數,前者僅能匹配從字元串開頭開始的模式,後者則不限制位置,只要符合模式即可。
name=[]nfor i in myword:n name.append(re.match("[a-z]{2,3}",i).group())n[fff, hh, ff, ccc]n
以上過程成功提取出了myword中的所有以小寫字母開頭連續小寫字母部分。
myword=["333-fff-888","hh-333","ff-666","ccc-666"]nname=[]nfor i in myword:n name.append(re.match("[a-z]{2,3}",i).group())nAttributeError: NoneType object has no attribute groupn
name=[]nfor i in myword:n name.append(re.search("[a-z]{2,3}",i).group())n[fff, hh, ff, ccc]n
我將myword中的第一個字元串字母部分開頭加了數字,這種情況下re.match就無能為力了,此時就需要re.search大顯神通了。
re.findall()
re.findall()是一個強大的字元串查找函數,它會以列表形式默認返回所有搜索到的結果。
myword=["333-fff-888","hh-333-hhh","ff-666-nnn","ccc-666"]nname=[]nfor i in myword:n name.append(re.findall("[a-z]{2,3}",i))n[[fff], [hh, hhh], [ff, nnn], [ccc]]n
re.findall成功捕獲了myword中所有觀測值的不同位置符合條件的模式字元串,如果單個記錄有兩處符合目標模式的字元串,則會組成列表同時輸出。
這時候大家肯定會疑惑到底re.search和re.findall如何區別運用,各自的使用場景是什麼。
我覺得,re.search更加適合目標字元串中嵌套有很規範的匹配對象的情況,比如一段文本包含一組日期或者職業信息,可以最大化利用正則表達式所具有的分組捕獲功能分別提取各自位置的信息。re.findall則會不佳思索的不管你想要返回具體哪一個信息,都一咕嚕全部甩給你。之後你還需要在嵌套列表中繼續篩選,但是倘若是不規範文本,裡面嵌套的信息不是很規律,re.findall可以發揮它的全面性優勢,把所有符合條件的全部給你篩選出,這在網頁文本這種非結構化文本中超級有用。(個人看法,比較粗淺)。
舉一個小小的例子!
word="222-555ggg999dddd000dfff"nre.search(r"(d{3})-(d{3}).*?(d{3}).*?(d{3})[a-z].+$",word).group()nre.search(r"(d{3})-(d{3}).*?(d{3}).*?(d{3})[a-z].+$",word).group(1)nre.search(r"(d{3})-(d{3}).*?(d{3}).*?(d{3})[a-z].+$",word).group(2)nre.search(r"(d{3})-(d{3}).*?(d{3}).*?(d{3})[a-z].+$",word).group(3)nre.search(r"(d{3})-(d{3}).*?(d{3}).*?(d{3})[a-z].+$",word).group(4)n
222-555ggg999dddd000dfffn222n555n999n000n
re.search結合正則表達式的分組功能,可以輕而易舉的按照順序匹配出所有特定位置的目標模式字元串。
re.findall(r"(d{3})-(d{3}).*?(d{3}).*?(d{3})[a-z].+$",word)n[(222, 555, 999, 000)]n
re.findall更狠,不管你願不願意,直接把所有捕獲到的內容都給你弄成列表輸出了。
re.sub()
最後一個re.sub就很好理解了,它跟R語言裡面的sub函數作用差不多,就是替換。不過通常 我們用來清洗數據中的無效內容。
myword=["333-fff-888","hh-333","ff-666","ccc-666"]n
比如我們想要把myword中的「-」全部清除掉,僅需以下步驟。
name=[]nfor i in myword:n name.append(re.sub("-","",i))nprint(name)n[333fff888, hh333hhh, ff666nnn, ccc666]n
然後輸出就基本不帶無效字元串啦。
好了,R語言和派森中的有關字元串處理與正則支持函數基本就這些了(並未包含完,主要我使用的也很有限,這幾個是很高頻的需求,可以解決數據清洗中的大部分問題)。
擦,介紹了這麼多,上面使用的正則還沒有怎麼介紹呢,不過正則表達式博大精深,絕非一兩篇文章能夠將清除的,我這裡僅僅做一些常見匹配模式羅列,強烈建議大家去看專業的參考書和網站,說實話,正則表達式寫好了,就像藝術家 一樣,短短几行火星文,能夠讓你少寫十行代碼。
首先幾個元字元必須要掌握:
轉義符,對沒有任何特殊含義的字母進行轉義,使之具備某種特殊含義(包括轉義它自己),或者對具有特殊含義的字元進行本義還原。n^ 匹配以目標模式開頭的字元串。n$ 匹配以目標模式結束的字元串。n* 這是一個數量限定符,匹配前面的子表達式零次或多次,不可獨立實用。n+ 同上,匹配前面的子表達式一次或多次。n? 同上,匹配前面的子表達式零次或一次。n{n} 同上,n是一個非負整數。匹配確定的n次。n{n,} 同上,n是一個非負整數。至少匹配n次。n{n,m} 同上,匹配目標字元串出現次數在n~m之間。n. 匹配除「n」之外的任何單個字元。n[] 匹配一組可能出現的組合,內部的任意單個模式之間是或關係。n[^] 匹配一組不可能出現的組合,內部的任意單個模式之間是或關係。n() 將可能出現的模式進行分組,可以從返回的匹配結果中捕獲分組內容。 nx|y 匹配x或yn
常用的數字與字母匹配:
[0-9] #匹配任意一個數字(0~9之間)n[a-z] #匹配任意一個小寫字母n[A-Z] #匹配任意一個大寫字母n[a-zA-Z] #匹配任意一個字母n[0-9a-zA-Z] #匹配任意一個字母或者數字n
當出現連續數字或者 字母時,使用以上模式看起來很不美觀,正則表達式中提供了經過轉義的簡寫形式。
d #匹配任意一個數字nw #匹配包括下劃線的任何單詞字元。等價於「[A-Za-z0-9_]」。n[一-龥] #匹配中文字元n.*? #這裡是將.*的貪婪匹配模式轉化為懶惰匹配模式,防止匹配過多內容n
當然,這些僅僅是正則表達式的冰山一角,真正能夠達到簡化代碼效率的正則表達式,有些時候看著很可怕的,至少超過一行的正則我基本就很難看懂了,還是那句話,正則 表達式博大精深,大家要勇於探索。
這是一張總結的很棒的正則速查表:
來源:http://www.jb51.net/shouce/jquery1.82/regexp.htmln
本文參考文獻:
http://www.cnblogs.com/nxld/p/6062950.html nhttp://cuiqingcai.com/977.htmlnhttp://www.jb51.net/shouce/jquery1.82/regexp.htmlnhttp://www.jb51.net/tools/zhengze.htmln
牆裂建議各位看看這四篇文章(大神請閃開)。
本文小結:
Python:
- re.split(pattern, string[, maxsplit])
- re.match(pattern, string[, flags])
- re.search(pattern, string[, flags])
- re.findall(pattern, string[, flags])
- re.sub(pattern, repl, string[, count])
R語言:
- strsplit() #字元串分割函數
- grep/grepl() #字元串篩選函數
- sub/gsub() #字元串替換函數
- regexpr()/gregexpr() #返回目標字元串起始位置
- substr( )/substring() #字元串截取函數
- str_extract() #返回匹配值
在線課程請點擊文末閱讀原文:
Hellobi Live | 9月12日 R語言可視化在商務場景中的應用
往期數據源文件請移步本人GitHub:
https://github.com/ljtyduyu/DataWarehouse/tree/master/File
推薦閱讀: