掌握 PHP 字符串截取:兼容中文、避免乱码与性能优化51


在Web开发中,字符串处理是一项非常基础且频繁的操作。无论是数据库中长文本的展示限制、用户界面(UI)的布局要求,还是纯粹的信息摘要,我们常常需要对字符串进行截取,以控制其长度。PHP作为一门强大的服务器端脚本语言,提供了多种字符串截取的方法。然而,面对不同字符编码,尤其是中文等多字节字符时,简单的截取方式往往会导致乱码或显示不完整的问题。本文将深入探讨PHP中字符串截取的各种姿势,从基础函数到多字节字符处理,再到高级应用场景和性能优化,旨在帮助开发者构建健壮、高效且兼容性强的字符串处理逻辑。

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

`substr()` 是PHP中最基本、最常用的字符串截取函数。它简单直观,适用于处理单字节字符(如ASCII编码的英文、数字和符号)。

1. `substr()` 的语法和用法


`substr()` 函数的语法如下:string substr ( string $string , int $start [, int $length ] )

`$string`: 必需。要截取的字符串。
`$start`: 必需。截取的起始位置。

如果为正数,则从字符串的开头算起,第一个字符的索引是0。
如果为负数,则从字符串的末尾算起。例如,-1表示倒数第一个字符,-2表示倒数第二个字符。

`$length`: 可选。要截取的字符串长度。

如果为正数,则从 `start` 位置截取 `length` 个字符。
如果为负数,则表示从 `start` 位置开始,截取到距离字符串末尾 `length` 个字符的位置。
如果省略,则从 `start` 位置开始截取到字符串的末尾。


2. `substr()` 的使用示例


以下是一些 `substr()` 的基本用法示例:<?php
$string = "Hello, PHP World!";
// 截取前5个字符
echo substr($string, 0, 5); // 输出: Hello
// 从第7个字符开始截取到末尾 (索引从0开始)
echo substr($string, 7); // 输出: PHP World!
// 从倒数第6个字符开始截取4个字符
echo substr($string, -6, 4); // 输出: Worl
// 从倒数第10个字符开始截取到倒数第2个字符
echo substr($string, -10, -2); // 输出: P World
// 截取超出字符串长度的范围,不会报错,只会返回可用部分
echo substr($string, 0, 100); // 输出: Hello, PHP World! (返回整个字符串)
// 起始位置超出字符串长度,返回空字符串
echo substr($string, 50, 10); // 输出: (空字符串)
?>

3. `substr()` 在多字节字符(中文)处理上的局限性


`substr()` 函数在处理多字节字符集(如UTF-8编码的中文、日文、韩文等)时会遇到严重问题。这是因为 `substr()` 是按字节(byte)进行截取的,而不是按字符(character)进行截取。一个中文字符在UTF-8编码下通常占用3个字节,如果截取长度不刚好是3的倍数,就会导致一个中文字符被截断,从而出现乱码。

例如:<?php
$chinese_string = "你好,世界!PHP字符串截取示例。";
// 尝试使用 substr 截取前7个“字符”
// 预期:你好,世界!
// 实际:截取21个字节,可能导致“截”字被截断,出现乱码
echo substr($chinese_string, 0, 21); // 假设一个中文3字节,7个中文是21字节
// 实际输出可能类似:你好,世界!PHP字符� (最后一个字乱码)
?>

这就是为什么在处理包含中文等语言的Web应用中,我们不能直接使用 `substr()` 进行长度控制的原因。

二、解决多字节字符问题:`mb_substr()` 函数

为了解决 `substr()` 在多字节字符处理上的局限性,PHP提供了多字节字符串函数库(MultiByte String Functions),其中 `mb_substr()` 是专门用于多字节字符截取的函数。

1. `mb_substr()` 的必要性与优势


`mb_substr()` 函数会正确地识别并处理多字节字符,它根据字符而不是字节来计算长度和截取,从而避免了乱码问题,确保了截取内容的完整性和正确性。

2. `mb_substr()` 的语法和用法


`mb_substr()` 函数的语法如下:string mb_substr ( string $string , int $start [, int $length = NULL [, string $encoding = NULL ]] )

`$string`: 必需。要截取的字符串。
`$start`: 必需。截取的起始位置(字符索引)。

