第3章 3.3 字符串函数与正则

「上回说到,小明用数组函数把购物清单处理得明明白白,结果今天产品经理丢给他一份 2000 行的用户数据,让他「把邮箱格式正确的挑出来」「把所有手机号脱敏成 1381234」——小明当场石化。

别笑,这场景天天在发生。上一章你学了数组函数,能批量处理列表了。但现实中的数据更多是字符串形式存在的:日志文件、用户输入、API 返回的文本……不会处理字符串,你连最基本的「从一堆乱码里捞出有效信息」都做不到。

这一章,我们来解决这个问题。


🎯 开场 3 分钟:为什么要学这个?

场景还原

想象你运营一个论坛,用户注册时填的信息全是一团乱麻:

"张三,13812345678,zhangsan@qq.com"
"李四 13900001111 lisili@gmail.com "
"王五,13722223333,wangwu@wangwu。com"

有逗号分隔、有空格、有中文标点、有格式错误的邮箱。你要:
1. 把手机号提取出来(后面要发短信)
2. 把邮箱\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n格式校验一遍(过滤掉无效的)
3. 把手机号脱敏(保护用户隐私)

如果你只会数组函数,这条任务能把你干趴下。 因为这些数据本质上是「字符串」,不是数组。

学完本文你能

  • 用字符串函数对文本进行「切割、替换、提取、清洗」
  • 用正则表达式从乱码中精准捞出你想要的片段
  • 做一个「文本清洗小工具」,自动处理脏数据

🧱 基础 25 分钟:核心概念

3.3.1 字符串的「解剖课」

先搞懂字符串在 PHP 里是什么。

<?php
// 字符串就是一串字符的集合
$nicheng = "Python爱好者_007";

echo strlen($nicheng);  // 输出 15:一个中文字符 + 14 个英文/数字
echo mb_strlen($nicheng);  // 输出 9:中文字符按中文算

类比:字符串就像一根「腊肠」,strlen 是按长度算,mb_strlen 是按「节」算。你吃腊肠的时候,到底是按根数还是按节数?中文一个「节」顶英文好几个「节」。

3.3.2 切割字符串——「切腊肠」

生活类比:你有一根腊肠,想切成 1cm 一段,怎么切?用刀(定界符)按长度切。PHP 里切割字符串也是这个思路。

<?php
// 用 explode 切割 —— 按特定字符「刀」切开
$csv_line = "张三,13812345678,zhangsan@qq.com,北京";
$parts = explode(",", $csv_line);

print_r($parts);
/*
输出:
Array
(
[0] => 张三
[1] => 13812345678
[2] => zhangsan@qq.com
[3] => 北京
)
*/

explode(",", $csv_line) 的意思是:按逗号「,」把字符串切开,返回数组。

3.3.3 字符串替换——「换标签」

<?php
// 把手机号中间四位换成 ****
$phone = "13812345678";
$hided_phone = preg_replace("/(\d{3})\d{4}(\d{4})/", "$1****$2", $phone);

echo $hided_phone;  // 输出:138****5678

解释preg_replace 是正则替换函数,"/(\d{3})\d{4}(\d{4})/" 是正则表达式(后面细讲),"$1****$2" 是替换成什么。

3.3.4 正则表达式——「万能的筛子」

为什么要用正则?

普通字符串函数像「菜刀」,能切固定的宽度、找固定的字符。但如果你想从一堆乱码里找出「所有手机号」或「所有邮箱」,菜刀就不够用了。你需要一个「筛子」——能按「规则」筛选。

正则表达式就是这个筛子。

先记住几个最常用的符号:

符号 意思 例子
\d 任意数字 \d{3} = 任意 3 位数字
\w 字母、数字、下划线 \w+ = 一串字母
. 任意字符 a.c = a开头c结尾,中间任意
^ 开头 ^138 = 138 开头的
$ 结尾 .com$ = .com 结尾的
[] 字符集 [aeiou] = 任意元音字母
() 分组 (\d{3}) = 捕获第一组

3.3.5 手机号提取——实战第一个正则

<?php
$text = "李经理的号码是13812345678,王总13900001111,张总13666668888";

// 匹配所有 11 位手机号
preg_match_all("/1[3-9]\d{9}/", $text, $matches);

print_r($matches[0]);
/*
输出:
Array
(
[0] => 13812345678
[1] => 13900001111
[2] => 13666668888
)
*/

解释:正则 /1[3-9]\d{9}/ 的意思是:
- 1 开头
- 第 2 位是 3-9 之间的数字
- 后面跟 9 个任意数字

