PHP 字符串截取:从 `substr` 到 `mb_substr`,深度掌握多字节字符处理技巧75


在 PHP 的日常开发中,对字符串进行处理是一项极其频繁且重要的任务。无论是显示文章摘要、解析日志文件、处理用户输入,还是从 URL 中提取信息,我们都可能需要从一个较长的字符串中精确地“截取”出我们需要的“子串”。掌握 PHP 中字符串子串截取的各种方法和技巧,对于编写健壮、高效且能够正确处理多语言(特别是中文)的应用程序至关重要。

本文将作为一篇全面的指南,深入探讨 PHP 中获取字符串子串的核心函数:`substr()` 和 `mb_substr()`。我们将从它们的基本用法开始,详细解析它们的参数,通过丰富的代码示例来演示不同场景下的应用,并特别强调在处理多字节字符(如 UTF-8 编码的中文、日文等)时需要注意的关键点和最佳实践。目标是让您不仅能使用这些函数,更能理解它们背后的原理,并在实际开发中游刃有余。

一、`substr()` 函数:基础字符串截取

`substr()` 是 PHP 中最基础也是最常用的字符串截取函数。它能够从一个字符串中返回指定长度的子串。然而,它的一个重要特性是它按字节进行操作,这在使用单字节编码(如 ASCII、ISO-8859-1)时工作良好,但在处理多字节编码(如 UTF-8)时可能会遇到问题。

1.1 `substr()` 的语法与参数解析


substr() 函数的语法如下:substr(string $string, int $start, ?int $length = null): string|false

$string:必需。要截取的原始字符串。
$start:必需。起始位置。可以是正数、负数或 0。

如果为正数,截取将从 `$string` 的 `$start` 偏移量开始。字符串中的第一个字符的偏移量是 0。
如果为负数,截取将从 `$string` 结尾处倒数 `$start` 个字符开始。例如,-1 表示最后一个字符,-2 表示倒数第二个字符。


$length:可选。截取的长度。

如果为正数,表示要截取的长度。
如果为负数,表示从 `$string` 的结尾处保留 `$length` 个字符。例如,-1 表示截取到倒数第二个字符。
如果省略,则截取从 `$start` 到 `$string` 结尾的所有字符。


返回值:成功则返回截取到的子字符串,失败(例如 `$start` 超出字符串长度)则返回 `false`。

1.2 `substr()` 的应用示例


我们通过几个例子来理解 `substr()` 的不同用法:<?php
$text = "Hello, world! This is PHP.";
// 1. 从开头截取指定长度
// 截取 "Hello"
echo "<p>示例1: " . substr($text, 0, 5) . "</p>"; // 输出: Hello
// 2. 从指定位置开始截取指定长度
// 截取 "world"
echo "<p>示例2: " . substr($text, 7, 5) . "</p>"; // 输出: world
// 3. 从指定位置截取到字符串末尾
// 截取 "This is PHP."
echo "<p>示例3: " . substr($text, 14) . "</p>"; // 输出: This is PHP.
// 4. 使用负数作为起始位置 (从末尾倒数)
// 截取 "PHP."
echo "<p>示例4: " . substr($text, -5) . "</p>"; // 输出: PHP.
// 5. 使用负数作为长度 (从起始位置截取,并从末尾排除指定长度)
// 截取 "Hello, world! This is" (排除最后 " PHP.")
echo "<p>示例5: " . substr($text, 0, -5) . "</p>"; // 输出: Hello, world! This is
// 6. 结合使用负数起始和负数长度
// 截取 "PHP" (从倒数第5个字符开始,到倒数第1个字符结束)
echo "<p>示例6: " . substr($text, -4, -1) . "</p>"; // 输出: PHP
?>

1.3 `substr()` 的局限性:多字节字符问题


正如前文所述,`substr()` 是按字节操作的。这意味着在处理像 UTF-8 编码的中文、日文、韩文等字符时,它可能会将一个多字节字符截断,导致乱码或不完整的字符。

一个英文字符通常占用一个字节,而一个 UTF-8 编码的中文字符通常占用 3 个字节。<?php
$chinese_text = "你好,世界!PHP 很棒。"; // 11个中文字符,1个英文句号,共12个字符,但字节数远超12
// 尝试使用 substr 截取前 5 个“字符”
// 预期是 "你好,世界"
echo "<p>原始字符串: " . $chinese_text . "</p>";
echo "<p>使用 substr(UTF-8编码,截取长度5): " . substr($chinese_text, 0, 5) . "</p>";
// 实际输出可能类似于乱码或者只显示 "你好,"的一部分,因为5个字节不足以组成完整的2个汉字
// 例如,如果一个汉字3字节,5字节只能截取1个半汉字,导致乱码。
?>

这种按字节截取而非按字符截取的问题,在现代 Web 开发中是无法接受的,因为 UTF-8 编码已成为主流。为了正确处理多字节字符,我们需要使用 `mb_substr()` 函数。

