实战案例4:文本替换与过滤
各位同学,今天我们来聊一个非常实用的场景——文本替换与过滤。说白了,就是怎么用正则表达式在一堆文字里找到目标,然后把它换掉,或者干脆屏蔽掉。
我在做日志分析系统时,经常要处理用户输入的敏感信息。比如手机号、身份证号,这些不能直接显示出来。嗯,这时候正则就派上大用场了。
8.1 核心思路:查找 + 替换
我们用的还是 regexec() 这个老朋友。它负责找到匹配的位置,然后我们自己动手实现替换逻辑。为什么不用现成的替换函数?因为C标准库没有提供,得自己写。
你想想看,替换的本质是什么?其实就是三步:
- 找到匹配的字符串
- 把匹配部分替换成新内容
- 把剩下的部分拼接回去
我刚开始做的时候,也想过偷懒。但后来发现,自己实现替换逻辑反而更灵活。比如你可以控制替换次数,或者对不同的匹配做不同的处理。
regexec() 定位匹配 → 提取匹配位置 → 手动拼接新字符串 → 继续查找下一个
8.2 手动实现替换逻辑
我们先看一个简单的例子:把文本中的 "hello" 替换成 "world"。
#include <stdio.h>
#include <string.h>
#include <regex.h>
int replace(char *dest, const char *src, const char *pattern,
const char *replacement, int max_replace) {
regex_t regex;
regmatch_t match;
int err, count = 0;
const char *p = src;
err = regcomp(®ex, pattern, REG_EXTENDED);
if (err) return -1;
while (count < max_replace) {
err = regexec(®ex, p, 1, &match, 0);
if (err == REG_NOMATCH) break;
// 复制匹配前的内容
strncat(dest, p, match.rm_so);
// 追加替换内容
strcat(dest, replacement);
// 移动指针到匹配后
p += match.rm_eo;
count++;
}
// 复制剩余内容
strcat(dest, p);
regfree(®ex);
return count;
}
这段代码我用了很多年。注意看,match.rm_so 和 match.rm_eo 分别表示匹配的起始和结束位置。我们先把匹配前面的内容复制过去,然后插入替换文本,最后跳过匹配部分继续处理。
如果替换内容包含特殊字符(比如 $、\),记得先转义。我曾经因为这个踩过坑,调试了半天才发现是替换字符串里的反斜杠被误解了。
8.3 过滤敏感词
敏感词过滤,说白了就是替换的变种。把敏感词替换成星号或者 "[已过滤]" 这样的占位符。
我记得有一次做论坛系统,用户发帖里经常出现脏话。我们维护了一个敏感词列表,用正则一次性匹配多个词:
const char *sensitive_words = "(fuck|shit|damn|笨蛋|白痴)";
然后对每个匹配,替换成等长的星号:
void filter_sensitive(char *dest, const char *src) {
regex_t regex;
regmatch_t match;
const char *p = src;
regcomp(®ex, "(fuck|shit|damn)", REG_EXTENDED | REG_ICASE);
while (regexec(®ex, p, 1, &match, 0) == 0) {
// 复制匹配前的内容
strncat(dest, p, match.rm_so);
// 追加等长星号
int len = match.rm_eo - match.rm_so;
for (int i = 0; i < len; i++) {
strcat(dest, "*");
}
p += match.rm_eo;
}
strcat(dest, p);
regfree(®ex);
}
敏感词过滤要考虑上下文。比如 "ass" 在 "assignment" 里就不该被过滤。我建议用 \b 边界匹配来避免误伤。
8.4 格式化输出
格式化输出,就是把匹配到的内容按照特定格式重新排列。比如把日期从 "2024-01-15" 改成 "2024年01月15日"。
这里要用到子表达式捕获。我们用括号把年、月、日分别括起来:
const char *pattern = "([0-9]{4})-([0-9]{2})-([0-9]{2})";
然后手动拼接:
void format_date(char *dest, const char *src) {
regex_t regex;
regmatch_t match[4]; // 匹配0是整体,1-3是子表达式
const char *p = src;
regcomp(®ex, "([0-9]{4})-([0-9]{2})-([0-9]{2})", REG_EXTENDED);
while (regexec(®ex, p, 4, match, 0) == 0) {
// 复制匹配前的内容
strncat(dest, p, match[0].rm_so);
// 提取年、月、日
char year[5], month[3], day[3];
snprintf(year, 5, "%.*s", match[1].rm_eo - match[1].rm_so,
p + match[1].rm_so);
snprintf(month, 3, "%.*s", match[2].rm_eo - match[2].rm_so,
p + match[2].rm_so);
snprintf(day, 3, "%.*s", match[3].rm_eo - match[3].rm_so,
p + match[3].rm_so);
// 格式化输出
char formatted[32];
snprintf(formatted, 32, "%s年%s月%s日", year, month, day);
strcat(dest, formatted);
p += match[0].rm_eo;
}
strcat(dest, p);
regfree(®ex);
}
你看,用 %.*s 可以精确截取子表达式匹配的内容,非常方便。
8.5 知识体系总览
下面这张图总结了文本替换与过滤的核心逻辑:
8.6 综合实战:日志敏感信息脱敏
最后,我们把这些技术整合起来,做一个日志脱敏工具。把日志里的手机号、邮箱、IP地址都替换掉:
void desensitize_log(char *dest, const char *src) {
// 手机号:1开头的11位数字
const char *phone = "1[3-9][0-9]{9}";
// 邮箱:xxx@xxx.xxx
const char *email = "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}";
// IP地址
const char *ip = "([0-9]{1,3}\\.){3}[0-9]{1,3}";
// 拼接成正则
char pattern[256];
snprintf(pattern, 256, "(%s|%s|%s)", phone, email, ip);
// 执行替换(代码略,参考前面的 replace 函数)
// 手机号替换为 138****1234 格式
// 邮箱替换为 x***@x.com 格式
// IP替换为 ***.***.***.***
}
嗯,这里要注意的是,不同敏感信息的脱敏规则不一样。手机号要保留前3后4,邮箱要保留域名。所以不能简单粗暴地全换成星号。
我曾经在处理超大日志文件时,直接用 strcat 拼接导致内存溢出。后来改用动态分配缓冲区,每次替换后检查剩余空间。建议你也养成这个习惯。
好了,文本替换与过滤的核心内容就这些。说白了就是三步:找到它、换掉它、继续找。掌握了这个思路,大部分文本处理场景你都能应付。