天天看點

如何了解軟體測試學習中的正規表達式?

本文轉載自霍格沃茲測試學院優秀學員Sirius的學習筆記,原文連結: https://ceshiren.com/tag/精華帖

正規表達式學習心得

前言

    一個學習筆記居然會有前言?沒錯,這個是額外增加的,顯得專業一點。

    提起正規表達式,不知道大家第一印象是什麼,可能是強大好用也可能是晦澀難懂。正規表達式在文本進行中相當重要,各大程式設計語言中均有支援,但可能使用起來有細微的差别,該學習筆記中元字元介紹一節不特定于某一個程式設計語言,旨在簡要描述正則本身的基本用法。

    前言中先闡述一下正規表達式到底是個什麼東西,清楚這個概念的可以直接跳過。正規表達式是對字元串操作的一種邏輯公式,就是用事先定義好的一些特定字元、及這些特定字元的組合,組成一個“規則字元串”,這個“規則字元串”用來表達對字元串的一種過濾邏輯。(該概念摘自百度百科,不要問我為啥不用Google)

    看完上面的解釋,我的第一反應是有點似懂非懂。

    個人了解如下:某個大佬為了從字元串中比對或找出符合特定規律(如手機号、身份證号)的子字元串,先定義了一些通用符号來表示字元串中各個類型的元素(如數字用 d 表示),再将它們組合起來得到了一個模闆(如:dd模闆就是指代兩個數字),拿這個模闆去字元串中比對,找出符合該模闆的子字元串。

簡單的例子

    了解了什麼是正規表達式後,在由幾個例子去進一步了解。

現在有一個字元串為:

I am a tester, and My job is to test some software.

test是一個正規表達式,它的比對情況:I am a tester, and My job is to test some software. 它既可以比對tester中的test,又可以比對第二個test。正規表達式中的test就代表test這個單詞本身。

btestb是一個正規表達式,它的比對情況:I am a tester, and My job is to test some software. 它隻能比對第二個test。因為b具有特殊意義,指代的是單詞的開頭或結尾。故tester中的test就不符合該模式。

testw是一個正規表達式,它的比對情況:I am a tester, and My job is to test some software. 它比對出了tester,也比對出了第二個test。其中w的意思是比對字母數字下劃線,表示的是數量,指有0個或多個w。是以這個正則表達是的意思就是比對開頭為test,後續跟着0個及以上字母數字下劃線的子字元串

testw+是一個正規表達式,它的比對情況:I am a tester, and My job is to test some software. 它隻比對了tester。因為+與*不同,+的意思是1個或多個,是以該正規表達式比對的是開頭為test,後續跟着1個及以上字母數字下劃線的字元串。

    

    通過上述幾個例子,應該可以看出正規表達式的工作方式,正規表達式由一般字元和元字元組成,一般字元就是例子中的‘test’,其指代的意思就是字元本身,t比對的就是字母t;元字元就是例子中有特殊含義的字元,如w, b, *, +等。後續介紹一些基礎的元字元。

元字元介紹

元字元有很多,不同元字元有不同的作用,大緻可以分為如下幾類。

用于表示意義

    有些元字元專門用來指代字元串中的元素類型,常用的如下:

    通過上述表格中的資料可以發現,w,d,s都有一個與之相反的元字元(将對應字母大寫後就是了)。w比對所有字母數字下劃線,那麼W就是比對所有不是字母數字下劃線的字元。隻要記住其中3個,另外3個就很好記了。

    乍一看這幾個元字元挺簡單的,但是經常不用的話保不準會忘記,此處分享一下我的記憶方法。我把這幾個元字元都當作是某一個單詞的縮寫(雖然可能就是某個單詞的縮寫,但是沒有找到準确的資料去印證),s是space(空間)的縮寫、d是digit(數字)的縮寫、w是word(可以了解成不是傳統意義上的單詞而是代碼中的變量名,變量名可包含的元素就是字母數字下劃線)的縮寫。好了,看到此處你應該已經熟記了6個元字元了。接下來,n和t平時會經常用到,這個肯定比較熟了,最後一個元字元‘.’可以了解它比對一行中的所有元素,因為遇到換行符後就不再進行比對了(萬事萬物源于一點)。

