第30章:综合项目:构建一个完整的正则表达式工具库
终于到了最后一章。说实话,我每次带新人做项目,都会让他们自己封装一个正则工具库。为什么?因为只有亲手搭过一遍,你才能真正理解那些API背后的设计哲学。今天我们就来干这件事——从零构建一个可用的C语言正则工具库。
项目目标:封装常用正则函数、支持链式调用、提供完善的错误处理、编写可复用的单元测试。
30.1 库的整体设计思路
先说说我的设计原则。我个人习惯把库分成三层:底层引擎层、中间封装层、上层接口层。底层直接调用POSIX的regex.h,中间层做错误处理和内存管理,上层提供链式调用的语法糖。
你想想看,如果每个函数都返回一个结构体指针,那链式调用就顺理成章了。嗯,这里要注意一点:C语言没有自动垃圾回收,所以链式调用必须配合好内存释放策略。
设计决策:我选择用regex_t作为核心结构体,外面包一层RegexEngine上下文。这样既保留了POSIX的灵活性,又增加了易用性。
30.2 核心数据结构
先定义我们的核心类型。我在项目中遇到过好几次结构体设计不合理导致后期重构的坑,所以这次一步到位:
// regex_toolkit.h
#ifndef REGEX_TOOLKIT_H
#define REGEX_TOOLKIT_H
#include <regex.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_ERROR_MSG 256
#define MAX_MATCHES 10
typedef struct {
regex_t *compiled; // 编译后的正则
char pattern[256]; // 原始模式串
int flags; // 编译标志
char error_msg[MAX_ERROR_MSG]; // 错误信息
int error_code; // 错误码
} RegexEngine;
typedef struct {
int matched; // 是否匹配
int count; // 匹配数量
char **groups; // 捕获组内容
size_t *group_lens; // 每组长度
} RegexResult;
// 函数声明
RegexEngine* regex_engine_create(const char *pattern, int flags);
RegexEngine* regex_engine_compile(RegexEngine *engine);
RegexResult* regex_engine_match(RegexEngine *engine, const char *text);
void regex_engine_free(RegexEngine *engine);
void regex_result_free(RegexResult *result);
#endif // REGEX_TOOLKIT_H
说白了,RegexEngine就是正则的“发动机”,RegexResult是它的“仪表盘”。两者分开,职责清晰。
30.3 链式调用的实现
链式调用的核心是什么?每个函数都返回this指针。在C语言里,就是返回RegexEngine*。我曾经在做一个日志解析工具时用过这个模式,效果出奇的好。
// regex_toolkit.c
RegexEngine* regex_engine_create(const char *pattern, int flags) {
RegexEngine *engine = (RegexEngine*)malloc(sizeof(RegexEngine));
if (!engine) {
fprintf(stderr, "内存分配失败\n");
return NULL;
}
engine->compiled = (regex_t*)malloc(sizeof(regex_t));
if (!engine->compiled) {
free(engine);
return NULL;
}
strncpy(engine->pattern, pattern, sizeof(engine->pattern) - 1);
engine->pattern[sizeof(engine->pattern) - 1] = '\0';
engine->flags = flags;
engine->error_code = 0;
engine->error_msg[0] = '\0';
return engine;
}
RegexEngine* regex_engine_compile(RegexEngine *engine) {
if (!engine) return NULL;
int ret = regcomp(engine->compiled, engine->pattern, engine->flags);
if (ret != 0) {
engine->error_code = ret;
regerror(ret, engine->compiled, engine->error_msg, MAX_ERROR_MSG);
return NULL; // 编译失败返回NULL,中断链式调用
}
return engine; // 返回自身,支持链式
}
看到没?regex_engine_compile返回engine本身,这样就能写出这样的代码:
RegexEngine *engine = regex_engine_compile(
regex_engine_create("^[a-z]+@[a-z]+\\.[a-z]{2,}$", REG_EXTENDED)
);
注意:链式调用虽然爽,但一旦中间某步返回NULL,后面的调用就会崩溃。所以一定要做好NULL检查。我建议在调用链外层包一层if判断。
30.4 错误处理机制
错误处理这块,我吃过不少亏。早期我写的库,错误信息就一个int返回值,调试起来想骂娘。后来我学乖了——错误码+错误信息双保险。
来看看我的错误处理设计:
typedef enum {
RE_OK = 0,
RE_ERR_NOMEM, // 内存不足
RE_ERR_COMPILE, // 编译失败
RE_ERR_MATCH, // 匹配失败
RE_ERR_INVALID, // 无效参数
RE_ERR_OVERFLOW // 结果溢出
} RegexErrorCode;
const char* regex_strerror(int error_code) {
switch (error_code) {
case RE_OK: return "操作成功";
case RE_ERR_NOMEM: return "内存分配失败,请检查系统资源";
case RE_ERR_COMPILE: return "正则表达式编译错误,请检查语法";
case RE_ERR_MATCH: return "匹配过程发生错误";
case RE_ERR_INVALID: return "传入的参数无效";
case RE_ERR_OVERFLOW: return "匹配结果超出缓冲区限制";
default: return "未知错误";
}
}
为什么要有错误码枚举?因为程序里需要根据错误码做分支处理。而错误信息是给人看的,调试时一目了然。两者缺一不可。
30.5 匹配与捕获组实现
匹配函数是库的核心。我记得有一次帮同事调试一个日志解析器,就是因为捕获组索引搞错了,查了半天。所以这里我特意把捕获组设计成动态数组:
RegexResult* regex_engine_match(RegexEngine *engine, const char *text) {
if (!engine || !text) return NULL;
RegexResult *result = (RegexResult*)malloc(sizeof(RegexResult));
if (!result) return NULL;
regmatch_t pmatch[MAX_MATCHES];
int ret = regexec(engine->compiled, text, MAX_MATCHES, pmatch, 0);
result->matched = (ret == 0) ? 1 : 0;
result->count = 0;
result->groups = NULL;
result->group_lens = NULL;
if (ret == 0) {
// 计算匹配到的组数
for (int i = 0; i < MAX_MATCHES; i++) {
if (pmatch[i].rm_so == -1) break;
result->count++;
}
// 分配内存
result->groups = (char**)malloc(result->count * sizeof(char*));
result->group_lens = (size_t*)malloc(result->count * sizeof(size_t));
for (int i = 0; i < result->count; i++) {
size_t len = pmatch[i].rm_eo - pmatch[i].rm_so;
result->groups[i] = (char*)malloc(len + 1);
strncpy(result->groups[i], text + pmatch[i].rm_so, len);
result->groups[i][len] = '\0';
result->group_lens[i] = len;
}
}
return result;
}
小技巧:我习惯在分配内存后立即初始化,避免野指针。你看result->groups = NULL那几行,就是防患于未然。
30.6 单元测试编写
单元测试怎么写?我的原则是:覆盖正常路径、边界条件、异常输入。下面是我写的一个测试文件片段:
// test_regex_toolkit.c
#include <assert.h>
#include <stdio.h>
#include "regex_toolkit.h"
void test_basic_match() {
printf("测试1: 基本匹配...\n");
RegexEngine *engine = regex_engine_create("hello", REG_EXTENDED);
assert(engine != NULL);
engine = regex_engine_compile(engine);
assert(engine != NULL);
RegexResult *result = regex_engine_match(engine, "hello world");
assert(result != NULL);
assert(result->matched == 1);
regex_result_free(result);
regex_engine_free(engine);
printf("通过!\n");
}
void test_no_match() {
printf("测试2: 不匹配情况...\n");
RegexEngine *engine = regex_engine_compile(
regex_engine_create("^[0-9]+$", REG_EXTENDED)
);
assert(engine != NULL);
RegexResult *result = regex_engine_match(engine, "abc123");
assert(result != NULL);
assert(result->matched == 0);
regex_result_free(result);
regex_engine_free(engine);
printf("通过!\n");
}
void test_capture_groups() {
printf("测试3: 捕获组...\n");
RegexEngine *engine = regex_engine_compile(
regex_engine_create("(\\w+)@(\\w+)\\.(\\w+)", REG_EXTENDED)
);
assert(engine != NULL);
RegexResult *result = regex_engine_match(engine, "user@example.com");
assert(result != NULL);
assert(result->matched == 1);
assert(result->count >= 4); // 完整匹配 + 3个捕获组
printf(" 完整匹配: %s\n", result->groups[0]);
printf(" 用户名: %s\n", result->groups[1]);
printf(" 域名: %s\n", result->groups[2]);
printf(" 顶级域: %s\n", result->groups[3]);
regex_result_free(result);
regex_engine_free(engine);
printf("通过!\n");
}
void test_invalid_pattern() {
printf("测试4: 无效模式...\n");
RegexEngine *engine = regex_engine_create("[invalid", REG_EXTENDED);
assert(engine != NULL);
engine = regex_engine_compile(engine);
assert(engine == NULL); // 编译失败应返回NULL
printf("通过! (预期失败)\n");
}
void test_null_input() {
printf("测试5: NULL输入...\n");
RegexResult *result = regex_engine_match(NULL, "test");
assert(result == NULL);
result = regex_engine_match(NULL, NULL);
assert(result == NULL);
printf("通过!\n");
}
int main() {
printf("===== 正则工具库单元测试 =====\n\n");
test_basic_match();
test_no_match();
test_capture_groups();
test_invalid_pattern();
test_null_input();
printf("\n所有测试通过!\n");
return 0;
}
你看,测试用例覆盖了:正常匹配、不匹配、捕获组、无效模式、NULL输入。这五种情况基本能覆盖90%的使用场景。我在实际项目中,还会加上性能测试和内存泄漏检测。
30.7 知识体系总览
下面这张图,是我对这个工具库整体架构的理解。你可以把它当作一张“地图”,随时回来对照:
30.8 使用示例
最后,给你看一个完整的调用示例。这是我当年写的一个日志分析工具的核心片段:
#include "regex_toolkit.h"
int main() {
const char *log_line = "2024-01-15 14:30:22 [ERROR] 数据库连接超时 (code: 1001)";
// 链式调用:创建 -> 编译 -> 匹配
RegexEngine *engine = regex_engine_compile(
regex_engine_create(
"\\[(ERROR|WARN|INFO)\\]\\s+(.+?)\\s+\\(code:\\s+(\\d+)\\)",
REG_EXTENDED
)
);
if (!engine) {
fprintf(stderr, "正则引擎初始化失败\n");
return -1;
}
RegexResult *result = regex_engine_match(engine, log_line);
if (result && result->matched) {
printf("日志级别: %s\n", result->groups[1]);
printf("错误信息: %s\n", result->groups[2]);
printf("错误代码: %s\n", result->groups[3]);
}
regex_result_free(result);
regex_engine_free(engine);
return 0;
}
输出结果:
日志级别: ERROR
错误信息: 数据库连接超时
错误代码: 1001
嗯,到这里,我们的正则工具库就基本成型了。说实话,这个库虽然不大,但五脏俱全。你以后做任何需要正则处理的项目,都可以拿它当基础模板。记住我常说的那句话:好的工具库,不是功能堆砌,而是设计出来的。
公众号:蓝海资料掘金营,微信deep3321