这就是一个「11 位手机号」的通用规则。

3.3.6 邮箱验证——更复杂的规则

<?php
$emails = ["zhangsan@qq.com", "李四@gmail.com", "错误邮箱", "wangwu@.com"];

foreach ($emails as $email) {
// 简单邮箱正则:字母数字下划线 + @ + 字母数字点 + 字母
if (preg_match("/^\w+@\w+\.\w+$/", $email)) {
    echo "$email - 格式正确\n";
} else {
    echo "$email - 格式错误\n";
}
}
/*
输出:
zhangsan@qq.com - 格式正确
李四@gmail.com - 格式错误(中文在正则里算特殊字符)
错误邮箱 - 格式错误
wangwu@.com - 格式错误
*/

注意! 这个正则比较简单,真实的邮箱验证要复杂得多。但作为「快速校验」够用了。

3.3.7 中文处理——mb_str 系列

痛点:PHP 诞生于英文环境,原生字符串函数对中文不友好。

<?php
$name = "小明你好";

// strlen 算的是字节数,不是字符数
echo strlen($name);  // 输出 13(UTF-8 下中文 3 字节一个)

// mb_strlen 才是算字符数
echo mb_strlen($name);  // 输出 5

// 截取中文也要用 mb_substr
echo mb_substr($name, 0, 3);  // 输出 "小明你好"

记住一条:处理中文,用 mb_ 开头的函数。


🔥 实战 35 分钟:3 个递进的小项目

项目 1:5 分钟——做一个「手机号脱敏器」

需求:把用户手机号中间 4 位隐藏。

<?php
/**
* 手机号脱敏器
* 输入:13812345678
* 输出:138****5678
*/

function hide_phone($phone) {
// 去除空格和短横线
$phone = preg_replace("/[\s\-]/", "", $phone);

// 验证是否是有效手机号
if (!preg_match("/^1[3-9]\d{9}$/", $phone)) {
    return "无效手机号";
}

// 脱敏:保留前3位和后4位
return preg_replace("/(\d{3})\d{4}(\d{4})/", "$1****$2", $phone);
}

// 测试
$phones = ["138-1234-5678", "139 0000 1111", "13812345678", "12345678901"];

foreach ($phones as $phone) {
echo "$phone => " . hide_phone($phone) . "\n";
}
/*
预期输出:
138-1234-5678 => 138****5678
139 0000 1111 => 139****1111
13812345678 => 138****5678
12345678901 => 无效手机号
*/

一句话解释:先用正则去掉空格和短横线,再用正则分组提取前3后4位,中间用 **** 替换。


项目 2:15 分钟——「用户数据清洗工具」

需求:从一段混乱的用户数据里提取有效信息。

<?php
/**
* 用户数据清洗工具
* 从混合格式中提取:姓名、手机号、邮箱
*/

$raw_data = <<<DATA
张三, 13812345678, zhangsan@qq.com
李四,13900001111,lisili@gmail.com
王 五 13722223333 wangwu@wangwu.com
赵六,13666668888,
DATA;

// 按行分割
$lines = explode("\n", trim($raw_data));

echo "=== 用户数据清洗结果 ===\n";

foreach ($lines as $line) {
// 去除首尾空白
$line = trim($line);

// 统一分隔符:中文逗号、顿号、空格都换成英文逗号
$line = preg_replace("/[,、\s]+/", ",", $line);

// 按逗号分割
$parts = explode(",", $line);

$name = isset($parts[0]) ? trim($parts[0]) : "";
$phone = isset($parts[1]) ? trim($parts[1]) : "";
$email = isset($parts[2]) ? trim($parts[2]) : "";

// 验证手机号
$phone_valid = preg_match("/^1[3-9]\d{9}$/", $phone) ? "✓" : "✗";

// 验证邮箱(简单版)
$email_valid = preg_match("/^\w+@\w+\.\w+$/", $email) ? "✓" : "✗";

echo "姓名: $name | 手机: $phone ($phone_valid) | 邮箱: $email ($email_valid)\n";
}
/*
预期输出:
=== 用户数据清洗结果 ===
姓名: 张三 | 手机: 13812345678 (✓) | 邮箱: zhangsan@qq.com (✓)
姓名: 李四 | 手机: 13900001111 (✓) | 邮箱: lisili@gmail.com (✓)
姓名: 王 五 | 手机: 13722223333 (✓) | 邮箱: wangwu@wangwu.com (✓)
姓名: 赵六 | 手机: 13666668888 (✓) | 邮箱:  (✗)
*/

