目前在读《LinuxC编程一站式学习》,读到有关正则表达式(Regular Expression)的部分。书中把Regex讲述得很清晰,除了基本语法之外还介绍了一些linux平台的工具(如sed(一种流编辑器)、awk(可以以列为单位处理文件)),但将C中的使用(regex库)留给了读者去钻研。
起初看man page,总感觉不能领会到其中的真意,连功能都不太懂。后来在百度上搜索,查阅了一些资料,做了一些实验,对这个库的组织方式有了一些认识。
在这里着重分析各个函数、信息之间的交流方式,简要介绍一些重要的功能。
正则表达式
定义
《LinuxC编程一站式学习》中这样定义正则表达式:
规定一些特殊语法表示的字符类、数量限定符和位置关系,然后用这些特殊语法和普通字符一起表示一个模式,这就是正则表达式(Regular Expression)。
正则表达式代表着一类字符串,因此可用来判定字符串是否合法,搜索符合条件的字符串。虽然对初次接触的人来说比较晦涩难懂,但熟练掌握后就能体会到它的强大、灵活之处。子正则表达式
正则表达式中用一对圆括号()括起来的式子就是一个子正则表达式。例如([0-9]{1,3}.){3}就有一个子正则表达式([0-9]{1,3}.),而[a-b]3则没有子正则表达式使用
有关正则表达式的使用,已经有很多不错的文章了,这里不再赘述。
C中正则表达式的解析
POSIX规定了正则表达式的C语言库函数,查看详细信息:
1 | $ man 3 regex |
man page显示整个regex库有四个函数。
但我们的目的只有一个:解析正则表达式。由此可以判断许多函数的存在是为了帮助我们简化流程和分析错误的。
1 | 编译: int regcomp(regex_t *preg, const char *regex, int cflags); |
而解析正则表达式的流程也比较清晰:正则表达式的字符串 -> 编译(regex_t)+错误检测 -> 判断(并分离) -> 释放空间
先来看看函数内部的组成
首先引起我们注意的是regex_t类型的参数preg。下面是regex.h头文件中关于regex_t类型的定义。
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
36typedef struct re_pattern_buffer regex_t;
struct re_pattern_buffer
{
/* Space that holds the compiled pattern. It is declared as
`unsigned char *' because its elements are sometimes used as
array indexes. */
unsigned char *__REPB_PREFIX(buffer);
/* Number of bytes to which `buffer' points. */
unsigned long int __REPB_PREFIX(allocated);
unsigned long int __REPB_PREFIX(used);
reg_syntax_t __REPB_PREFIX(syntax);
char *__REPB_PREFIX(fastmap);
__RE_TRANSLATE_TYPE __REPB_PREFIX(translate);
/* Number of subexpressions found by the compiler. */
size_t re_nsub;
unsigned __REPB_PREFIX(can_be_null) : 1;
unsigned __REPB_PREFIX(regs_allocated) : 2;
unsigned __REPB_PREFIX(fastmap_accurate) : 1;
unsigned __REPB_PREFIX(no_sub) : 1;
unsigned __REPB_PREFIX(not_bol) : 1;
unsigned __REPB_PREFIX(not_eol) : 1;
unsigned __REPB_PREFIX(newline_anchor) : 1;
};因为篇幅的原因,删掉了一些注释,但我们不难看出,这其中包含有原字符串的信息,一些设置和奇奇怪怪的东西(还搞不懂)。由此推测regcomp函数的作用就是接受字符串类型的正则表达式(参数中的regex),对其进行编译后,将信息放入preg参数中,以供regexec函数继续使用。
需要指出的是,re_nsub代表着子串的数目(下面会再次提到)而cflags则是一些设置选项。这里有四个选项
- REG_EXTENDED: 使用正则表达式的Extented规范,未设置则使用Basic规范。(和grep -E同理)
- REG_NOSUB:不储存位置信息。(用于仅验证的场景)
- REG_ICASE:不区分大小写。
- REG_NEWLINE: 开始完全不能理解,后来看了这篇文章才明白一些。
作用总结起来就是三个:- 使^和$有效。(开头和结尾通过换行符判定、正则表达式中的^和$不再被当作普通符号)
- 绝对不匹配换行符。(能以行为单位搜索)
- 使REG_NOTBOL和REG_NOTEOL无效。(regexec函数中的选项)
返回值则与错误检测有关,见下文中的regerror函数。
regexec函数
1
int regexec(const regex_t *preg, const char *string, size_t nmatch, regmatch_t pmatch[], int eflags);
preg参数即是经过regcomp函数处理的正则表达式
至于pmatch和nmatch,man page的说明如下:
nmatch and pmatch are used to provide information regarding the location of any matches.
These are filled in by regexec() with substring match addresses. The offsets of the subexpression starting at the ith open parenthesis are stored in pmatch[i]. The entire regular expression’s match addresses are stored in pmatch[0]. (Note that to return the offsets of N subexpression matches, nmatch must be at least N+1.) Any unused structure elements will contain the value -1.- 我们初步了解到pmatch储存子串(substring)的位置信息,nmatch总是比子串的个数多1。
需要特别注意的是其中的substring不是指每个符合正则表达式的字符串,而是符合某个子正则表达式的字符串。(一开始理解错误,总是想不明白)
于是我们可以解释为什么nmatch总是比字串的个数多1了,因为pmatch[0]中存放的是符合整个正则表达式的字符串的位置信息,而pmatch[i]中存放的是第i个字串的位置信息。
联系上文中的re_nsub,我们可以轻松定义pmatch和nmatch。 - 如果向regcomp函数中的cflag参数添加了REG_NOSUB,参数pmatch和nmatch填NULL和0就行(因为根本不会被操作)
- 最后来看看pmatch的结构:regoff_t是一种整型,rm_so与rm_eo分别标记了子串的起始位和终止位,用区间来表示可以写成[rm_so, rm_eo)。若分配了pmatch[i]的空间但是没有装载信息,则rm_so与rm_so均为-1。
1
2
3
4
5typedef 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;
- 我们初步了解到pmatch储存子串(substring)的位置信息,nmatch总是比子串的个数多1。
eflag只有两个:
REG_NOTBOL : 匹配开头的正则表达式总是失败(No match)
REG_NOTEOL : 匹配结尾的正则表达式总是失败(No match)返回值:0代表成功,REG_NOMATCH代表匹配失败
regerror函数
似乎只用于测试阶段,检测正则表达式是否符合规范。1
size_t regerror(int errcode, const regex_t *preg, char *errbuf, size_t errbuf_size);
但写不动了…改天再弄吧!
应用
下面就是两个不同的应用实例
ip验证
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/*
* check_ip.c
* 判断字符串是否符合ip地址规范(不考虑地址的范围)
*/
int main(void)
{
char re[] = "^([0-9]{1,3}\.)([0-9]{1,3}\.){2}([0-9]{1,3})$";
char str[] = "198.123.45.1";
regex_t re_t;
/* 编译正则表达式 */
if (regcomp(&re_t, re, REG_EXTENDED | REG_NOSUB) != 0)
{
perror("regcomp");
exit(EXIT_FAILURE);
}
/* 匹配并判断*/
if (regexec(&re_t, str, 0, NULL, REG_NOSUB) == REG_NOMATCH) // 不需要传入nmatch和pmatch
printf("Not match\n");
else
printf("(%s) in format\n", str);
regfree(&re_t);
return 0;
}
提取邮箱地址信息
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52/*
* extract_substring.c
* 功能:取出源字符串中的第一个字串
* 缺陷:无法判断'.'连续的情况
*/
void extract_substring(char *dest, const char src[], int start, int end);
int main(void)
{
char re[] = "([a-zA-Z0-9_.-]+)@([a-zA-Z0-9_.-]+\.[a-zA-Z0-9])"; // 判断是否可能为一个e-mail
char str[] = "123456@qq.com"; // 源字符串
char e_mail[strlen(str) + 1], login_name[strlen(str) + 1], website_name[strlen(str) + 1]; // 储存各种属性
regex_t re_t;
const size_t nmatch = 3; // 比substr的个数多1
regmatch_t pmatch[3]; // 与nmatch保持一致
/* 编译 */
if (regcomp(&re_t, re, REG_EXTENDED) != 0)
{
perror("regcomp");
exit(EXIT_FAILURE);
}
/* 匹配、分配 */
if (regexec(&re_t, str, nmatch, pmatch, 0) == REG_NOMATCH)
{
perror("No match");
exit(EXIT_FAILURE);
}
else /* 拷贝至相应空间 */
{
extract_substring(e_mail, &str, pmatch[0].rm_so, pmatch[0].rm_eo);
extract_substring(login_name, &str, pmatch[1].rm_so, pmatch[1].rm_eo);
extract_substring(website_name, &str, pmatch[2].rm_so, pmatch[2].rm_eo);
}
/* 输出 */
printf("e-mail: %s, login_name: %s, website_name: %s\n", e_mail, login_name, website_name);
/* 记得free */
regfree(&re_t);
return 0;
}
void extract_substring(char *dest, const char *src, int start, int end)
{
strncpy(dest, &src[start], end - start); // 拷贝src中位置从[start,end)的元素
dest[end - start] = '\0'; // 必须手动添加'\0'
}