衆裏尋她千百度--正則表達式

原文地址html

先來看一個讓人震撼的小故事,故事來自知乎問題PC用戶的哪些行爲讓你當時就震驚了?python

同窗在一個化妝品公司上班,旁邊一個大媽(四十多歲)發給他一個exl表,讓他在裏面幫忙找一個經銷商的資料。
表格裏面大約有幾百個客戶資料,我同窗直接篩選填入信息,而後沒找到,就轉頭告訴大媽,說這個表裏沒有。
大媽很嚴厲的批評了我同窗,說年輕人幹工做必定要沉的住氣,心浮氣躁可不行。這才幾分鐘啊,我纔看了二十行,你怎麼就找完了。
同窗過去一看,大媽在一行一行的精挑細選,頓時一身冷汗。把篩選辦法告知後,大媽不但不領情,還召集辦公司其餘老職員,一塊兒聲討我同窗,咱們平時都是這麼找的,你確定是偷工減料,咱們找一個小時沒找完,你幾分鐘就找完了。git

不知道是否確有此事,不過看起來好嚇人的樣子。仔細想一想,大多數人都是用以往的經驗來分析碰見的新問題的。就上面的大媽而言,在接觸計算機以前的幾十年裏,她面對的都是紙質的客戶資料,此時,要查找某一客戶資料,只能一行一行看下去了。github

如今,雖然有了計算機,可是隻是簡單的把它看作一個比較大的紙質資料庫罷了,並無認識到計算機的強大之處。這裏的強大主要就是說計算機在處理電子文檔時的強大的搜索功能了。正則表達式

固然,對於大部分年輕人來講,計算機中的搜索功能是再熟悉不過了。咱們能夠在word、excel、網頁中搜索特定內容,能夠在整個計算機文件系統中搜索文件名,甚至搜索文件中的內容(Win下的everthing,Mac下的Spotlight)。算法

這些搜索主要用到了兩種技術:數據庫

  1. 正則表達式express

  2. 數據庫索引編程

這裏咱們先介紹一下正則表達式。數組

正則表達式介紹

簡單來講,正則表達式就是用來匹配特定內容的字符串。舉個例子來說,若是我想找出由a、b組成的,以abb結尾的字符串,好比ababb,那麼用正則表達式來表示就是[ab]*abb

正則表達的理念是由數學家Stephen Kleene在1950年首次提出來的,開始時主要用於UNIX下文本編輯器ed和過濾器grep中。1968年開始普遍應用於文本編輯器中的模式匹配和編譯器中的詞法分析。1980年,一些複雜的正則表達語句開始出如今Perl中,使用了由Henry Spencer實現的正則表達解析器。而Henry Spencer後來寫了更高效的正則解析器Tcl,Tcl混合使用了NFA(非肯定有限自動機)/DFA(肯定有限自動機)來實現正則表達語法。

正則表達式有如下優勢:

  • 容易理解

  • 能高效實現

  • 具備堅實的理論基礎

正則表達式的語法十分簡單,雖然各類編程語言在正則表達式的語法上有細節上的區別,不過主要部分以下:

  1. [a-z]表示全部小寫字母,[0-9]表示全部數字,[amk]表示a、m或k。

  2. +表示字符重複1或者屢次,*表示字符重複0或者屢次。在使用+或者*時,正則表達式聽從maximal munch的原則,也就是說它匹配可以匹配到的最大字符串。

  3. a|z 表示匹配字符'a'或者'z'

  4. ?表示字符出現0次或者1次

  5. \是正則表達式中的escape符號,\\*表示的就是'*'這個字符,而不是它在正則表達式中的功能。

  6. . 表示出了換行符以外的任何字符,而^表示出了緊接它的字符之外的任何字符

  7. ^ 匹配字符串的開始,$ 匹配字符串的結尾。

回到咱們前面的例子中,咱們用正則表達式[ab]*abb來匹配由a、b組成的,以abb結尾的字符串。這裏[ab]*abb便可以這樣解讀:a或者b重複0或者屢次,而後是abb的字符串

下面用python在"aababbaxz abcabb abbbbabb"中搜索[ab]*abb

import re
content = "aababbaxz abcabb abbbbabb"
pattern = re.compile("[a|b]*abb")
print pattern.findall(content)
# outputs: ['aababb', 'abb', 'abbbbabb']

其實,正則表達式不僅用於文本搜索和模糊匹配,還能夠用於如下場景:

  1. 合法性檢查

  2. 文本的自動更正和編輯

  3. 信息提取

正則表達式實現原理

正則表達式便於咱們理解使用,可是如何讓計算機識別用正則表達式描述的語言呢?仍然之前面的[a|b]*abb爲例,計算機如何識別[a|b]*abb的意義呢?首先咱們來看判斷輸入內容是否匹配正則表達式的流程圖:

肯定有限自動機

圖中一共有4個狀態S0, S1, S2, S3,在每一個狀態基礎上輸入字符a或者b就會進入下一個狀態。若是通過一系列輸入,最終若是能達到狀態S3,則輸入內容必定知足正則表達式[a|b]*abb。

爲了更清晰表述問題,將上圖轉換爲狀態轉換表,第一列爲當前狀態,第二列爲輸入a後當前狀態的跳轉,第三列爲輸入b後當前狀態的跳轉。其中S0爲起始狀態,S3爲接受狀態,從起始狀態起通過一系列輸入到達接受狀態,那麼輸入內容即知足[a|b]*abb。

狀態 a b
S0 S1 S0
S1 S1 S2
S2 S1 S3
S3 S1 S0

其實上圖就是一個DFA實例(肯定有限自動機),下面給出DFA較爲嚴格的定義。一個肯定的有窮自動機(DFA) M 是一個五元組:M = (K, ∑, f, S, Z),其中:

  1. K是一個有窮集,它的每一個元素稱爲一個狀態;

  2. ∑是一個有窮字母表,它的每一個元素稱爲一個輸入符號,因此也稱∑爲輸入符號表;

  3. f是轉換函數,是在K×∑→K上的映射,如f(ki, a)→kj,ki∈K,kj∈K就意味着當前狀態爲ki,輸入符號爲a時,將轉換爲下一個狀態kj,咱們將kj稱做ki的一個後繼狀態;

  4. S∈K是惟一的一個初態;

  5. Z⊆K是一個狀態集,爲可接受狀態或者結束狀態。

DFA的肯定性表如今轉換函數f:K×∑→K是一個單值函數,也就是說對任何狀態ki∈K和輸入符號a∈∑,f(k, a)惟一地肯定了下一個狀態,所以DFA很容易用程序來模擬。

下面用字典實現[a|b]*abb的肯定有限自動機,而後判斷輸入字符串是否知足正則表達式。

DFA_func = {0: {"a": 1, "b": 0},
            1: {"a": 1, "b": 2},
            2: {"a": 1, "b": 3},
            3: {"a": 1, "b": 0}
            }
input_symbol = ["a", "b"]
current_state = 0
accept_state = 3

strings = ["ababaaabb",
           "ababcaabb",
           "abbab"]
for string in strings:
    for char in string:
        if char not in input_symbol:
            break
        else:
            current_state = DFA_func[current_state][char]

    if current_state == 3:
        print string, "---> Match!"
    else:
        print string, "--->No match!"
    current_state = 0
    """outputs:
    ababaaabb ---> Match!
    ababcaabb --->No match!
    abbab --->No match!
    """

上面的例子能夠看出DFA識別語言簡單直接,便於用程序實現,可是DFA較難從正則表達式直接轉換。若是咱們能找到一種表達方式,用以鏈接正則表達式和DFA,那麼就可讓計算機識別正則表達式了。事實上,確實有這麼一種表達方式,能夠做爲正則表達式和DFA的橋樑,並且很相似DFA,那就是非肯定有限自動機(NFA)。

仍是上面的例子,若是用NFA表示流程圖,就以下圖所示:

肯定有限自動機

看上去很直觀,頗有[a|b]*abb的神韻。它轉換爲狀態轉換表以下:

狀態 a b
S0 S0, S1 S0
S1 Φ S2
S2 Φ S3
S3 Φ Φ

NFA的定義與DFA區別不大,M = (K, ∑, f, S, Z),其中:

  1. K是一個有窮集,它的每一個元素稱爲一個狀態;

  2. ∑是一個有窮字母表,它的每一個元素稱爲一個輸入符號,ε表示輸入爲空,且ε不存在於∑;

  3. f是轉換函數,是在K×∑*→K上的映射,∑*說明存在遇到ε的狀況,f(ki, a)是一個多值函數;

  4. S∈K是惟一的一個初態;

  5. Z⊆K是一個狀態集,爲可接受狀態或者結束狀態。

數學上已經證實:

  1. DFA,NFA和正則表達式三者的描述能力是同樣的。

  2. 正則表達式能夠轉換爲NFA,已經有成熟的算法實現這一轉換。

  3. NFA能夠轉換爲DFA,也有完美的實現。

這裏不作過多陳述,想了解詳情能夠參考《編譯原理》一書。至此,計算機識別正則表達式的過程能夠簡化爲:正則表達式→NFA→DFA。不過有時候NFA轉換爲DFA可能致使狀態空間的指數增加,所以直接用NFA識別正則表達式。