与 `substr()` 类似,正数从开头算起,负数从末尾算起。

`$length`: 可选。要截取的字符串长度(字符数)。

与 `substr()` 类似,正数表示长度,负数表示距离末尾的字符数。
如果省略,则从 `start` 位置开始截取到字符串的末尾。

`$encoding`: 可选。指定字符编码。如果省略,则使用内部字符编码设置(`mb_internal_encoding()`)。强烈建议明确指定编码,通常是 'UTF-8'。

3. `mb_substr()` 的使用示例


以下是 `mb_substr()` 处理中文的示例:<?php
// 推荐在应用入口处设置内部字符编码,确保所有mb_*函数都使用正确的编码
mb_internal_encoding("UTF-8");
$chinese_string = "你好,世界!PHP字符串截取示例。";
// 使用 mb_substr 截取前7个字符
echo mb_substr($chinese_string, 0, 7, "UTF-8"); // 输出: 你好,世界!
echo "<br>";
// 从第5个字符开始截取3个字符
echo mb_substr($chinese_string, 4, 3, "UTF-8"); // 输出: 世界!
echo "<br>";
// 从倒数第6个字符开始截取
echo mb_substr($chinese_string, -6, null, "UTF-8"); // 输出: 符串截取示例。
echo "<br>";
// 获取字符串的字符长度
echo mb_strlen($chinese_string, "UTF-8"); // 输出: 15 (包括中文、英文和标点)
?>

可以看到,`mb_substr()` 能够正确处理中文字符,避免了乱码问题。因此,在开发Web应用时,只要涉及到用户输入或多语言内容,几乎都应该优先使用 `mb_*` 系列函数,尤其是 `mb_substr()` 和 `mb_strlen()`。

三、实用场景与进阶技巧

仅仅截取字符串往往不能满足所有需求,我们还需要根据具体场景进行一些进阶处理。

1. 添加省略号 (...)


当字符串被截断时,通常需要添加省略号(`...`)来提示用户内容不完整。这是一种非常常见的需求。<?php
function truncateWithEllipsis(string $text, int $maxLength, string $encoding = 'UTF-8'): string {
if (mb_strlen($text, $encoding) <= $maxLength) {
return $text;
}
// 确保省略号也占用字符长度,如果总长度为10,省略号占3,则实际截取7个字符
$truncated = mb_substr($text, 0, $maxLength - 3, $encoding);
return $truncated . '...';
}
mb_internal_encoding("UTF-8");
$long_text_en = "This is a very long English text that needs to be truncated for display purposes.";
$long_text_zh = "这是一段非常长的中文文本,需要在显示时进行截断并添加省略号,以保持版面整洁。";
echo truncateWithEllipsis($long_text_en, 20); // 输出: This is a very lon...
echo "<br>";
echo truncateWithEllipsis($long_text_zh, 10); // 输出: 这是一段非常长的中...
echo "<br>";
echo truncateWithEllipsis($long_text_zh, 5); // 输出: 这是一... (只截取了2个字)
?>

在上述 `truncateWithEllipsis` 函数中,我们预留了3个字符的长度给省略号。需要注意的是,当 `$maxLength` 过小时,可能会导致截取到的实际内容非常少,甚至只剩下省略号。可以根据需求调整逻辑,例如,如果 `$maxLength` 小于等于3,直接返回省略号或空字符串。

2. 按单词截取 (Word-safe Truncation)


