实战案例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(®ex, pattern, REG_EXTENDED);
if (ret) {
fprintf(stderr, "正则编译失败\n");
return -1;
}
ret = regexec(®ex, email, 0, NULL, 0);
regfree(®ex);
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(®ex, pattern, REG_EXTENDED);
if (ret) return -1;
ret = regexec(®ex, cleaned, 0, NULL, 0);
regfree(®ex);
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。结果呢?维护的人想死,而且性能还差。
实际项目中,我通常分两步走:
- 用正则做基本格式检查
- 用
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(®ex, pattern, REG_EXTENDED);
if (ret) return -1;
ret = regexec(®ex, url, 0, NULL, 0);
regfree(®ex);
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(®ex, pattern, REG_EXTENDED);
if (ret) return -1;
ret = regexec(®ex, ip, 0, NULL, 0);
regfree(®ex);
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 知识体系总览
下面这张图,把本章涉及的数据验证场景和对应的正则策略串起来了。你想想看,是不是这个理儿?
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(®ex, pattern, REG_EXTENDED);
if (ret) return -1;
ret = regexec(®ex, input, 0, NULL, 0);
regfree(®ex);
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