用于表示數量

    有些元字元用于表示某種元素的數量,如d表示一個數字,當你想表示6位數字怎麼辦?當然可以dddddd ,但确實太麻煩了,為了簡便就需要一些表示數量的元字元,上述可以寫成d{6},元字元詳情如下:

    這幾個元字元還算比較好記。表示0個或多個,+表示1個或多個,這個可能會混淆,或許你可以這麼記,表示1*0=0或多個,+表示1+0=1或多個。?表示0或1個,可以了解成某個人在問你這個類型的元素有還是沒有呀?你回答可能有(1)也可能沒有(0)。

    剩下的三個隻要記住大括号是用來表示數量,後續我們還會看到除了{}外,還有[]和()。它們各有各的作用。

用于表示位置

    有些元字元沒有具體的的比對項,它隻是一個抽象的位置概念,它用來表示字元串中的各個位置。一個字元串的位置可以分成:字元串的開頭或結尾、單詞的開頭或結尾。如字元串‘I am a tester_.’,I前面是字元串的開頭位置,英文句号後面為字元串的結尾位置,每一個word(注意此處指的不是傳統意義上的單詞)前後的位置即為單詞的開頭或結尾,對于‘tester_’來說t前面是單詞開頭,下劃線是單詞結尾。

    其中b在前面的例子中有說過,此處可以以這種方式記憶:b是block(塊)的縮寫,即一個單詞是一塊内容,b是這一塊的邊界。至于另外兩個元字元,暫時沒找到很好的記憶方法(^一個尖角,小荷才露尖尖角?),但應該也不難記。

    此處有個地方要提及一下,所有表示位置的不會實際占用字元。為了了解可以繼續看最上面的第二個例子,btestb最終比對出來了子字元串“test”,而不是“ test ”。

    大家依據目前了解的元字元概念,可以思考一下這個正規表達式^d{6,10}$,和d{6,10}的差別。針對字元串‘12345678‘,第一個和第二個都可以比對出’12345678‘。但是針對字元串’W12345678‘,隻有第二個可以正确比對出’12345678‘,原因在于第一個正規表達式的意思比對一個字元串隻有6-10個數字組成,而第二個正規表達式意思是比對字元串中的6-10個連續數字。

    除了這三個元字元表示位置外,還有零寬斷言、負向零寬斷言也表示位置,後續會詳細介紹。

用于字元轉義

    字元轉義的概念大家肯定不陌生,對于, +等有特殊意義的元字元,假如你想比對5個号應該怎麼寫,{5}嗎?肯定不是,這樣寫是文法錯誤,應該使用将其轉義:*{5}。這樣一來的特殊意義就被給取消了,想要比對的話,也是一樣,再用一個把特殊意義取消掉就好了。

字元集

    前面列出了部分用于表示意義的元字元,但是可能這幾個元字元覆寫的都太廣泛了,想要具體的比對某一類字元。比如就是想比對abcd這四個字元中的某一個,正規表達式當然也是支援的。

    這時候就需要用到第二種括号,中括号[]。比對abcd中的某一個可以寫成[abcd]或者[a-d],意思是比對一個a-d中的任意字元。相反若比對非abcd的任意字元,可以寫成

[^abcd]

意思是比對一個不是abcd的字元。

    括号内也可以寫入不同類型的元素,如[a-d1-7@],表示的是比對一個a-d或1-7或@中的任意字元,

[^a-d1-7@]

則與之相反

分組

    講完中括号後我們可以看一下小括号(),小括号的意思是分組,即小括号内部的所有元字元是一個整體。

    之前有學過表示數量的元字元,但是那個表示的數量都是針對于一個元字元來說的,比如ab+表示的是比對一個a後面跟着1個或多個b的子字元串。

    倘若我們想要比對的是1個或多個ab(如:abababab),此時分組就派上作用了,可以這麼寫:(ab)+。此時ab被綁定為一個整體,後面的數量元字元對這個整體起作用。

分枝條件

    元字元中有一個或運算符,它與大多數程式設計語言類似都是用 | 來表示。它的作用為:Ab|aB表示的是比對Ab或者aB。通過這個例子可以很直覺的了解該元字元的作用。當然它也經常和分組一起使用:(Ab|aB)+c,該正則比對開始為1-N個Ab或aB之後是c的子字元串,如:AbaBc, AbAbAbaBc。

