自動生成正規表示式

wyzsk發表於2020-08-19
作者: 大學生 · 2015/03/15 12:57

0x00 前言


自動生成正規表示式這個話題其實國外有相關的研究,這次的使用的方法是我按一個論文的思路做實現的時候想到的。所謂的自動生成其實沒有想象的那麼高大上,其實就是使用了一些非常簡單的思路進行組合。文中提到的方法已經使用python進行了實現,效果就我本身提供的幾個二逼的資料來說是不錯的,但是我不好評價真實場景下的效果,還有需要注意的就是我文中提供的程式碼適合生成一些簡單的正規表示式。

正規表示式本身的存在是為了匹配具有某個固定模式的文字,既然目標具有固定的模式,那麼自動生成就是存在可能性的,先思考一個思路,我們寫一個正規表示式的時候我們在思考什麼,我們在思考例子,比如我們想去匹配網址,那麼我們就會思考不同的例子,比如www.baidu.com www.python.org au.fuck.com,都代表了不同的例子,我們會從其中抽取他們共有的模式編寫可以進行匹配的正規表示式語法。

0x01 思考模式


1. 正規表示式


我們需要先去思考正規表示式本身所具有的模式,我們先做一個簡單的假設,假設正規表示式本身具有兩種模式,一種用於匹配具體的字串 比如 “baidu”,“google”,“fuck you”,我們稱為 FullPatten,另外一種用於匹配模仿的字串,比如純數字等等,稱為HalfPatten。

2. 共有模式


所謂的共有,就是都擁有的元素。比如 www.baidu.com www.google.com www.hello.com 。我們可以顯而易見的看出,三個例子共同存在的元素很多,比如都存在 "www.", ".com" 或者點號和 w,c,o,m....可以看出這我們思考的這幾個例子都有一些共同的模式。

3. 丟一起


現在把我們思考出來的模式組合到一起,就是說我們要做的事只是簡單的給計算機輸入幾個栗子,然後讓計算機得出共有的模式去生成我們想要的正規表示式。打個比方,我們現在有幾個栗子:

example = [www.baidu.com, www.google.com, www.hello.com]

然後計算機得出最長的的共有字串:

union = ['www.', '.com']

我們轉化成正規表示式的模式後:

regexPatten = [FullPatten, HalfPatten, FullPatten]

然後迭代生成正規表示式:

re = "www.\w+.com"

好了,本次教程就到這裡。

開個玩笑,這種二逼的東西就怎麼可能浪費我這幾天的美劇時間,只要思考幾個相反的例子我們就會很容易的得出上面基於規則生成的破逼玩意有多麼坑爹。比如如果我們想匹配 “www.au.baidu.com” 比如 我們想匹配 .com 和 .au 結尾的怎麼辦,或者我們只是想匹配所有的url?下面我們就需要去解決這個問題。

4. 資訊熵


當年夏農大神提出了資訊熵這個概念用於描述事物的不確定性,什麼是不確定性,比如1+1=?對於學過加法的你來說它具有不確定性嗎?沒有,那麼他的資訊熵就是0。如果問的是 你明天會不會在公司樓下碰見美女,它具有不確定性麼?有,很大。。。。。

0x02 資料的多樣性


上面的機制的缺點在於沒有考慮到資料的多樣化,和外面的世界太危險。我們先思考HalfPatten然後將其寫成模組,那麼現在我們先思考HalfPatten的價值,就是用於匹配模糊的資料,比如 數字 \d ,字母\w 任意字元 . ,有一個問題,所謂的模糊資料是所有的,還是有固定長度的。比如上面的模糊字串 google,hello,baidu 我們是匹配長度為 5到6的還是 直接任意字元 ?

我們需要一種簡單暴力的方式可以描述資料的不確定性,就是我剛才說的資訊熵。比如我們有一些需要模糊匹配的數字長度為 length= [5,6,5,6],那麼它的資訊熵就是 (1/2) · log(1/2) – (1/2) · log(1/2) ≈ 0.693 又或者說我們有 5,5,5,5這有的數字長度,那麼他的shan就是0. 接下來我們轉化成程式碼。

#!python
from math import log
def shan(x):
    shan = 0.
    for i in set(x):
        shan -= (float(x.count(i))/len(x))*log(float(x.count(i))/len(x))
    return shan

print shan([5,6,5,6])
print shan([5,5,5,5])

out:

0.69314718056
0.0

1. HalfPatten


現在我們可以來寫HalfPatten模組了。我們可以從下面的程式碼看出 halfPatten其實也存在 all zone length這三種模式,比如我們檢測長度的資訊熵為0那麼我們只需要單純的返回 \d{長度} 同時我們加入了一個 熵引數shan,只要長度的資訊熵超過這個值就說明資料不確定性太大,匹配任意長度就行,低於這個值 我們就只需要匹配某個區間的長度就行。

