正則表達式(regular expression,以下簡稱正則)是指用抽象的字符串來描述匹配文本中特定的結構,以達到提取結構化數據的目的。許多程式語言(如R,python,C++ ,JS等等),甚至連大家熟悉的excel 都支持正則。學習正則能夠幫助數據分析者更高效的從非結構化的文本中提取信息,從而高效的解決實踐中遇到的各種與文本相關的數據問題。本文將總結正則能處理的各類問題,同時介紹在R語言中如何使用正則,並給出實例演示。最後給出一些在R語言中使用正則的tips & tricks。
我們在日常的數據分析中,可能會遇到如下問題 如:
請對下列名人名言中出現的數字(不包括中文數字)求和。
所以你需要提取的數字有 30、90、80、200和69。
如果僅僅只有這一個文本,手動倒也無可厚非。但是在處理研究數據的實踐中,你可能要面對許多這樣的非結構化文本。如果全部手動,不僅會耗費很大的時間和精力,而且容易出錯。
再如,有時候數據上的小數點是逗號,而我想要把逗號都改成」.」 ,否則excel可能把逗號當成分割符,數據就出錯了。
期望的數據:
實際的數據:
同理,如果數據點非常多,則很難保證不犯錯誤。
上述問題如果使用正則就會變得非常簡單。
此外,如果你想系統學習文本分析,以及文本分析衍生出的內容分析,情感分析等等方法,正則幾乎是必學內容之一。另外,正則也是網絡爬蟲的基礎技能之一。
下面我將具體介紹如何在R語言中使用正則。
R語言中有許多可以和正則配合使用的函數,本文主要介紹stringr包中的str_extract_all 和str_replace功能 (實際上stringr包中有許多能夠實現字符抓取,替換,定位和計量的功能,但是這些函數都要通過正則來實現)。關於stringr包的詳情見:https://stringr.tidyverse.org/
2.1 基本字符串匹配
首先是最基本的字符匹配,比如我們想看看在之前提到的名人名言中出現了多少個中文「一」字。我們只需要先寫好正則,再使用stringr包中的str_extract_all 提取,然後使用unlist() 把列表拆解為向量,然後使用length得出向量長度即可。具體代碼如下:
text <- readLines("text1.txt",encoding = 'UTF-8')ming_ren_ming_yan <- stringr::str_extract_all(text,"一") %>% unlist() %>% length()(註:讀中文需要設置編碼為『UTF-8』,另外這裡使用了「%>%」管道函數,不熟悉的同學可以參考此前公眾號發布的R相關文章)
這種簡單的字符匹配同樣適用於其他語言(比如英文)。
2.2 特殊字符與元字符
2.2.1 特殊字符與元字符簡介
在實際的數據分析中,人們常常不需要抓取給定的字符(比如給定的漢字或者英文),而是需要抓取一系列有意義的字符串。比如前文提到的問題 1(找出數字並求和),在這種情況下,我們需要藉助正則表達式的特殊字符。
所謂特殊字符,即能夠表示一系列有意義字符串的字符。比如下圖中的\d 可以代表任意數字字符(和「[:digit:]」同義)。不同的特殊字符相互組合可以靈活匹配文本內容。
而元字符則指不直接表示自身含義而用來指代其他特殊含義的字符,在正則中一共有12個元字符,它們分別是(左括弧和右括弧單獨算一個符號):
[ ] \ ^ $ . | ? * + ( )
其具體指代的含義如下:
[] 表示字符的集合,用於匹配方括號中的其中一個字符
\ 特殊標記符,可以將元字符轉義為普通字符
^ 匹配字符串開始的位置
$ 匹配字符串結束的位置
. 匹配除了換行符之外的任意字符1次
| x或y 取其中一個字符
? 貪婪模式和懶惰模式轉換符,模糊匹配轉換符,或者用於零寬斷言判斷(見下文)
* 匹配該符號前的字符任意次
+ 匹配該符號前的字符1到多次
() 分組符號(見下文)
之後的實例將介紹其中一些元字符的用法,其他元字符的用法可以參考:
http://yphuang.github.io/blog/2016/03/15/regular-expression-and-strings-processing-in-R/ 進行進一步學習。
下列表格總結了R中特殊字符的用法:
R語言中的特殊字符(table 8-1 & table 8-3)總結
(上述表格摘抄自:Automated Data Collection with R Chapter 8)
接下來我們使用元字符來回答名人名言求和的問題。
首先,需要構建正則,我們需要提取數字,所以輸入數字的特殊字符(注,在R中使用特殊字符需要在特殊字符前多加一個「\」)。代碼如下:
這裡「\\d」 表示匹配數字字符,後跟「+」 表示至少匹配一個數字字符(也就是個位數),所以連起來的意思是匹配整數數字(包括十位,百位,千位數等等,但不包括小數,因為沒有匹配小數點)。
這裡同樣可以用上述表格中提到的「[:digit:]「來構建正則,代碼如下:
接下來使用str_extract_all函數來提取數字(註:text 為已經讀入R的名人名言),代碼如下:
numbers <- stringr::str_extract_all(text,pattern) %>% unlist()然後再使用purrr包中的reduce一鍵求和,代碼如下(這裡由於提取的是字符,所以需要轉換為數字):
purrr::reduce(as.numeric(numbers),sum)所以求和的結果應該是:
2.2.2 匹配元字符本身
匹配元字符本身這種情況比較少出現,但是不排除這種情況會出現。如果需要匹配元字符,比如匹配問號」?」,則需要在斜槓前增加轉義符(也就是兩個斜槓,轉義符的意思是把元字符變為普通字符),所以正則寫出來是這樣:
Pattern <- 「\\?」
比如從下列字符中匹配問號(註:中文的問號和英文問號編碼不同,故中文問號不是元字符,可以不加轉義符直接匹配,這裡出於演示目的使用英文問號):
代碼如下:
a <- 「這好嗎?這不好。」stringr::str_extract_all(a,Pattern) %>% unlist()結果如下:
2.3 正則表達式進階
2.3.1 懶惰模式與模糊匹配
R語言正則默認是貪婪匹配模式,意思是正則會匹配儘量長的字符串。通過添加一個問號「?」 就可以將貪婪模式改為懶惰模式,即提取滿足正則的最短的字符串 。需要注意的是,關閉貪婪模式只能在「*」,「+」和「{n}」 等匹配次數限制符號之後加「?」,懶惰模式通常和後文提到的零寬斷言一起使用,用來匹配第一次出現xx字符之前的所有內容。例子請參考零寬斷言的實例。若問號 「?」 出現在了非次數限制符號之後,則表示模糊匹配,意為可以與問號「?」(英文問號)前的字符匹配也可不匹配。實例如下:
Pattern <- "年輕?" stringr::str_extract_all(text,Pattern) %>% unlist()所以上述代碼可能匹配「年輕」也可能匹配「年」。
抓取結果如下:
2.3.2 分組與引用
元字符中有一些可以用來重複單個字符,比如「+」,「{}」 等等,但如果要重複多個字符組,就需要使用分組了。在正則中,分組可以使用 「()」 將一組正則符號括起來,這樣就可以進一步使用「+」 和「{}」等符號來匹配若干個「組」了(所以括號也是元字符,如果要匹配括弧,也需要使用轉義符)。在進行分組之後,你還可以對括號分組的正則進行引用。這一方法通常用來修改抓取的字符串上。具體實例如下
比如我們截取上述名人名言裡兩個年輕人對馬大師使用的前兩招:
前兩招在名人名言中還是很直觀的:
正則如下:
Pattern <- "(一個[左|右]\\S{0,1}[拳|腿|蹬])(,)"這裡「[左|右]」表示字符「左」或者字符「右」,「\\S」表示非空格任意字符,連上「{0,1}」表示「\\S」所佔的字符位置可能沒有字符,也可能有一個非空格字符。然後使用str_replace 分別配上解說:
stringr::str_replace("一個左正蹬,一個右鞭腿", pattern=Pattern, replacement = "(第一招)\\1 \\2 (第二招)")結果如下:
這裡的replacement 參數中的\\1 和\\2 即是對分組結果的引用。\\1表示第一個括號內正則抓取的內容。
2.3.3 零寬斷言(Zero-Length Assertions)
在實際進行數據提取時,也會常常遇到這種情況,即需要提取的字符串本身沒有明顯的規律,但是在這些字符串前面或者後面出現的字符串卻有規律。這樣我們就可以通過正則添加限定條件,即匹配這些有規律字符之後或者之前的字符,來完成字符匹配。這裡正則僅僅是佔了一個位置,並不直接匹配目標內容, 故稱為零寬斷言。
具體用法為:
匹配某某字符串之後的內容:(?<= 某某字符串)
匹配某某字符串之前的內容:(?= 某某字符串)
此外,我們還可以加入否定邏輯,具體用法為:
匹配非某某字符串之後的內容:(?<!某某字符串)
匹配非某某字符串之前的內容:(?! 某某字符串)
比如我們想把APA格式中出現的文章標題全部提取出來。
文章標題相對而言並沒有什麼規律可言,空格,數字,英文字母都可能出現。但是,APA格式的文章標題卻都出現在「(2020)」 這樣的年份之後,並且以標點符號(一般為「.」 或者「?」)結尾。這種規律使得我們可以通過零寬斷言來提取標題。
比如我們需要從下列10個APA格式的參考文獻中提取標題。
首先讀入數據:
papers1 <- readLines("paper.txt")正則如下:
pattern2 <- "(?<=\\d{4}\\)\\.\\s)(.+?)(?=[\\.|\\?])"解讀:該正則分為三個組
第一組:表示4個數字後有一個反括號(年份及反括號,注意轉義符「\\」)然後接一個句點(注意轉義符「\\」)和一個空格「\\s」 。這一組正則的意義如下:
提取」 任意4位數字).空格「後面的字符
第二組:「(.+?)」問號出現在限制元字符之後,表示懶惰模式,匹配除回車鍵換行以外的任意字符(就是標題)。但因為開了懶惰模式,所以還要看第三組正則表達的內容才能決定匹配的字符的長度。
第三組:首先是一組零寬斷言,表示匹配句點「.」或者「?「之前的內容(即文章標題結尾的標點符號)
所以整體上來看,這一組正則表達的意思是:
提取」任意4位數字). 「後面,且第一個」.」或者「?」之前的除了回車鍵換行之外的所有的字符,也就是標題了。
提取的代碼如下:
str_extract_all(papers1,pattern = pattern2) %>% unlist()
結果如下:
3.1 使用Rebus 包
可能各位看官也注意到了,正則的一大缺點就是可讀性實在感人。如果不加注釋,可能幾天之後就完全忘了之前寫的正則是什麼意思了。這樣給後續的修改工作帶來了極大的麻煩。為了讓人更好理解正則表達的意思,有人將正則表達式的元字符和特殊字符全部函數化(也就是變成了R語言的一個function)也就造就了Rebus 包。Rebus包可以通過install.packages或者在Rstudio中的package面板中點擊install,找到Rebus之後直接進行安裝。
Rebus和正則的具體教程(取自datacamp 的官方slides)如下:
連結:https://pan.baidu.com/s/1SX7FbC57PhFScObc73OwdQ
提取碼:hzwz
但是,Rebus包也有一些缺點,比如函數太繁雜,掌握這些函數的用法非常花時間等等。
3.2 使用glue 包
相對於Rebus, 我更推薦使用glue包來編寫正則。glue包提供了關於粘貼和注釋字符的強大解決方案,實際上許多R包的底層代碼中(特別是要輸出某些內容時)都會用到glue包。glue包的其他用法見:
https://github.com/tidyverse/glue
本文接下來主要講述如何配合glue包使用正則。
以剛才的APA格式抽取文章標題為例,使用glue包再寫一遍正則,代碼如下:
pattern2 <- glue::glue_collapse(c( "year"="(?<=\\d{4}\\)\\.\\s)", "title"="(.+?)", "punctuation_once"="(?=[\\.|\\?])"))這裡主要用到了glue_collapse這個函數,它能同時把正則拆分成了若干個向量值,每個向量值都有注釋,比如正則的第一個組表示匹配「year」 (可以自己決定寫什麼樣的注釋)。然後再把這個向量拼起來成一個完整的正則。
然後我們使用str_extract_all 再看一下提取結果:
str_extract_all(papers1,pattern = pattern2) %>% unlist()
由上例可以看出,通過使用glue包,能更加清楚的標明正則每個部分的功能,方便後續修改和閱讀。
3.3 tidyverse 全家桶中與正則相關的函數
其實大家平時常用的tidyverse全家桶中就有許多封裝了正則相關的函數。
比如常用的dplyr包中的filter,select就可以通過str_detect,start_with,end_with等函數作為參數來選取特定內容,詳情見:
https://www.codenong.com/22850026/
再如,tidyr包中的unite 和separate 函數,它們可以通過正則捕捉數據中出現的特定字符串,並按照這些字符串將多列數據合併為一列,或者將一列數據分裂為多列。詳情見:
https://tidyr.tidyverse.org/reference/unite.html
歡迎大家在評論區補充其他類似的函數!
無論是專門從事文本類的數據分析,還是其他類型的數據分析,實踐中經常會有從不規則的文本中提取結構化數據的需求。而正則可以專門用來應對這樣的需求,而且還是可遷移的知識(幾乎每種程式語言都包含正則)。總之一句話,學了不虧。
參考文獻:
Munzert, S., Rubba, C., Meißner, P., & Nyhuis, D. (2014). Automated data collection with R: A practical guide to web scraping and text mining. John Wiley & Sons.
獲取名人名言和APA格式提取樣例
請在後臺回復(hzwz)