实战案例4:文本替换与过滤

各位同学,今天我们来聊一个非常实用的场景——文本替换与过滤。说白了,就是怎么用正则表达式在一堆文字里找到目标,然后把它换掉,或者干脆屏蔽掉。

我在做日志分析系统时,经常要处理用户输入的敏感信息。比如手机号、身份证号,这些不能直接显示出来。嗯,这时候正则就派上大用场了。

8.1 核心思路:查找 + 替换

我们用的还是 regexec() 这个老朋友。它负责找到匹配的位置,然后我们自己动手实现替换逻辑。为什么不用现成的替换函数?因为C标准库没有提供,得自己写。

你想想看,替换的本质是什么?其实就是三步:

  1. 找到匹配的字符串
  2. 把匹配部分替换成新内容
  3. 把剩下的部分拼接回去

我刚开始做的时候,也想过偷懒。但后来发现,自己实现替换逻辑反而更灵活。比如你可以控制替换次数,或者对不同的匹配做不同的处理。

核心流程:

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(&regex, pattern, REG_EXTENDED);
    if (err) return -1;
    
    while (count < max_replace) {
        err = regexec(&regex, 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(&regex);
    return count;
}

这段代码我用了很多年。注意看,match.rm_somatch.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(&regex, "(fuck|shit|damn)", REG_EXTENDED | REG_ICASE);
    
    while (regexec(&regex, 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(&regex);
}
注意:

敏感词过滤要考虑上下文。比如 "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(&regex, "([0-9]{4})-([0-9]{2})-([0-9]{2})", REG_EXTENDED);
    
    while (regexec(&regex, 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(&regex);
}

你看,用 %.*s 可以精确截取子表达式匹配的内容,非常方便。

8.5 知识体系总览

下面这张图总结了文本替换与过滤的核心逻辑:

文本替换与过滤核心流程 输入文本 regcomp() 编译正则 regexec() 循环查找匹配 匹配到? 手动替换逻辑 复制+替换+拼接 输出结果 剩余内容直接复制 继续查找下一个 核心:regexec() 定位 → 手动拼接 → 循环处理

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 拼接导致内存溢出。后来改用动态分配缓冲区,每次替换后检查剩余空间。建议你也养成这个习惯。

好了,文本替换与过滤的核心内容就这些。说白了就是三步:找到它、换掉它、继续找。掌握了这个思路,大部分文本处理场景你都能应付。


公众号:蓝海资料掘金营,微信 deep3321