後向引用

    後向引用的使用是依附于分組的,分組的概念之前講過了。

    首先,我們先看一下正規表達式中組号的配置設定方式,此時先看一個用到分組的正規表達式:(ab)?(c|C)d。這個正則的意思大家現在肯定都清楚了。這個正規表達式裡面用到了兩個分組分别是(ab)和(c|C)。正則内部會對所有分組進行組号配置設定,從左向右,第一個分組(ab)的組号是1,第二個分組(c|C)的組号是2。而組号0代表的是整個正規表達式。嘗試過python正則的此處應該有印象,比對對象的group方法傳參為0或不傳則傳回整個正則所比對的結果,傳參為1為第一個分組比對的結果。

    了解了組号配置設定方式後,可以開始解釋後向引用了。後向引用就是将前面某個分組已經比對的資料拿過來用,第一個分組比對的資料用1代替,第二個分組比對的資料用2代替,依次類推。

    似乎不是特别好了解,直接看例子吧,(ab)?(c|C)d2D該正則中2表示的是第二個分組比對到的資料,若第二個分組比對到了c那麼2就是c,反之亦然。是以它能比對到:abcdcD, abCdCD。不能比對:abcdCD, abCdcD。通過這個例子可以了解它的作用了吧。

    當然分組除了有自己的組号外,還可以給它自定義組名。不同程式設計語言中的方式不同,Python中自定義組名的格式為:(?Pexp),Name為你自定義的組名,exp代表任意元字元的組合。後面引用的方法為(?P=name)。是以上面例子可以修改成:(ab)?(?Pc|C)d(?P=CWord)D。

組号配置設定介紹

    上一節簡單的講了一下正規表達式是如何配置設定組号的,但其實還有幾個需要注意的地方。

雖然組号是從左向右進行配置設定,但是掃描兩遍,第一遍先配置設定給未命名的分組,第二遍再配置設定給命名的分組。是以命名後的分組組号會更大

使用(?:exp)可以使一個分組不配置設定組号,如(?:ab)?(c|C)d2D中(ab)就沒有配置設定到組号,而(c|C)組号為1

貪婪與懶惰

    人性是貪婪的,正規表達式與人一樣也是貪婪的。一個正規表達式會盡量多的去比對字元串,如:ab.+c去比對’abccccc’是會将該字元串全部比對出來。但有時候我們隻想要其比對’abcc’,此時怎麼辦呢?需要給正規表達式中表示數量的元字元加一個?變成ab.+?c。此時該正規表達式就變懶了,不會再去比對那麼多,比對到‘abcc’就完事了。

零寬斷言及負向零寬斷言

    這兩個個概念有些不太好了解。正如前面所說這兩個也是表示位置的元字元。從字面意思上了解,零寬代表其沒有寬度,即如之前介紹表示位置的元字元中提到的一樣,不會實際占用字元。斷言是什麼?是assert,是用來判斷條件是True還是False。了解完這兩個詞語的意思後,零寬斷言的概念應該也就能了解了。那麼負向無非就是它的反義詞。

    上面的表格主要看第一列它是什麼格式就好,反正後面的名稱和說明也很難看懂。接下來我來用自己的了解通俗的解釋一下這些概念。

   首先字元串中可以有四種方式确認某個子字元串的位置,如字元串‘BACAB’中有兩個A,A前面是B、A前面不是B、A後面是C、A後面不是C。上述四種條件都能夠比對出唯一一個子字元串A。這個例子大概了解的話就可以往後看了。

(?=exp)中exp指代的是任意元字元的組合,結合具體的例子來了解該元字元的用法,一個正規表達式為A(?=C),它代表的情況就是A後面是C的情況。是以比對出了第一個A,由于該元字元是零寬是以它隻能比對出A而不是AC。

(?<=exp)與上面用法相反,一個正規表達式為(?<=B)A,它代表的情況就是A前面是B的情況。是以比對出了第一個A。如果改成(?<=C)A,則能比對出第二個A。

(?!exp)的例子為:A(?!C),它代表的情況為A後面不是C,是以比對出第二個A。

