---
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(®);
```
## 錯誤處理: 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) 許可協議。轉載請標明作者、連結與出處!