实战案例3:数据验证:邮箱格式验证、手机号验证、URL验证、IP地址验证

数据验证,说白了就是帮程序把把关。

用户填了个邮箱,你总得看看是不是长了个邮箱样。用户输了个手机号,你总得确认不是乱敲的13个8。这些活儿,正则表达式干起来最顺手。

我个人习惯,凡是涉及用户输入的字符串,一律先过一遍正则。别信用户,信正则。这是我踩过无数坑之后总结出来的铁律。

7.1 邮箱格式验证

邮箱这东西,看着简单,其实坑不少。

我记得刚入行那会儿,写了个邮箱正则,以为万事大吉。结果上线第二天,用户投诉说「我明明填了正确的邮箱,为什么提示格式错误?」我一看,好家伙,用户邮箱里带了个加号,比如 user+tag@example.com。我的正则没考虑这个,直接给拦了。

一个相对靠谱的邮箱正则长这样:

#include <stdio.h>
#include <regex.h>

int validate_email(const char *email) {
    regex_t regex;
    // 这个正则允许字母、数字、点、下划线、百分号、加号、减号
    // 域名部分至少有一个点,顶级域名至少2个字母
    const char *pattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";
    int ret;

    ret = regcomp(&regex, pattern, REG_EXTENDED);
    if (ret) {
        fprintf(stderr, "正则编译失败\n");
        return -1;
    }

    ret = regexec(&regex, email, 0, NULL, 0);
    regfree(&regex);

    return ret == 0 ? 1 : 0;
}

int main() {
    const char *tests[] = {
        "user@example.com",
        "user+tag@example.com",
        "user.name@sub.example.co.uk",
        "invalid-email@",
        "justtext",
        NULL
    };

    for (int i = 0; tests[i] != NULL; i++) {
        printf("%s -> %s\n", tests[i],
               validate_email(tests[i]) ? "有效" : "无效");
    }
    return 0;
}

关键点说明:

  • [a-zA-Z0-9._%+-]+:邮箱本地部分,允许常见字符
  • \\.[a-zA-Z]{2,}:顶级域名至少2个字母,比如 .com、.cn
  • 注意反斜杠在C字符串中要转义,写两个 \\

我曾经踩过的坑:

别试图写一个「完美」的邮箱正则。RFC 5322 标准定义的邮箱格式极其复杂,支持注释、引号、空格等。实际项目中,用上面这个级别就够了。太严格的正则反而会误杀合法用户。

7.2 手机号验证

手机号验证,国内场景下相对简单。中国大陆手机号目前是11位,以1开头,第二位通常是3-9。

但这里有个细节——你想想看,用户可能输入 138 1234 5678 带空格的,也可能输入 +86 13812345678 带国际区号的。要不要处理?

我个人建议:先做预处理,去掉空格、横线、括号,再验证核心号码。

#include <stdio.h>
#include <regex.h>
#include <string.h>

// 简单清理:去掉空格和横线
void clean_phone(const char *src, char *dst) {
    while (*src) {
        if (*src != ' ' && *src != '-' && *src != '(' && *src != ')') {
            *dst++ = *src;
        }
        src++;
    }
    *dst = '\0';
}

int validate_phone(const char *phone) {
    regex_t regex;
    // 匹配中国大陆手机号:1[3-9]开头,后面9位数字
    const char *pattern = "^1[3-9][0-9]{9}$";
    int ret;

    char cleaned[32];
    clean_phone(phone, cleaned);

    ret = regcomp(&regex, pattern, REG_EXTENDED);
    if (ret) return -1;

    ret = regexec(&regex, cleaned, 0, NULL, 0);
    regfree(&regex);

    return ret == 0 ? 1 : 0;
}

int main() {
    const char *tests[] = {
        "13812345678",
        "139 1234 5678",
        "+86 13812345678",  // 这个会失败,因为+86没处理
        "12345678901",
        NULL
    };

    for (int i = 0; tests[i] != NULL; i++) {
        printf("%s -> %s\n", tests[i],
               validate_phone(tests[i]) ? "有效" : "无效");
    }
    return 0;
}

小技巧:

如果项目需要支持国际号码,建议用专门的库(比如 libphonenumber)。正则只适合做「快速过滤」,不适合做「精确验证」。我在一个国际化项目中试过纯正则验证全球手机号,结果正则写了200多行,最后还是漏了不少情况。

7.3 URL验证

URL验证,嗯,这里要注意。URL的格式比邮箱还复杂。协议、域名、端口、路径、查询参数、锚点……

我见过有人写了一个超长的正则,试图覆盖所有合法URL。结果呢?维护的人想死,而且性能还差。

实际项目中,我通常分两步走:

  1. 用正则做基本格式检查
  2. curl 或系统调用做实际可达性验证(如果需要)
#include <stdio.h>
#include <regex.h>

int validate_url(const char *url) {
    regex_t regex;
    // 基本URL格式:协议://域名/路径
    const char *pattern =
        "^(https?|ftp)://"
        "([a-zA-Z0-9.-]+)"
        "(\\.[a-zA-Z]{2,})?"
        "(:[0-9]+)?"
        "(/[a-zA-Z0-9._~:/?#\\[\\]@!$&'()*+,;=-]*)?$";
    int ret;

    ret = regcomp(&regex, pattern, REG_EXTENDED);
    if (ret) return -1;

    ret = regexec(&regex, url, 0, NULL, 0);
    regfree(&regex);

    return ret == 0 ? 1 : 0;
}