一句话解释:先用 preg_replace 把各种奇怪的分隔符统一成逗号,再用 explode 切分,最后逐个字段验证格式。


项目 3:15 分钟——「文本关键词提取器」

需求:从一段文本里提取所有手机号和邮箱,统计出现次数。

<?php
/**
* 文本关键词提取器
* 从任意文本中提取手机号和邮箱
*/

$text = <<<TEXT
客户李经理联系:手机13812345678,邮箱zhangsan@qq.com
技术对接王工:189-0000-8888,tech@company.com
再次联系李经理:13812345678(同上)
商务合作:13900001111,business@biz.cn
TEXT;

/**
* 从文本中提取所有手机号
*/
function extract_phones($text) {
preg_match_all("/1[3-9]\d{9}/", $text, $matches);
return $matches[0];
}

/**
* 从文本中提取所有邮箱
*/
function extract_emails($text) {
preg_match_all("/\w+@\w+\.\w+/", $text, $matches);
return $matches[0];
}

/**
* 统计数组中每个元素的出现次数
*/
function count_occurrences($array) {
$counts = array_count_values($array);
arsort($counts);  // 按次数降序排列
return $counts;
}

// 提取
$phones = extract_phones($text);
$emails = extract_emails($text);

// 统计
$phone_counts = count_occurrences($phones);
$email_counts = count_occurrences($emails);

echo "=== 文本关键词提取结果 ===\n\n";
echo "📱 手机号提取:\n";
foreach ($phone_counts as $phone => $count) {
echo "  $phone 出现 {$count} 次\n";
}

echo "\n📧 邮箱提取:\n";
foreach ($email_counts as $email => $count) {
echo "  $email 出现 {$count} 次\n";
}

echo "\n=== 数据摘要 ===\n";
echo "共提取手机号:" . count($phones) . " 个(去重后:" . count($phone_counts) . " 个)\n";
echo "共提取邮箱:" . count($emails) . " 个(去重后:" . count($email_counts) . " 个)\n";
/*
预期输出:
=== 文本关键词提取结果 ===

📱 手机号提取:
13812345678 出现 2 次
18900008888 出现 1 次
13900001111 出现 1 次

📧 邮箱提取:
zhangsan@qq.com 出现 1 次
tech@company.com 出现 1 次
business@biz.cn 出现 1 次

=== 数据摘要 ===
共提取手机号:4 个(去重后:3 个)
共提取邮箱:3 个(去重后:3 个)
*/

一句话解释preg_match_all 用正则把所有匹配项「一网打尽」,array_count_values 统计出现次数,组合起来就是一个小型的「文本分析器」。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:正则里的「贪婪模式」

<?php
// ❌ 错误示例:贪婪匹配
$html = "<div>你好</div><div>世界</div>";
preg_match("/<div>.*<\/div>/", $html, $match);
echo $match[0];  // 输出:<div>你好</div><div>世界</div> —— 把两段都匹配了!

// ✅ 正确示例:非贪婪匹配(加 ?)
preg_match("/<div>.*?<\/div>/", $html, $match);
echo $match[0];  // 输出:<div>你好</div> —— 只匹配第一段

注意! 正则默认是「贪婪」的,会尽量多匹配。加个 ? 变成非贪婪模式,才会「见好就收」。

坑 2:中文正则匹配失败

<?php
// ❌ 错误示例:不加 u 修饰符
$name = "张三";
preg_match("/^[\u4e00-\u9fa5]+$/", $name);  // 可能匹配失败

// ✅ 正确示例:加 u 修饰符(Unicode)
preg_match("/^[\u4e00-\u9fa5]+$/u", $name);  // 匹配成功

注意! 中文字符的 Unicode 范围是 \u4e00-\u9fa5,但要加 u 修饰符 PHP 才知道你用的是 Unicode 编码。

坑 3:preg_match 和 preg_match_all 的区别

<?php
$text = "客户A:13812345678,客户B:13900001111";

// preg_match 只找第一个匹配
preg_match("/1[3-9]\d{9}/", $text, $single);
echo "preg_match 结果:" . $single[0] . "\n";  // 输出:13812345678

// preg_match_all 找所有匹配
preg_match_all("/1[3-9]\d{9}/", $text, $all);
print_r($all[0]);  // 输出:13812345678 和 13900001111

注意! 如果你只写 preg_match,只会有一个结果,不会报错,但你会漏掉后面的匹配。

坑 4:转义字符的正确姿势

<?php
// ❌ 错误示例:没转义特殊字符
$url = "https://example.com?a=1&b=2";
preg_match("/https:\/\/example\.com/", $url);  // 报错或匹配失败