二、`mb_substr()` 函数:多字节安全截取

`mb_substr()` 是 PHP 多字节字符串函数(MultiByte String Functions)扩展库的一部分。它专门设计用于正确处理各种多字节编码的字符串,如 UTF-8、GBK、Shift_JIS 等,确保字符串截取是基于字符而非字节进行的。

2.1 `mb_substr()` 的语法与参数解析


mb_substr() 函数的语法如下:mb_substr(string $string, int $start, ?int $length = null, ?string $encoding = null): string|false

$string:必需。要截取的原始字符串。
$start:必需。起始位置。与 `substr()` 类似,可以是正数、负数或 0。不同之处在于,这里的偏移量是按字符计算的。
$length:可选。截取的长度。与 `substr()` 类似,可以是正数、负数或省略。这里的长度是按字符计算的。
$encoding:可选。要使用的字符编码。如果省略,则使用内部字符编码(由 `mb_internal_encoding()` 设置)。强烈建议显式指定编码,通常是 'UTF-8'。
返回值:成功则返回截取到的子字符串,失败则返回 `false`。

2.2 `mb_substr()` 的应用示例


我们来看 `mb_substr()` 如何优雅地解决多字节字符截取问题:<?php
$chinese_text = "你好,世界!PHP 很棒。";
// 确保使用 UTF-8 编码
mb_internal_encoding("UTF-8"); // 或者直接在函数参数中指定
echo "<p>原始字符串: " . $chinese_text . "</p>";
// 1. 显式指定编码截取前 5 个字符
// 截取 "你好,世界"
echo "<p>使用 mb_substr (显式编码,截取长度5): " . mb_substr($chinese_text, 0, 5, 'UTF-8') . "</p>"; // 输出: 你好,世界!
// 2. 使用内部编码截取
echo "<p>使用 mb_substr (内部编码,截取长度5): " . mb_substr($chinese_text, 0, 5) . "</p>"; // 输出: 你好,世界!
// 3. 从末尾开始截取 (按字符)
// 截取 "PHP 很棒。" (中文1个字符,英文3个字符,中文3个字符,英文1个字符,共8个字符)
echo "<p>使用 mb_substr (负数起始,截取最后8个字符): " . mb_substr($chinese_text, -8, null, 'UTF-8') . "</p>"; // 输出: PHP 很棒。
// 4. 从开头截取,并从末尾保留指定字符数
// 截取 "你好,世界!" (保留最后 8 个字符 "PHP 很棒。")
echo "<p>使用 mb_substr (正数起始,负数长度,保留最后8个字符): " . mb_substr($chinese_text, 0, -8, 'UTF-8') . "</p>"; // 输出: 你好,世界!
?>

可以看到,`mb_substr()` 能够正确地处理多字节字符,确保截取操作是按照我们期望的“字符”逻辑进行的,而不是原始的“字节”逻辑。这对于全球化和多语言应用程序来说是必不可少的。

2.3 `mb_internal_encoding()` 的重要性


如果您在 `mb_substr()` 中不指定 `$encoding` 参数,它将使用由 `mb_internal_encoding()` 函数设定的内部编码。因此,在应用程序的入口点(例如 `` 或公共配置文件)设置正确的内部编码是一个非常好的习惯:<?php
mb_internal_encoding("UTF-8");
mb_regex_encoding("UTF-8"); // 如果也使用多字节正则表达式,也应设置
// 此后所有不指定编码的 mb_functions 都会默认使用 UTF-8
$str = "示例文本";
echo mb_substr($str, 0, 2); // 此时默认使用 UTF-8
?>

尽管如此,在每个 `mb_substr()` 调用中显式指定编码(例如 `mb_substr($str, 0, 5, 'UTF-8')`)可以增加代码的清晰度和鲁棒性,因为它不受全局内部编码设置的影响。

三、高级字符串子串截取场景

除了直接截取,我们常常需要结合其他字符串函数来实现更复杂的子串提取逻辑。

3.1 结合 `strpos()` / `mb_strpos()` 进行截取


当我们需要提取特定分隔符之间的内容,或者从某个标志位开始截取时,`strpos()`(或 `mb_strpos()`)就变得非常有用。它用于查找子串在主字符串中首次出现的位置。<?php
$log_entry = "[INFO] 2023-10-27 10:30:00 - User 'Alice' logged in from 192.168.1.1.";
// 提取日志消息本身(去除时间戳和用户信息前的部分)
$start_pos = strpos($log_entry, "- ") + 2; // 找到 "- " 的位置,并跳过这两个字符
$message = substr($log_entry, $start_pos);
echo "<p>日志消息: " . $message . "</p>"; // 输出: User 'Alice' logged in from 192.168.1.1.
// 提取文件名后缀
$filename = "";
$dot_pos = strrpos($filename, '.'); // strrpos() 查找最后一次出现的位置
if ($dot_pos !== false) {
$extension = substr($filename, $dot_pos + 1);
echo "<p>文件后缀: " . $extension . "</p>"; // 输出: pdf
}
// 结合 mb_strpos() 处理中文分隔符
$data_string = "姓名:张三|年龄:30|城市:北京";
$name_start = mb_strpos($data_string, "姓名:") + mb_strlen("姓名:");
$name_end = mb_strpos($data_string, "|", $name_start); // 从 $name_start 开始查找 '|'
if ($name_start !== false && $name_end !== false) {
$name = mb_substr($data_string, $name_start, $name_end - $name_start, 'UTF-8');
echo "<p>提取姓名: " . $name . "</p>"; // 输出: 张三
}
?>