int main() {
    const char *tests[] = {
        "https://www.example.com",
        "http://example.com/path?query=1",
        "ftp://files.example.org",
        "not-a-url",
        "http://",
        NULL
    };

    for (int i = 0; tests[i] != NULL; i++) {
        printf("%s -> %s\n", tests[i],
               validate_url(tests[i]) ? "有效" : "无效");
    }
    return 0;
}

避坑指南:

我曾经在项目中用正则验证URL,结果把 http://localhost:8080 给拦了。为什么?因为我的正则要求域名里必须有点。localhost没有点,就被误杀了。所以,如果项目需要支持本地开发环境,记得把 localhost 和IP地址也考虑进去。

7.4 IP地址验证

IP地址分IPv4和IPv6。IPv4相对简单,四个数字段,每段0-255,用点分隔。

但这里有个坑——你不能简单用 \\d{1,3} 来匹配,因为 999 也是三位数字,但它不是合法IP段。

正确的做法是:把0-255这个范围用正则表达出来。

#include <stdio.h>
#include <regex.h>

int validate_ipv4(const char *ip) {
    regex_t regex;
    // 匹配0-255:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]
    const char *segment = "(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])";
    char pattern[256];

    snprintf(pattern, sizeof(pattern),
             "^%s\\.%s\\.%s\\.%s$",
             segment, segment, segment, segment);

    int ret = regcomp(&regex, pattern, REG_EXTENDED);
    if (ret) return -1;

    ret = regexec(&regex, ip, 0, NULL, 0);
    regfree(&regex);

    return ret == 0 ? 1 : 0;
}

int main() {
    const char *tests[] = {
        "192.168.1.1",
        "255.255.255.255",
        "0.0.0.0",
        "256.1.2.3",    // 无效,256超出范围
        "192.168.1",    // 无效,只有三段
        "abc.def.ghi.jkl",
        NULL
    };

    for (int i = 0; tests[i] != NULL; i++) {
        printf("%s -> %s\n", tests[i],
               validate_ipv4(tests[i]) ? "有效" : "无效");
    }
    return 0;
}

IPv4段的正则拆解:

范围 正则 说明
250-255 25[0-5] 25开头,第三位0-5
200-249 2[0-4][0-9] 2开头,第二位0-4,第三位任意
100-199 1[0-9][0-9] 1开头,后两位任意
0-99 [1-9]?[0-9] 可选一位非零,加一位任意

7.5 知识体系总览

下面这张图,把本章涉及的数据验证场景和对应的正则策略串起来了。你想想看,是不是这个理儿?

数据验证 邮箱验证 手机号验证 URL验证 IP地址验证 本地部分 @符号 域名 顶级域名 1开头 3-9第二位 9位数字 协议 域名 端口 路径 4段数字 0-255范围 点分隔

7.6 综合实战:一个通用的验证函数

实际项目中,我们往往需要同时验证多种格式。我习惯写一个统一的验证接口,根据传入的类型参数,调用对应的正则。

#include <stdio.h>
#include <string.h>
#include <regex.h>

typedef enum {
    TYPE_EMAIL,
    TYPE_PHONE,
    TYPE_URL,
    TYPE_IPV4
} ValidateType;

int validate(ValidateType type, const char *input) {
    const char *pattern = NULL;

    switch (type) {
        case TYPE_EMAIL:
            pattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";
            break;
        case TYPE_PHONE:
            pattern = "^1[3-9][0-9]{9}$";
            break;
        case TYPE_URL:
            pattern = "^(https?|ftp)://[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}(:[0-9]+)?(/.*)?$";
            break;
        case TYPE_IPV4: {
            const char *seg = "(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])";
            static char buf[256];
            snprintf(buf, sizeof(buf), "^%s\\.%s\\.%s\\.%s$", seg, seg, seg, seg);
            pattern = buf;
            break;
        }
        default:
            return -1;
    }

    regex_t regex;
    int ret = regcomp(&regex, pattern, REG_EXTENDED);
    if (ret) return -1;

    ret = regexec(&regex, input, 0, NULL, 0);
    regfree(&regex);

    return ret == 0 ? 1 : 0;
}

int main() {
    printf("邮箱验证: %d\n", validate(TYPE_EMAIL, "test@example.com"));
    printf("手机号验证: %d\n", validate(TYPE_PHONE, "13812345678"));
    printf("URL验证: %d\n", validate(TYPE_URL, "https://www.example.com"));
    printf("IP验证: %d\n", validate(TYPE_IPV4, "192.168.1.1"));
    return 0;
}

我的个人习惯:

我会把常用的验证正则定义成宏,放在一个头文件里。这样多个源文件都能复用,而且修改起来方便。比如:

#define REGEX_EMAIL    "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
#define REGEX_PHONE    "^1[3-9][0-9]{9}$"
#define REGEX_IPV4_SEG "(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])"

别问我为什么不用 const char* 数组——宏在编译期展开,性能更好,而且可以用于静态初始化。

好了,数据验证这块儿,说白了就是「用正则做第一道筛子」。别指望正则解决所有问题,但它能帮你拦住90%的明显错误。剩下的10%,交给业务逻辑去处理。


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