// ✅ 正确示例:转义 . 和 /
preg_match("/https:\/\/example\.com\//", $url);  // 匹配成功

注意! 正则里的 ./ 都有特殊含义,需要用 \ 转义。

坑 5:mb_strlen vs strlen 的坑

<?php
$text = "你好123";

// ❌ 错误:用 strlen 判断「字符数」
if (strlen($text) > 5) {  // strlen = 9(中文3字节×2 + 数字3)
echo "超过5个字符";  // 会错误触发
}

// ✅ 正确:用 mb_strlen 判断「字符数」
if (mb_strlen($text) > 5) {  // mb_strlen = 5
echo "超过5个字符";  // 不会触发
}

性能小贴士:正则预编译

如果一个正则要执行上万次,可以预编译:

<?php
// ❌ 低效:每次调用都重新编译正则
for ($i = 0; $i < 10000; $i++) {
preg_match("/1[3-9]\d{9}/", $phones[$i]);
}

// ✅ 高效:预编译正则,只编译一次
$phone_pattern = "/1[3-9]\d{9}/";
for ($i = 0; $i < 10000; $i++) {
preg_match($phone_pattern, $phones[$i]);
}

原理:正则表达式需要「编译」成机器能懂的格式。编译一次用一万次,比每次都重新编译快很多。

调试技巧:print_r 看匹配结果

<?php
$text = "电话:13812345678,备用:18900001111";

preg_match_all("/1[3-9]\d{9}/", $text, $matches);

echo "所有匹配:\n";
print_r($matches);

echo "\n第一个分组:\n";
print_r($matches[0]);

echo "\n匹配个数:" . count($matches[0]);

注意! $matches 是一个二维数组,$matches[0] 才是所有匹配结果的数组。


✏️ 练习题

练习 1(2 分钟):手机号提取
- 输入:"订单客户:李女士,电话13512345678"
- 预期输出:13512345678
- 提示:用 preg_match 配合手机号正则

练习 2(3 分钟):邮箱格式校验
- 输入:["test@qq.com", "invalid", "a@b.c"]
- 预期输出:第一个有效,其他无效
- 提示:在项目 2 基础上改正则

练习 3(5 分钟):日期提取
- 输入:"2023年12月25日和2024-01-01都是重要日期"
- 预期输出:提取出两个日期
- 提示:\d{4}[年\-]\d{1,2}[月\-]\d{1,2} 能匹配两种格式

练习 4(8 分钟):敏感词过滤
- 输入:"这个文章太垃圾了,简直是垃圾中的战斗垃圾"
- 预期输出:"这个文章太***了,简直是***中的战斗***"
- 提示:用 preg_replace 配合数组形式的「敏感词列表」

练习 5(5 分钟):分析报错
- 代码:

<?php
preg_match("/[a-z]/", "你好ABC", $result);
echo $result[0];
  • 预期报错:Warning: Undefined array key 0
  • 提示:匹配不到时 $result 是空数组

作业:做一个「文本清洗与统计工具」

需求描述:写一个工具,处理一段用户反馈文本,提取关键信息并统计。

功能点
1. 从文本中提取所有手机号(11位,以1开头)
2. 从文本中提取所有邮箱
3. 统计「垃圾词」出现次数(至少5个词,如:垃圾、差、烂、骗、退款)
4. 输出清洗后的文本(敏感词用 * 替代)

加分项
1. 支持从文件读取文本
2. 支持自定义敏感词列表

验收标准
- 能跑起来
- 提取结果正确
- 有适当的注释

提交方式:评论区贴代码或 GitHub 链接


📚 总结

本文学了 3 件事
1. 字符串函数能对文本进行「切割、替换、截取」——用 explodepreg_replacemb_substr
2. 正则表达式是「万能筛子」——用 preg_match / preg_match_all 按规则提取
3. 中文处理要用 mb_ 系列函数——否则会出各种奇怪的 bug

延伸资源
- PHP 官方文档:PCRE 正则 ——最权威的参考
- 正则表达式 30 分钟入门 ——经典中文教程
- RegexOne ——交互式正则学习网站

互动钩子:你在实际工作中有没有遇到过「文本处理」的坑?比如邮箱格式校验漏了哪种边界情况?评论区聊聊,老粉优先回复!


下集预告:学会了处理字符串,下一步是什么?——把数据存到文件里! 下一章我们来解决「怎么把处理完的数据保存下来」「怎么批量读取一堆文件」的问题。敬请期待「第 3 章 3.4 文件读写与目录遍历」!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。