正則表達式應用實例

前面已經使用python的re模塊,簡單展現了正則表達式[ab]*abb的匹配過程。下面將結合幾個經常使用的正則表達式例子,展現正則表達式的強大之處。

開始以前,先來看下python中正則表達的一些規定。

  1. \w 匹配單詞字符,即[a-zA-Z0-9_],\W 則剛好相反,匹配[^a-zA-Z0-9_];

  2. \s 匹配單個的空白字符:space, newline(\n), return(\r), tab(\t), form(\f),即[ \n\r\t\f\v],\S 相反。

  3. \d 匹配數字[0-9],\D 剛好相反,匹配[^0-9]

  4. (...) 會產生一個分組,在後面須要時能夠用數組下標引用。

  5. (?P<name>...) 會產生命名組,須要時直接用名字引用。

  6. (?!...) 當...不出現時匹配,這叫作後向界定符

  7. r"pattern" 此時pattern爲原始字符串,其中的"\"不作特殊處理,r"\n" 匹配包含"\"和"n"兩個字符的字符串,而不是匹配新行。當一個字符串是原始類型時,Python編譯器不會對其嘗試作任何的替換。關於原始字符串更多的內容能夠看stackflow上問題Python regex - r prefix

python中經常使用到的正則表達式函數主要有re.search, re.match, re.findall, re.sub, re.split

  1. re.findall: 返回全部匹配搜索模式的字符串組成的列表;

  2. re.search: 搜索字符串直到找到匹配模式的字符串,而後返回一個re.MatchObject對象,不然返回None;

  3. re.match: 若是從頭開始的一段字符匹配搜索模式,返回re.MatchObject對象,不然返回None。

  4. re.sub(pattern, repl, string, count=0, flags=0): 返回repl替換pattern後的字符串。

  5. re.split: 在pattern出現的地方分割字符串。

re.search和re.match都可指定開始搜索和結束搜索的位置,即re.search(string[, pos[, endpos]])和re.match(string[, pos[, endpos]]),此時從pos搜索到endpos。須要注意的是,match老是從起始位置匹配,而search則從起始位置掃描直到遇到匹配。

re.MatchObject默認有一個boolean值True,match()和search()在沒有找到匹配時均返回None,所以能夠用簡單的if語句判斷是否匹配。

match = re.search(pattern, string)
if match:
    process(match)

re.MatchObject對象主要有如下方法:group([group1, ...])和groups([default])。group返回一個或多個分組,groups返回包含全部分組的元組。

例子1:匹配Hello,當且僅當後面沒有緊跟着World。

strings = ["HelloWorld!",
           "Hello World!"]
import re
pattern = re.compile(r"Hello(?!World).*")
for string in strings:
    result = pattern.search(string)
    if result:
        print string, "> ", result.group()
    else:
        print string, "> ", "Not match"
'''
outputs:
HelloWorld! >  Not match
Hello World! >  Hello World!
'''

例子2:匹配郵箱地址。目前沒有能夠完美表達郵箱地址的正則表達式,能夠看stackflow上問題Using a regular expression to validate an email address 。這裏咱們用[\w.-]+@[\w-]+\.[\w.-]+來簡單地匹配郵箱地址。

content = """
          alice@google.com
          alice-bob@gmail.._com gmail
          alice.bob@apple.com apple
          alice.bob@gmailcom invalid gmail
          """
import re
address = re.compile(r'[\w.-]+@[\w-]+\.[\w.-]+')
print address.findall(content)
'''
outpus:
['alice@google.com', 'alice-bob@gmail.._com', 'alice.bob@apple.com']
'''

例子3:給函數添加裝飾器。

original = """
def runaway():
    print "running away..."
"""
import re
pattern = re.compile(r"def (\w+\(\):)")
wrapped = pattern.sub(r"@get_car\ndef \1", original)
print original, "--->", wrapped, "----"
"""outputs
def runaway():
    print "running away..."
--->
@get_car
def runaway():
    print "running away..."
----
"""

看起來正則表達式彷佛無所不能,可是並非全部的場合都適合用正則表達式,許多狀況下咱們能夠找到替代的工具。好比咱們想解析一個html網頁,這時候應該使用使用 HTML 解析器,stackflow上有一個答案告訴你此時爲何不要使用正則表達式。python有不少html解析器,好比:

  • BeautifulSoup 是一個流行的第三方庫

  • lxml 是一個功能齊全基於 c 的快速的庫

參考
Wiki: Regular expression
正則表達式和有限狀態機
Python Regular Expressions
Python check for valid email address?
Python正則表達式的七個使用範例
高級正則表達式技術
編譯原理: 有窮自動機

相關文章
相關標籤/搜索