【C 語言】使用 Regular Expressions

挖了個坑給自己跳,為啥我會這想不開,要在 C 上寫 regexp?
不過最大的坑應該是…我幹麻選 C 阿,都 4、5 年沒碰了。


regex.h 函式庫

用 C 來寫正規表示式第一個碰到的問題是,C 的標準函式庫中並沒有支援正規表示式

不過還好我的開發環境是 Linux,在現行 Linux 中大多有安裝 POSIX.2,所以可以直接引入 <regex.h> 來使用,但如果是 Windows 就得自食其力了。

看了下它的原始碼還挺簡單的,抽掉細節跟註解,比較常用的也就 4 個函式與一些常數(根據 felix021 的網誌說明,整份文件有 2 個類別、4 個函式和 7 個常數)。

在使用 regex.h 進行開發時,一般會有三個步驟:編譯匹配釋放,三個步驟分別對應到四個常用函式中的其中三個:regcomp()regexec()regfree(),剩下一個是錯誤處理 regerror()



編譯:regcomp()

這個是把指定的表示式 pattern 編譯成特定的資料格式 regex_t ,在下一個階段會使用編譯後的結果 preg 進行匹配。

1
int regcomp(regex_t *preg, const char *pattern, int cflags)

這個函式有三的參數,分別是:

  1. preg: 它的資料格式是 regex_t,用來存放編譯後的結果,所以傳進去的是指標。
  2. pattern:這邊傳進我們的表示式,也是傳指標。除此之外,它是個常數。
  3. cflags: 這部份是一些參數的設定,可傳入一個或多個(用|串接)值,可使用的值有:
    1. REG_EXTENDED:使用 ERE(Extended Regular Expressions,擴展型正規表示式)模式進行匹配。
    2. REG_ICASE:忽略字母大小寫。
    3. REG_NOSUB:僅回報匹配成功或失敗。
    4. REG_NEWLINE: 識別換行符號。

      思考了下,我應該會開 ERE,因為我會的流派應該算是 PCRE (Perl Compatible Regular Expressions)一派,跟 BRE(Basic Regular Expression,基本型正規表示式)[差異](http://www.greenend.org.uk/rjk/tech/regexp.html)看起來頗多,相較之下 ERE 還稍微相近一點,不過還是有些差異。


回傳值的部份,編譯成功會回傳 0,否則傳回其他值。來個片段看一下:

1
2
3
4
5
6
// 比對電子信箱
regex_t preg; // 宣告編譯結果變數
const char* pattern = "^[a-z0-9_]+@([a-z0-9-]+\.)+[a-z0-9]+$"; // 定義表示式
// 編譯,這邊使用 ERE,且不考慮大小寫
int success = regcomp(&reg, pattern, REG_EXTENDED|REG_ICASE);
assert(success==0);

是說 POSIX 體系,沒有 \d\w 可用,只能寫成 [a-z0-9]



匹配:regexec()

編譯好後就可以把目標字串放進去匹配了。

1
2
int regexec (regex_t *preg, char *target, size_t nmatch, 
             regmatch_t matchptr [], int eflags)


先提一下 regmatch_t 這個資料格式,在程式碼中它是長這樣:

1
2
3
4
typedef struct{
    regoff_t rm_so;  /* Byte offset from string's start to substring's start.  */
    regoff_t rm_eo;  /* Byte offset from string's start to substring's end.  */
} regmatch_t;

其中的 rm_so 是用來記錄匹配結果在字串中的起始位置,rm_eo 是結束位置。一般會將它宣告為陣列,index 為 0 存放 full match,之後存放 group,陣列長度會決定記錄多少的 group。


我們來看下要傳啥參數進去,

  1. preg:這就是在上一個階段使用 regcomp 編譯完的 preg,直接丟進來就好。
  2. target:我們的目標字串。
  3. nmatchmatchptrmatchptr 就是上面所提過 regmatch_t 陣列。nmatch 則是 matchptr 的長度。
  4. eflags:有兩個值 REG_NOTBOLREG_NOTEOL


繼續我們的 Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
char* target = "testmail_10@gmail.com";   //目標字串
regmatch_t matchptr[1];   // 紀錄匹配結果陣列,長度為1僅紀錄 full match
const size_t nmatch = 1;    //  matchptr陣列長度
int status = regexec(&preg, target, nmatch, matchptr, 0); //匹配
if (status == REG_NOMATCH){ // 沒匹配
    printf("No Match\n");
}
else if (status == 0){  // 匹配
    printf("Match\n");
    // 取出起始與結束位置印出字串
    for (int i = matchptr[0].rm_so; i < matchptr[0].rm_eo; i++){  
        printf("%c", target[i]);
    }
    printf("\n");
}



釋放:regfree()

把空間釋放放掉。

1
void regfree (regex_t *preg)

在使用完畢或要重編譯新的表示式前,調用這個清空 preg 指向的 regex_t 的内容。

1
regfree(&reg);



錯誤處理: regerror()

當執行 regcompregexec 產生錯誤的時候,就可以調用這個函數返回錯誤訊息的字串。

1
2
size_t regerror(int errcode, const regex_t *preg,
    char *errbuf, size_t errbuf_size);

參數的部分:

  1. errcode:就是剛剛 regcompregexec 回傳的狀態碼,直接丟進去就好。
  2. preg:就是剛剛編譯好的正規表示式。
  3. errbuf:一個陣列,拿來存放錯誤訊息用的。
  4. errbuf_size:指 errbuf 的長度。
1
2
3
char msgbuf[256];
regerror(status, &preg, msgbuf, sizeof(msgbuf)); 
printf("error: %s\n", msgbuf);



程式範例

把上面的程式碼,整合起來:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <stdbool.h>
#include <regex.h>  
#include <assert.h>

int main(){
    regex_t preg; // 宣告編譯結果變數
    const char* pattern = "^[a-z0-9_]+@([a-z0-9-]+\.)+[a-z0-9]+$"; // 定義表示式
    int success = regcomp(&preg, pattern, REG_EXTENDED|REG_ICASE); // 編譯,這邊使用 ERE,且不考慮大小寫
    assert(success==0);

    char* target = "testmail_10@gmail.com";   //目標字串
    regmatch_t matchptr[1];   // 紀錄匹配結果陣列,長度為1僅紀錄 full match
    const size_t nmatch = 1;    //  matchptr陣列長度
    int status = regexec(&preg, target, nmatch, matchptr, 0); //匹配
    if (status == REG_NOMATCH){ // 沒匹配
        printf("No Match\n");
    }
    else if (status == 0){  // 匹配
        printf("Match\n");
        // 取出起始與結束位置印出字串
        for (int i = matchptr[0].rm_so; i < matchptr[0].rm_eo; i++){  
            printf("%c", target[i]);
        }
        printf("\n");
    }
    else {  // 執行錯誤
        char msgbuf[256];
        regerror(status, &preg, msgbuf, sizeof(msgbuf)); 
        printf("error: %s\n", msgbuf);
    }

    regfree(&preg);  // 釋放
    return 0;
}



參考資料

  1. 陈止风 (2017-02-06)。c regex 用法 。檢自 陈止风的博客|CSDN博客 (2020-08-28)。
  2. felix021 (2012-11-25)。使用Linux/Unix/BSD的regex库 。檢自 Felix021 (2020-08-28)。
  3. piedog (2016-06-22)。When using regex in C, \d does not work but [0-9] does 。檢自 StackOverflow (2020-08-28)。
  4. Richard Kettlewell 。Regexp Syntax Summary 。檢自 RJK (2020-08-28)。
  5. elbort (2012-09-04)。c语言 正则表达式可编译c文件 。檢自 elbort的专栏 | CSDN博客 (2020-08-28)。



更新紀錄

最後更新日期:2020-09-24
     
  • 2020-09-24 發布
  •  
  • 2020-09-09 完稿
  •  
  • 2020-08-28 起稿