对于英文等语言,直接在单词中间截断会影响阅读体验。按单词截取的目标是确保截取点位于单词边界,避免将一个单词劈开。<?php
function truncateWords(string $text, int $maxLength, string $encoding = 'UTF-8'): string {
if (mb_strlen($text, $encoding) <= $maxLength) {
return $text;
}
$truncated = mb_substr($text, 0, $maxLength, $encoding);

// 查找最后一个空格的位置
$lastSpace = mb_strrpos($truncated, ' ', 0, $encoding);
if ($lastSpace !== false) {
// 如果截取到的部分包含空格,则截取到最后一个空格
return mb_substr($truncated, 0, $lastSpace, $encoding) . '...';
} else {
// 如果截取到的部分没有空格(比如一个超长的单词),则直接按字符截取
// 并且如果截取后的长度仍大于 maxLength,则再次截取
if (mb_strlen($truncated, $encoding) > $maxLength - 3) {
return mb_substr($text, 0, $maxLength - 3, $encoding) . '...';
}
return $truncated . '...';
}
}
mb_internal_encoding("UTF-8");
$text1 = "This is a very long sentence that needs to be truncated gracefully by words.";
$text2 = "AnExtremelyLongWordWithoutAnySpacesForTestingWordSafeTruncation";
echo truncateWords($text1, 30); // 输出: This is a very long sentence...
echo "<br>";
echo truncateWords($text2, 30); // 输出: AnExtremelyLongWordWithoutAn... (此处无法按单词截取,因为没有空格)
echo "<br>";
echo truncateWords("你好,世界!PHP字符串截取示例。", 10); // 对于中文,没有“单词”的概念,此函数会退化为普通截取
?>

注意:对于中文等没有显式单词分隔符(空格)的语言,这种按单词截取的方法意义不大,会退化为按字符截取。

3. 截取中间部分


有时我们需要展示一个长字符串的开头和结尾,中间部分用省略号代替,常见于文件路径、哈希值或URL等场景。<?php
function truncateMiddle(string $text, int $totalLength, string $encoding = 'UTF-8'): string {
$currentLength = mb_strlen($text, $encoding);
if ($currentLength <= $totalLength) {
return $text;
}
// 至少需要3个字符给省略号
if ($totalLength <= 3) {
return mb_substr($text, 0, $totalLength, $encoding); // 或直接返回 "..."
}
$partLength = floor(($totalLength - 3) / 2); // 计算前后两部分的长度
$start = mb_substr($text, 0, $partLength, $encoding);
$end = mb_substr($text, $currentLength - ($totalLength - 3 - $partLength), $totalLength - 3 - $partLength, $encoding);
return $start . '...' . $end;
}
mb_internal_encoding("UTF-8");
$long_url = "/some/very/long/path/to/a/resource/that/is/deeply/nested/and/has/a/long/";
$hash_id = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2";
echo truncateMiddle($long_url, 50); // 输出: /.../
echo "<br>";
echo truncateMiddle($hash_id, 20); // 输出: a1b2c3d4e5...z6a7b8c9d0e1f2
?>

4. 处理HTML标签的安全截取


如果字符串包含HTML标签,直接截取可能会破坏标签结构,导致页面显示异常。处理这类问题通常有两种思路:

剥离HTML标签后截取:如果最终只需要纯文本内容,可以使用 `strip_tags()` 函数先移除HTML标签,然后再进行截取。<?php
$html_string = "<p>这是一个<strong>包含HTML标签</strong>的<a href="#">字符串</a>。</p>";
$plain_text = strip_tags($html_string);
echo truncateWithEllipsis($plain_text, 15); // 输出: 这是一个包含HTML标签的字符串...
?>


保留HTML标签的安全截取:这要复杂得多,因为需要解析HTML结构,在截取后自动闭合未完成的标签。PHP标准库没有直接提供这种功能,需要使用专门的HTML解析库或自定义逻辑。常见的解决方案包括:
使用 `DOMDocument` 类进行HTML解析和操作(复杂,但可靠)。
使用第三方库,如 `HTMLPurifier` 或 `voku/simple_html_dom` (这些库通常用于过滤和清理HTML,但也可以用于安全截取)。

一个简单的、但并非万无一失的思路是:先解析HTML,遍历节点并累加文本长度,当达到最大长度时停止,然后递归闭合所有未闭合的标签。这超出了本文作为“字符串截取”的主题范畴,且实现起来有诸多陷阱,不建议手动编写,而应依赖成熟的库。

四、性能考虑与最佳实践

在实际开发中,除了功能的正确性,性能和代码的可维护性也同样重要。

1. `substr()` 与 `mb_substr()` 的选择