#!python
class HalfPatten:

    def __init__(self,x):
        self.x = x
        self.type = "halfPatten"
        self.shan = 0.
        self.regShan= 0.
        self.x_length = [len(i) for i in x]
        for i in set(self.x_length):
            self.shan -= (float(self.x_length.count(i))/len(self.x_length))*log(float(self.x_length.count(i))/len(self.x_length))
        for c in set(self.x):
            self.regShan -= (float(self.x.count(c))/len(self.x))*log(float(self.x.count(c))/len(self.x))


    def detection(self,string):
        s = ''.join(string)
        if len(s)==0:
            return ""
        if s.isalnum():
            if s.isdigit():
                return "\d"
            return "\w"
        else:
            return "."

    def gener(self,patten):
        if patten=="all":
            return "%s+"%(self.detection(self.x))
        elif patten=="zone":
            x_len = self.x_length
            x_len.sort()

            return "%s{%i,%i}"%(self.detection(self.x),x_len[0],x_len[-1])
        else:
            return "%s{%i}"%(self.detection(self.x),self.x_length[0])

    def regex(self,shanRule):
        if self.detection(self.x)=="":
            return ""
        if self.shan>0:
            if self.shan>shanRule:
                return self.gener("all")
            else:
                return self.gener("zone")
        else:
            return self.gener("length")

def main(shan=1.):
    exmple = ['123','123']
    exmple2 = ['123','1234']
    half = HalfPatten(exmple)
    half2 = HalfPatten(exmple2)
    print half.regex(shan)
    print half2.regex(shan)

if __name__ =="__main__":
    main()
    main(0.)

out:

\d{3}
\d{3,4}
\d{3}
\d+

我們可以看到我們得出了一個非常不錯的結果,那麼接下來我們需要來寫FullPatten模組。

2. FullPatten


fullPatten其實相對於half來說要簡單了許多,我們只需要根據確定性的字串生成對應的正規表示式就好了。需要注意的是所謂的確定性其實也有不同的模式, 比如 我們想匹配 .com 和 .cn結尾的url而不想要其他的東西。現在寫成程式碼:

#!python
class FullPatten:

    def __init__(self,x):
        self.x = x
        self.type = "fullPatten"
        self.shan = 0.
        self.regShan = 0.

    def gener(self,patten):
        if patten=="all":
            return "(%s)"%(self.x[0])
        else:
            return "(%s)"%('|'.join(list(set(self.x))))

    def regex(self,shanRule):
        if len(set(self.x))>1:
            return self.gener("half")
        else:
            return self.gener("all")

def main():
    example = ['.fuck','.fuck']
    example1 = ['.com','.cn']
    full = FullPatten(example)
    full1 = FullPatten(example1)
    print full.regex(example)
    print full1.regex(example1)

if __name__ == "__main__":
    main()

out:

(.fuck)
(.com|.cn)

接下來我們來把這兩個模組組合到一起。

3. 組合


在把這幾個玩意丟一起之前我們需要先思考我們要的是什麼,我們需要一個模組,能夠根據我們提供的確定性的字串對資料分割成序列的形式。寫成程式碼:

#!python
class strspl:

    def __init__(self,y,shan):
        self.sentence = []
        self.y = y
        self.shan = shan

    def re_split(self,string):
        s = []
        for x in self.y:
            s.append([x[:x.index(string)],string,x[x.index(string)+len(string):]])
        half_1 = []
        full_1 = []
        half_2 = []
        for q,w,e in s:
            half_1.append(q)
            full_1.append(w)
            half_2.append(e)
        self.sentence.append(HalfPatten(half_1))
        self.sentence.append(FullPatten(full_1))
        self.sentence.append(HalfPatten(half_2))

        for l,i in enumerate(self.sentence):
            if i.shan!=0.:
                if i.regShan<self.shan:
                    self.sentence[l]=FullPatten(i.x)

def main(shan=1.5):
    example = ['asb.baidu.go','ww.baidu.com','www.baidu.fuck']
    a = strspl(example,shan)
    a.re_split('.baidu.')
    sentence=[]
    regex = []
    for i in a.sentence:
        sentence.append(i.type)
        regex.append(i.regex(0.))
    return sentence,''.join(regex)

if __name__ == "__main__":
    s,r = main()
    print s
    print r
    s,r = main(0.)
    print s
    print r

out:

['fullPatten', 'fullPatten', 'fullPatten']
(ww|asb|www)(.baidu.)(go|com|fuck)
['halfPatten', 'fullPatten', 'halfPatten']
\w+(.baidu.)\w+

我們可以看到我們在模組中又提供了一個引數,這個引數在於決定,.com 和 .cn 是full還是half,我們可以看到我們對儲存著 型別序列的sentence進行了一次迭代,抽取其中的regShan進行判斷,regShan與剛才的長度不同,由資料本身的的差異得出,由於full的regShan寫死了0.所以其實是判斷half的。目的在於選定一個值來判斷 這個型別是否需要改為full型別。