(?    通過上面四個例子的介紹,應該對于這兩個概念、四個元字元有了了解。了解是重點,記下來也是重點。本人是這樣記下來的,四個元字元的基本格式都是(?),隻不過問号後面的不一樣。分下面兩種情況:

XXX前/後是XXX的話就寫一個=,XXX前/後不是XXX的話就寫一個!。這個和日常用的=和!=差不多。

如果表示的意思是前的話,這個元字元就需要出現在前面且要加一個類似于向前指的箭頭<。如果表示的意思是後的話,就什麼都不需要加。

    通過上面兩個情況的歸納,是不是這四個元字元就都記下來了,上述記憶方法為個人拙見,僅供參考。

總結

    到目前為止,正規表達式的基本内容都介紹完了。但是文中用的例子都比較簡單,隻能幫助你了解概念。如果感興趣或者工作中能用到的話,還需要後續勤加練習。

    你以為文章到總結就結束了?So naive, 當我們知道正規表達式大概是個什麼東西,大概怎麼去用了之後。我再來列舉一個日常工作中的案例,将理論應用到實踐。正規表達式在日常使用中一定是基于某一種程式設計語言的,後面的案例程式設計語言選擇python(因為我目前隻會這個)。

    設想這麼一個場景,在測試過程中需要擷取某個時間段内某個程式的運作情況,進而分析出該程式的穩定性或使用頻率等名額,該程式的日志記錄完備,日志格式固定且已知。這時候最佳的辦法就是從該程式日志中進行相關資訊的擷取。假如該日志内容格式大概如下(注:該日志樣例不是實際項目中的日志檔案,為個人舉例):

2020-02-17 11:04:34 [INFO] 接收到來自IP: 182.168.3.111的通路,通路的認證方式為郵箱:[email protected],擷取資料狀态碼1,擷取資料12931KB
2020-02-17 11:05:34 [INFO] 接收到來:自IP:182.168.3.111的通路,通路的認證方式為手機号:008617626045747,擷取資料狀态碼2,擷取資料0KB
2020-02-17 12:04:34 [WARN] IP:182.168.3.111通路失敗
2020-02-17 11:04:34 [ERROR] 連接配接XXX服務失敗,正在重連。。。。           

    從這個日志中可以看到通路成功的IP及其認證賬号、通路失敗的IP、程式的錯誤資訊。那麼我們怎麼把這些資料給抓取出來呢?抓取的方法肯定有很多,如果此時你第一時間想到了正規表達式,那麼恭喜你,通過閱讀前面的文章,正則已經在你心中留下了痕迹,或者它本來就留有痕迹。

    我們先來分析一下第一條日志,其餘的與此類似,有用的資訊可以分成如下幾個片段:

時間字元串:2020-02-17 11:04:34

日志級别:INFO

IP:182.168.3.111

認證郵箱:[email protected]

狀态碼:1

用戶端擷取到的資料大小:12931KB

上面幾個片段對應的正則為:

時間字元串:d{4}-d{2}-d{2}s*d{2}:d{2}:d{2}

日志級别:[INFO]

IP:(d{1,3}.){3}d{1,3}

認證郵箱:w+@w+.w+

狀态碼:d+

用戶端擷取到的資料大小:d+KB

    上述中某幾個正則其實并不嚴謹,比如IP對應的正則還可以比對出999.999.999.999。嚴謹的正規表達式是((25[0-5]|20-4|[01]?0-9?).){3}(25[0-5]|20-4|[01]?0-9?)。由于該正則太長,加之此處重點在于如何應用,故暫用其寬松版的正規表達式。

    知道了各個字段的正則後,我們可以将它們各自寫成一個分組,分組之間填充上其餘元字元,把比對整行日志的正規表達式寫出來,如下:

(\d{4}-\d{2}-\d{2}\s*\d{2}:\d{2}:\d{2})\s*\[(INFO)\]\s*.*:((\d{1,3}\.){3}\d{1,3}).*:(\w+@\w+\.\w+)\D*(\d+)\D*(\d+)KB           

    現在我們通過這個正規表達式可以抓取出日志檔案中這種格式的日志字元串,再根據組号就可以拿出來對應的資料了。不過根據組号取資料可能會有些含糊不清,或許我們可以給每個分組進行命名(使用python支援的方式),形成如下正規表達式:

(?P<Time>\d{4}-\d{2}-\d{2}\s*\d{2}:\d{2}:\d{2})\s*\[(?P<LogLevel>INFO)\]\s*.*:(?P<IP>(\d{1,3}\.){3}\d{1,3}).*:(?P<Email>\w+@\w+\.\w+)\D*(?P<status>\d+)\D*(?P<data_size>\d+)KB           

    好了現在我們可以很清楚的看到,表示時間的分組命名為Time,依次類推。接下來,我們可以使用上述正規表達式去抓取一行日志,再通過分組的名稱拿到對于的字元串資料了。具體的代碼可以參考下面的樣例:

import re

def reg_deal(pattern_list, text, func_dict=None):
    if func_dict is None:
        func_dict = {}
    for pattern in pattern_list:
        match_obj = re.match(pattern, text)
        if match_obj:
            return {k: func_dict.get(k, lambda x: x)(v) for k, v in match_obj.groupdict().items()}

if __name__ == '__main__':
    email_pattern = r"(?P<Time>\d{4}-\d{2}-\d{2}\s*\d{2}:\d{2}:\d{2})\s*\[(?P<LogLevel>INFO)\]\s*.*:(?P<IP>(\d{1," \
                    r"3}\.){3}\d{1,3}).*:(?P<Email>\w+@\w+\.\w+)\D*(?P<status>\d+)\D*(?P<data_size>\d+)KB"
    phone_pattern = r"(?P<Time>\d{4}-\d{2}-\d{2}\s*\d{2}:\d{2}:\d{2})\s*\[(?P<LogLevel>INFO)\]\s*.*:(?P<IP>(\d{1," \
                    r"3}\.){3}\d{1,3}).*:(?P<Phonenum>((\+|00)86)?1[3-9]\d{9})\D*(?P<status>\d+)\D*(?P<data_size>\d+)KB"
    warn_pattern = r"(?P<Time>\d{4}-\d{2}-\d{2}\s*\d{2}:\d{2}:\d{2})\s*\[(?P<LogLevel>WARN)\]\s*.*:(?P<IP>(\d{1," \
                   r"3}\.){3}\d{1,3}).*"
    error_pattern = r"(?P<Time>\d{4}-\d{2}-\d{2}\s*\d{2}:\d{2}:\d{2})\s*\[(?P<LogLevel>ERROR)\]\s*(?P<ERROR_Message>.*)"
    pattern_list = [email_pattern, phone_pattern, warn_pattern, error_pattern]
    status_dict={
        '1': 'Sucess',
        '2': 'Fail'
    }
    func_dict = {
        'status': lambda x: status_dict[x],
        'data_size': lambda x: int(x)/1024
    }
    result_list = []
    with open('logcontent.log', 'r', encoding='utf-8') as f:
        for data in f:
            result_dict = reg_deal(pattern_list, data, func_dict)
            result_list.append(result_dict)
    print(result_list)           

    代碼中實作了一個函數reg_deal,後面代碼都是對于這個函數的實際應用,該函數入參為:正規表達式組成的清單、待比對的字元串、特殊函數組成的字典。其先循環将字元串與清單中各個正規表達式進行比對,比對成功後得到一個比對對象,調用該比對對象的groupdict函數可以傳回一個結果字典,該結果字典的鍵為分組的名稱,值為分組比對到的值。針對這一結果字典再進行一步特殊函數處理,如上述中的status字段日志中是碼值,但輸出結果需要是具體的漢字。故對其進行了一步碼值轉換操作,對與資料大小将KB轉化成了MB。

    若使用該函數,需自己将正規表達式寫出來并對正規表達式中的分組進行命名,若有些分組資料需要特殊處理,則維護一個特殊函數字典,鍵為分組名,值為函數(匿名函數或者是函數名稱)。将參數傳入後即可獲得結果字典或者None。得到結果字典後具體怎麼處理就看你接下來的發揮啦。

更多技術文章分享及測試資料點此擷取

如何了解軟體測試學習中的正規表達式?
如何了解軟體測試學習中的正規表達式?