3.2 截取并添加省略号(...)


在显示文章摘要或产品描述时,我们通常需要限制字符串的长度,并在截取后添加省略号。这需要结合 `mb_strlen()` 来获取正确的字符长度。<?php
function truncate_string_with_ellipsis(string $text, int $max_length = 100, string $encoding = 'UTF-8'): string {
if (mb_strlen($text, $encoding) <= $max_length) {
return $text;
}
return mb_substr($text, 0, $max_length, $encoding) . '...';
}
$long_text = "这是一段非常长中文本,用于测试字符串截取功能,并且要在末尾添加省略号以示其内容被缩短了。在网站中经常会看到这样的摘要显示。";
echo "<p>截取前: " . $long_text . "</p>";
echo "<p>截取后 (30个字符): " . truncate_string_with_ellipsis($long_text, 30, 'UTF-8') . "</p>";
// 输出: 这是一段非常长中文本,用于测试字符串截取功能,并且要在末尾添加省略号以示其内容被缩短了...
?>

四、性能考量与最佳实践

在字符串截取操作中,虽然通常不是性能瓶颈,但在大规模数据处理或高并发场景下,仍有一些最佳实践值得注意:

知晓并统一编码: 这是最重要的。始终明确您的字符串编码,并在需要时显式指定编码。对于现代 Web 应用,推荐全程使用 UTF-8。


如果您的应用环境和数据库都是 UTF-8,那么建议设置 `mb_internal_encoding("UTF-8");`,并在所有 `mb_*` 函数中省略 `encoding` 参数,以保持代码简洁。
但如果您的应用需要处理多种编码(例如来自不同来源的数据),那么在每次调用 `mb_substr()` 时显式指定 `$encoding` 会更安全。


`substr()` 与 `mb_substr()` 的选择:
如果确定字符串只包含单字节字符(如纯英文和数字),或者您确实需要按字节操作(这通常是低级协议解析等特殊场景),使用 `substr()` 可能会稍快一点(因为不需要额外的编码解析)。
在任何可能包含多字节字符(如中文、日文、特殊符号等)的场景下,务必使用 `mb_substr()`。性能差异在大多数情况下可以忽略不计,而正确性远比微小的性能提升更重要。



边界条件处理: 截取操作应考虑字符串为空、`$start` 或 `$length` 超出字符串范围等情况。`substr()` 和 `mb_substr()` 在这些情况下会返回 `false` 或空字符串,或者截取到字符串的实际末尾。在生产代码中,应根据需求进行检查。 <?php
$empty_str = "";
echo "<p>空字符串截取: " . (mb_substr($empty_str, 0, 5) === '' ? '空字符串' : '非空') . "</p>"; // 输出: 空字符串
$short_str = "abc";
echo "<p>超长截取: " . mb_substr($short_str, 0, 10) . "</p>"; // 输出: abc (不会报错)
echo "<p>起始位置超出: " . (mb_substr($short_str, 10, 5) === false ? 'false' : '非false') . "</p>"; // 输出: false
?>


避免不必要的循环截取: 在处理大量字符串时,如果需要在循环中对每个字符串进行截取,确保每次操作都是必要的,避免重复计算。

五、总结

字符串截取是 PHP 编程中一项基本技能,但其背后涉及的字符编码问题往往容易被忽视,尤其是在处理多语言内容时。通过本文的深入学习,您应该已经掌握了:
`substr()` 函数的基本用法、参数及其按字节操作的特性。
`mb_substr()` 函数如何解决多字节字符截取问题,及其在处理多语言内容时的关键作用。
如何结合 `strpos()` / `mb_strpos()` 实现更灵活的子串提取逻辑。
截取字符串并添加省略号的常见实践。
在选择 `substr()` 或 `mb_substr()` 时,以及在处理编码时应遵循的最佳实践和性能考量。

记住,在大多数现代 Web 开发场景中,只要可能涉及非 ASCII 字符,就应该优先使用 `mb_substr()`,并始终注意字符串的编码。这将帮助您构建出更加健壮、全球化友好的 PHP 应用程序。

2025-11-04


上一篇:PHP文件流传输深度解析:高效、安全处理大文件的核心技术

下一篇:PHP字符串子串提取终极指南:从基础到多字节编码及性能优化