多字节字符串 mbstring
概述
mbstring 扩展是 PHP 处理多字节字符编码(如 UTF-8、GBK、Big5 等)的核心工具。PHP 原生字符串函数按字节操作,对中文、日文、韩文等多字节字符会产生错误结果。mbstring 提供了对应的 mb_* 系列函数,按字符而非字节处理字符串。
基础概念
为什么需要 mbstring
UTF-8 编码中,不同字符占用的字节数不同:
| 字符类型 | 字节数 | 示例 |
|---|---|---|
| ASCII 字母/数字 | 1 字节 | A (0x41) |
| 拉丁扩展 | 2 字节 | é (0xC3 0xA9) |
| 中文/日文/韩文 | 3 字节 | 你 (0xE4 0xBD 0xA0) |
| Emoji 表情 | 4 字节 | 😀 (0xF0 0x9F 0x98 0x80) |
原生函数 vs mbstring 函数对照表
| 操作 | 原生函数(字节) | mbstring 函数(字符) |
|---|---|---|
| 长度 | strlen | mb_strlen |
| 截取 | substr | mb_substr |
| 查找位置 | strpos | mb_strpos |
| 最后查找 | strrpos | mb_strrpos |
| 转小写 | strtolower | mb_strtolower |
| 转大写 | strtoupper | mb_strtoupper |
| 首字母大写 | ucfirst | mb_convert_case + MB_CASE_TITLE |
| 子串出现次数 | substr_count | mb_substr_count |
| 发送邮件 | mail | mb_send_mail |
语法与代码
mb_strlen — 获取字符数
php
<?php
declare(strict_types=1);
// mb_strlen(string $str, ?string $encoding = null): int
// 返回字符串的字符数(而非字节数)
echo strlen('Hello'); // 5(字节长度 = 字符长度)
echo mb_strlen('Hello'); // 5(字符长度)
echo strlen('你好世界'); // 12(4 个中文 x 3 字节)
echo mb_strlen('你好世界'); // 4(字符长度)
// 指定编码
echo mb_strlen('你好', 'UTF-8'); // 2
echo mb_strlen('你好', 'GBK'); // 2
// 使用 mb_internal_encoding 的默认编码
mb_internal_encoding('UTF-8');
echo mb_strlen('你好世界'); // 4mb_substr — 按字符截取
php
<?php
declare(strict_types=1);
// mb_substr(string $str, int $start, ?int $length, ?string $encoding): string
$str = '你好,世界!Hello!';
echo mb_substr($str, 0, 2); // 你好
echo mb_substr($str, 3, 2); // 世界
echo mb_substr($str, -6); // Hello!
echo mb_substr($str, -6, 5); // Hello
// 获取第一个字符
echo mb_substr('你好世界', 0, 1); // 你
// 获取最后三个字符
echo mb_substr('你好世界', -3); // 好世界
// 原生 substr 在多字节字符串上的错误结果
echo substr('你好世界', 0, 2); // (乱码:只截取了部分字节)mb_strpos — 按字符查找
php
<?php
declare(strict_types=1);
// mb_strpos(string $haystack, string $needle, int $offset, ?string $encoding): int|false
$str = '你好世界,Hello World';
echo mb_strpos($str, '世界'); // 2(字符位置)
echo mb_strpos($str, 'World'); // 8
echo mb_strrpos($str, '你'); // 0(最后出现位置)
// 使用偏移量
echo mb_strpos($str, 'o', 3); // 10(从第 3 个字符开始查找)
// mb_strpos 同样返回 false 或位置,需要严格比较
$pos = mb_strpos($str, 'PHP');
if ($pos !== false) {
echo "在位置 {$pos} 找到";
}mb_strtolower / mb_strtoupper — 大小写转换
php
<?php
declare(strict_types=1);
// 多字节安全的大小写转换
echo mb_strtoupper('hello world'); // HELLO WORLD
echo mb_strtolower('HELLO WORLD'); // hello world
// 对特殊字符的支持(原生函数不支持)
echo strtoupper('déjà vu'); // Déjà VU(部分字符不转换)
echo mb_strtoupper('déjà vu', 'UTF-8'); // Déjà VU -> 正确: Déjà VU
echo mb_strtolower('CAFÉ', 'UTF-8'); // café
echo mb_strtoupper('café', 'UTF-8'); // CAFé -> 正确: CAFé
// mb_convert_case - 更灵活的大小写转换
echo mb_convert_case('hello world', MB_CASE_UPPER, 'UTF-8'); // HELLO WORLD
echo mb_convert_case('HELLO WORLD', MB_CASE_LOWER, 'UTF-8'); // hello world
echo mb_convert_case('hello world', MB_CASE_TITLE, 'UTF-8'); // Hello World
// MB_CASE_TITLE 对多字节字符的支持
echo mb_convert_case('hello世界', MB_CASE_TITLE, 'UTF-8'); // Hello世界mb_substr_count — 子串计数
php
<?php
declare(strict_types=1);
// mb_substr_count — 统计子串出现次数(多字节安全)
$str = '你好你好,你好吗?';
echo mb_substr_count($str, '你好'); // 3
echo mb_substr_count($str, '?'); // 1
echo mb_substr_count($str, '你好吗'); // 1mb_internal_encoding — 设置默认编码
php
<?php
declare(strict_types=1);
// mb_internal_encoding — 设置/获取 mbstring 默认内部编码
// 获取当前编码
echo mb_internal_encoding(); // 例如: UTF-8
// 设置默认编码
mb_internal_encoding('UTF-8');
// 设置后,mb_* 函数可以省略编码参数
echo mb_strlen('你好世界'); // 4(使用 UTF-8)
// 通过 php.ini 设置
// mbstring.internal_encoding = UTF-8
// 检测字符串编码
$encoding = mb_detect_encoding($str, ['UTF-8', 'GBK', 'Big5', 'SJIS']);
echo $encoding; // 例如: UTF-8
// 编码转换
$gbk = mb_convert_encoding('你好', 'GBK', 'UTF-8');
$utf8 = mb_convert_encoding($gbk, 'UTF-8', 'GBK');详细说明
编码检测与转换
php
<?php
declare(strict_types=1);
// mb_detect_encoding — 检测字符串编码
// 注意:自动检测不完全可靠,优先指定编码
$enc = mb_detect_encoding($str, 'UTF-8, GBK, Big5, SJIS, ISO-8859-1');
// mb_convert_encoding — 编码转换
$utf8 = mb_convert_encoding($gbkStr, 'UTF-8', 'GBK');
$utf8 = mb_convert_encoding($anyStr, 'UTF-8', 'auto');
// mb_list_encodings — 列出支持的编码
$encodings = mb_list_encodings();
// ['UTF-8', 'UTF-7', 'ASCII', 'EUC-JP', 'SJIS', ...]HTTP 输入输出编码设置
php
<?php
declare(strict_types=1);
// 设置 HTTP 输入编码
mb_http_input('UTF-8');
// 设置 HTTP 输出编码
mb_http_output('UTF-8');
// 设置内部编码
mb_internal_encoding('UTF-8');
// 使用 mb_convert_variables 同时转换多个变量的编码
mb_convert_variables('UTF-8', 'GBK', $var1, $var2, $var3);
// 在 php.ini 中推荐配置
// mbstring.language = Chinese
// mbstring.internal_encoding = UTF-8
// mbstring.http_input = UTF-8
// mbstring.http_output = UTF-8mbstring 正则函数
php
<?php
declare(strict_types=1);
// mb_ereg 系列函数 — 多字节正则表达式
// mb_ereg — 正则匹配
if (mb_ereg('[\p{Han}]', '你好World')) {
echo '包含中文字符';
}
// mb_eregi — 不区分大小写的正则匹配
if (mb_eregi('hello', '你好HELLO世界')) {
echo '匹配成功';
}
// mb_ereg_replace — 正则替换
$result = mb_ereg_replace('[\s]+', ' ', '你好 世界'); // 你好 世界
// mb_split — 按正则分割
$parts = mb_split('[,,]', '你好,世界,Hello');
// ['你好', '世界', 'Hello']实战示例
中文字符串截断(不截断字符)
php
<?php
declare(strict_types=1);
function truncateChinese(string $str, int $maxLength, string $suffix = '...'): string
{
if (mb_strlen($str, 'UTF-8') <= $maxLength) {
return $str;
}
return mb_substr($str, 0, $maxLength - mb_strlen($suffix, 'UTF-8'), 'UTF-8') . $suffix;
}
echo truncateChinese('这是一段很长的中文字符串', 10);
// 输出: 这是一段很长的中文...
echo truncateChinese('短文本', 10);
// 输出: 短文本(不超过长度,不截断)中文字符串关键词高亮
php
<?php
declare(strict_types=1);
function highlightKeywords(string $text, array $keywords): string
{
foreach ($keywords as $keyword) {
if (mb_strlen($keyword, 'UTF-8') === 0) {
continue;
}
$text = mb_ereg_replace(
preg_quote($keyword, '/'),
'<mark>\0</mark>',
$text
);
}
return $text;
}
echo highlightKeywords('PHP是一种流行的编程语言', ['PHP', '编程语言']);
// 输出: <mark>PHP</mark>是一种流行的<mark>编程语言</mark>中文验证(姓名、手机号)
php
<?php
declare(strict_types=1);
function isChineseName(string $name): bool
{
// 2-4 个中文字符
return (bool)preg_match('/^[\x{4e00}-\x{9fa5}]{2,4}$/u', $name);
}
function isChineseText(string $text): bool
{
// 包含至少一个中文字符
return (bool)preg_match('/[\p{Han}]/u', $text);
}
function containsOnlyChinese(string $str): bool
{
// 仅包含中文字符
return (bool)preg_match('/^[\p{Han}]+$/u', $str);
}
echo isChineseName('张三'); // true
echo isChineseName('张'); // false(少于 2 个字符)
echo isChineseText('Hello世界'); // true
echo isChineseText('Hello'); // false多语言字符串统计
php
<?php
declare(strict_types=1);
function analyzeString(string $str): array
{
$chinese = preg_match_all('/[\p{Han}]/u', $str);
$english = preg_match_all('/[a-zA-Z]/', $str);
$digits = preg_match_all('/[0-9]/', $str);
$spaces = preg_match_all('/\s/', $str);
$punctuation = preg_match_all('/[^\w\s]/u', $str);
return [
'total_chars' => mb_strlen($str, 'UTF-8'),
'total_bytes' => strlen($str),
'chinese' => $chinese,
'english' => $english,
'digits' => $digits,
'spaces' => $spaces,
'punctuation' => $punctuation,
];
}
print_r(analyzeString('你好 World 2024!'));
// ['total_chars' => 14, 'total_bytes' => 18, 'chinese' => 2, 'english' => 5, 'digits' => 4, 'spaces' => 2, 'punctuation' => 1]注意事项
编码一致性
整个项目必须统一使用一种编码(推荐 UTF-8)。混合编码会导致乱码和难以调试的错误。确保 PHP 文件、数据库、HTTP 头部、HTML 页面都使用相同的编码。
mb_detect_encoding 不完全可靠
mb_detect_encoding() 使用启发式方法检测编码,对于短字符串或包含特定字符的字符串,检测结果可能不准确。在生产环境中,应明确知道编码来源,不要依赖自动检测。
mb_internal_encoding 全局影响
mb_internal_encoding() 设置的是全局默认编码,影响所有省略了编码参数的 mb_* 函数调用。在共享代码中修改全局编码可能导致其他代码出错。建议在调用时显式指定编码。
最佳实践
- 始终使用 UTF-8 编码:现代 PHP 项目应统一使用 UTF-8 编码
- 显式指定编码参数:在
mb_*函数调用中显式传递'UTF-8',不依赖全局设置 - 多字节字符串使用 mbstring:所有涉及中文、日文等非 ASCII 字符的操作都应使用
mb_*函数 - 设置 php.ini 编码配置:在 PHP 配置中统一设置
mbstring.internal_encoding - HTML 指定编码:使用
<meta charset="UTF-8">确保 HTML 页面编码正确 - 数据库连接指定编码:PDO 连接时指定 charset=utf8mb4