单字节字符:如果确定字符串只包含单字节字符(如纯英文、数字),并且始终使用ISO-8859-1或类似编码,`substr()` 性能略优,因为它不涉及复杂的字节编码分析。
多字节字符(推荐):在绝大多数现代Web应用中,字符串都可能包含UTF-8编码的多字节字符(中文、表情符号等)。在这种情况下,始终使用 `mb_substr()`。 尽管 `mb_substr()` 可能会有轻微的性能开销,但它的正确性远比这点性能差异重要,且现代PHP版本对 `mb_*` 函数的优化已经非常出色,这种性能差异通常可以忽略不计。

2. 避免不必要的截取操作


在进行字符串截取之前,应该先判断字符串的实际长度是否已经小于或等于目标长度。如果已经足够短,就没有必要执行截取操作,直接返回原字符串即可,这能减少不必要的函数调用。<?php
function smartTruncate(string $text, int $maxLength, string $encoding = 'UTF-8'): string {
// 预留省略号的长度
$ellipsisLength = 3;
if ($maxLength <= $ellipsisLength) { // 如果最大长度小于等于省略号长度,直接截取并返回
return mb_substr($text, 0, $maxLength, $encoding);
}
if (mb_strlen($text, $encoding) <= $maxLength) {
return $text;
}

return mb_substr($text, 0, $maxLength - $ellipsisLength, $encoding) . '...';
}
?>

3. 设置正确的字符编码


无论使用 `mb_substr()` 还是其他 `mb_*` 函数,始终确保字符编码设置正确。可以通过以下两种方式:
全局设置:在应用入口文件(如 ``)或配置文件中通过 `mb_internal_encoding("UTF-8");` 设置。这样,所有未显式指定编码的 `mb_*` 函数都会使用此默认编码。
局部指定:在每次调用 `mb_substr()` 时显式指定 `$encoding` 参数,例如 `mb_substr($string, 0, 10, "UTF-8")`。这可以覆盖全局设置,但在代码中会显得冗余,通常作为保险措施。

推荐的方式是全局设置,并在关键处(如数据库连接、文件读写)再次确认编码,确保整个应用环境编码一致。

4. 封装成辅助函数


将常用的字符串截取逻辑(如添加省略号、按单词截取)封装成独立的辅助函数或类方法,可以提高代码的复用性、可读性和维护性。在大型项目中,这通常是一个好的实践。// 示例:可以创建一个 StringHelper 类
class StringHelper {
public static function truncate(string $text, int $maxLength, string $encoding = 'UTF-8'): string {
// ... 实现上述 smartTruncate 逻辑 ...
}
public static function truncateWords(string $text, int $maxLength, string $encoding = 'UTF-8'): string {
// ... 实现上述 truncateWords 逻辑 ...
}
// ... 其他字符串处理方法
}
// 使用
// echo StringHelper::truncate($myText, 50);
?>

5. 考虑空字符串和非字符串输入


在自定义截取函数时,要考虑输入字符串可能是空字符串(`""`)、`null` 或者其他非字符串类型。PHP的内置函数通常会进行类型转换或抛出警告,但在自定义函数中最好加入类型检查和异常处理,确保代码的健壮性。function safeTruncate(string $text = '', int $maxLength, string $encoding = 'UTF-8'): string {
if (!is_string($text)) {
// 或者抛出异常,或者将其转换为字符串
$text = (string)$text;
}
if (empty($text)) {
return '';
}
// ... 其他截取逻辑
}
?>

现代PHP(7.0+)可以利用类型声明(`string $text`)和严格模式(`declare(strict_types=1);`)来强制类型检查,减少此类错误。

五、总结

字符串截取是Web开发中一个看似简单实则充满细节的问题。对于单字节字符,`substr()` 函数足够胜任;但一旦涉及中文等多字节字符,务必使用 `mb_substr()` 来避免乱码和截取错误。在实际应用中,我们还需要结合业务需求,实现添加省略号、按单词截取、处理HTML等高级功能。通过将这些逻辑封装成可复用的辅助函数,并遵循正确的字符编码实践,可以大大提高代码的健壮性、可读性和开发效率。

记住,在处理字符串时,尤其是Web应用,默认使用UTF-8编码,并优先考虑 `mb_*` 系列函数,是确保国际化和多语言内容正确显示的关键。

2025-10-11


上一篇:PHP 文件操作最佳实践:构建安全、高效且可扩展的读写封装类

下一篇:PHP高效获取MySQL数据库外键信息:方法、应用与最佳实践