--- title: 【C 語言】使用 Regular Expressions date: 2020-09-24 23:28 is_modified: false disqus: cynthiahackmd categories: - "程式設計 › 程式語言與架構" tags: - "程式設計 › 程式語言與架構" - "C/C++" - "regexp" - "Published" --- {%hackmd @CynthiaChuang/Github-Page-Theme %} <br> [挖了個坑](/@CynthiaChuang/Check-Love-Code)給自己跳,為啥我會這想不開,要在 C 上寫 regexp? 不過最大的坑應該是...我幹麻選 C 阿,都 4、5 年沒碰了。 <!--more--> ## regex.h 函式庫 用 C 來寫正規表示式第一個碰到的問題是,==C 的標準函式庫中並沒有支援正規表示式==。 不過還好我的開發環境是 Linux,在現行 Linux 中大多有安裝 POSIX.2,所以可以直接引入 `<regex.h>` 來使用,但如果是 Windows 就得[自食其力](https://stackoverflow.com/questions/8230905/regex-h-for-windows)了。 看了下它的[原始碼](https://code.woboq.org/linux/include/regex.h.html)還挺簡單的,抽掉細節跟註解,比較常用的也就 4 個函式與一些常數(根據 [felix021 的網誌](https://www.felix021.com/blog/read.php?2099)說明,整份文件有 2 個類別、4 個函式和 7 個常數)。 在使用 regex.h 進行開發時,一般會有三個步驟:**編譯**、 **匹配**、 **釋放**,三個步驟分別對應到四個常用函式中的其中三個:`regcomp()`、 `regexec()`、 `regfree()`,剩下一個是錯誤處理 `regerror()`。 ## 編譯:regcomp() 這個是把指定的表示式 `pattern` 編譯成特定的資料格式 `regex_t` ,在下一個階段會使用編譯後的結果 `preg` 進行匹配。 ```cpp= 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**: 識別換行符號。 <br> 思考了下,我應該會開 **ERE**,因為我會的**流派**應該算是 ==PCRE (Perl Compatible Regular Expressions)== 一派,跟 BRE(Basic Regular Expression,基本型正規表示式)[差異](http://www.greenend.org.uk/rjk/tech/regexp.html)看起來頗多,相較之下 ERE 還稍微相近一點,不過還是有些差異。 <br> 回傳值的部份,編譯成功會回傳 0,否則傳回其他值。來個片段看一下: ```cpp= // 比對電子信箱 regex_t preg; // 宣告編譯結果變數 const char* pattern = "^[a-z0-9_]+@([a-z0-9-]+\\.)+[a-z0-9]+$"; // 定義表示式 // 編譯,這邊使用 ERE,且不考慮大小寫 int success = regcomp(&preg, pattern, REG_EXTENDED|REG_ICASE); assert(success==0); ``` 是說 POSIX 體系,==沒有== `\d` 跟 `\w` 可用,只能寫成 `[a-z0-9]` 。 ## 匹配:regexec() 編譯好後就可以把目標字串放進去匹配了。 ```cpp= int regexec (regex_t *preg, char *target, size_t nmatch, regmatch_t matchptr [], int eflags) ``` <br> 先提一下 `regmatch_t` 這個資料格式,在程式碼中它是長這樣: ```cpp= 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。 <br> 我們來看下要傳啥參數進去, 1. **preg**:這就是在上一個階段使用 `regcomp` 編譯完的 `preg`,直接丟進來就好。 2. **target**:我們的目標字串。 3. **nmatch** 與 **matchptr**:`matchptr` 就是上面所提過 `regmatch_t` 陣列。`nmatch` 則是 `matchptr` 的長度。 4. **eflags**:有兩個值 **REG_NOTBOL**、**REG_NOTEOL**。 <br> 繼續我們的 Code: ```cpp= 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() 把空間釋放放掉。 ```cpp= void regfree (regex_t *preg) ``` 在使用完畢或要重編譯新的表示式前,調用這個清空 `preg` 指向的 `regex_t `的内容。 ```cpp= regfree(&reg); ``` ## 錯誤處理: regerror() 當執行 `regcomp` 或 `regexec` 產生錯誤的時候,就可以調用這個函數返回錯誤訊息的字串。 ```cpp= size_t regerror(int errcode, const regex_t *preg, char *errbuf, size_t errbuf_size); ``` 參數的部分: 1. **errcode**:就是剛剛 `regcomp` 跟 `regexec` 回傳的狀態碼,直接丟進去就好。 2. **preg**:就是剛剛編譯好的正規表示式。 3. **errbuf**:一個陣列,拿來存放錯誤訊息用的。 4. **errbuf_size**:指 `errbuf` 的長度。 ```cpp= char msgbuf[256]; regerror(status, &preg, msgbuf, sizeof(msgbuf)); printf("error: %s\n", msgbuf); ``` ## 程式範例 把上面的程式碼,整合起來: ```cpp= #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 用法](https://blog.csdn.net/yingzinanfei/article/details/54893394) 。檢自 陈止风的博客|CSDN博客 (2020-08-28)。 2. felix021 (2012-11-25)。[使用Linux/Unix/BSD的regex库](https://www.felix021.com/blog/read.php?2099) 。檢自 Felix021 (2020-08-28)。 3. piedog (2016-06-22)。[When using regex in C, \d does not work but [0-9] does](https://stackoverflow.com/questions/37956719/when-using-regex-in-c-d-does-not-work-but-0-9-does) 。檢自 StackOverflow (2020-08-28)。 4. Richard Kettlewell 。[Regexp Syntax Summary](http://www.greenend.org.uk/rjk/tech/regexp.html) 。檢自 RJK (2020-08-28)。 5. elbort (2012-09-04)。[c语言 正则表达式可编译c文件](https://blog.csdn.net/elbort/article/details/7943317) 。檢自 elbort的专栏 | CSDN博客 (2020-08-28)。 ## 更新紀錄 :::spoiler 最後更新日期:2020-09-24 - 2020-09-24 發布 - 2020-09-09 完稿 - 2020-08-28 起稿 ::: <br><br> > **本文作者**: 辛西亞.Cynthia > **本文連結**: [辛西亞的技能樹](https://cynthiachuang.github.io/Regular-Expressions-in-C/) / [hackmd 版本](https://hackmd.io/@CynthiaChuang/Regular-Expressions-in-C) > **版權聲明**: 部落格中所有文章,均採用 [姓名標示-非商業性-相同方式分享 4.0 國際](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en) (CC BY-NC-SA 4.0) 許可協議。轉載請標明作者、連結與出處!