4. 丟一起


現在我們需要把他組成一個成品了,現在我們已經有了各種模組,我們就還需要一個能夠自動提取共有模式並且轉換的模組,我們可以從上面的程式碼看到,演算法切割資料的方式是找到第一個匹配的字串然後進行切割,那麼,比如這樣的資料 www.baidu.com www.python.com.ho www.sb.com.as 我們經過切割後會變成 [www., baidu.com] [www., python.com] [www, .sb.com.as] 會被轉化成類似 www..{7,8} 類似的玩意,但是我們知道剩下的字串還存在可以被提取的共有模式就是.com,所以我們設定一個迭代值iter,讓機器能夠多次提取共有模式,為了讓我們能夠更好的調整演算法的引數,在有多個halfPatten的情況下,每次機器只會對其中一個halfPatten進行提取,每次迭代演算法會選取其中 regShan最大的進行再提取,就是說,不確定性更大的資料其中存在共有模式的可能性越高,but一但其中提取不到共有模式就會自動退出迴圈。寫成程式碼:

#!python
class Auto:


    def __init__(self,x,shan=.3,regShan=.8):
        self.x = x
        self.sentence = []
        self.U = ReU()
        self.shan = shan
        self.regShan = regShan

    def __guess(self,x,y):
        if y in x:
            return 1
        return 0


    def generation(self,iter=1):
        x = self.x
        location = 0.
        for i in range(iter):
            if len(self.sentence)==0:
                str = strspl(self.x,self.regShan)
                di = {}
                di['data']= x
                frame = pd.DataFrame(di)
                #這個proccess是我之前寫的一個坑爹的模組,其實就是提取共有的字串,然後key = sorted(x,key=len,reverse=True) return key[0]
                key = self.U.proccess(frame,'data')
                sen = str.re_split(key)
                self.sentence  = str.sentence
                continue
            print "counting %i"%(i)
            chanceShan = 0.
            chance = None
            for l,i in enumerate(self.sentence):
                if i.regShan !=0.:
                    if chanceShan<i.regShan:
                        chance = l
                        chanceShan = i.regShan
            if chance!=None:

                di={}
                di['data']=self.sentence[chance].x
                str = strspl(di['data'],self.regShan)
                frame = pd.DataFrame(di)
                try:
                    key = self.U.proccess(frame,'data')
                except IndexError,e:
                    break
                sen = str.re_split(key)
                self.sentence = self.sentence[:chance]+str.sentence+self.sentence[chance+1:]


    def build(self):
        reg = []
        shan = []
        for i in self.sentence:
            reg.append(i.regex(self.shan))
            shan.append(i.regShan)
        return shan,"".join(reg)

def main(shan=.7,regShan=.3,it=1):
    s = ['www.asb.baids.com','www.ww.baidu.com','www.www.baidu.com']
    a = Auto(s,shan,regShan)
    a.generation(it)
    shan,reg =  a.build()
    print shan
    print reg


if __name__=="__main__":
    main()
    main(0,0,)
    main(0,1.5)
    main(0,1.5,it=2)
    main(it=2)

out:

[1.0986122886681096, 0.0, 0.6365141682948128]
.{6,7}(.bai).{6}
[1.0986122886681096, 0.0, 0.6365141682948128]
.+(.bai).{6}
[0.0, 0.0, 0.6365141682948128]
(www.ww|www.www|www.asb)(.bai).{6}
counting 1
[0.0, 0.0, 0.6365141682948128, 0.0, 0.0]
(www.ww|www.www|www.asb)(.bai)\w{2}(.com)
counting 1
[0.0, 0.0, 1.0986122886681096, 0.0, 0.6365141682948128]
(www.)\w{2,3}(.bai).{6}

我們可以看到,三個引數的不同可以導致生成不同型別的正規表示式。其實到這裡差不多就結束了,這裡的程式碼其實在於提供一種思路,裡面的邏輯非常簡單,關鍵在於我們可以透過類似資訊熵這種可以量化不確定性的方式來生成正規表示式,整個演算法思路其實還有很多需要改進的地方。不過還是有另外一點需要講.

5. 選取最佳


我們可以看到上面的程式碼必要時候需要我們自己調整引數,再資料較為簡單的情況下,不同引數的生成的正則其實是可以被列舉完的,我們就需要一種可以度量效能的公式。

enter image description here

函式R 表示正規表示式匹配,t表示要匹配的文字 R(t)表示,正規表示式匹配後的值,s表示要匹配的值,函式d表示編輯距離。這樣我們就可以度量他的效能,列舉列舉所有的可能性並選取最小值。

0x03 結語


幾個事說下,如果有幸你打算把這個方法用在專案中請重寫程式碼,我文中的程式碼是為了快速實現而寫的,其中有很多需要最佳化的地方,支援的正則符號也不多,主要還是思路本身的可行性。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章