PHP字符串截取深度解析:从基础到高级,掌握多字节字符与优雅截断技巧77


在Web开发中,尤其是使用PHP构建动态网站时,字符串截取是一个极其常见且重要的操作。无论是文章摘要、商品描述、用户评论的预览,还是页面标题的字数限制,我们都需要将过长的字符串截断到指定长度。然而,这看似简单的操作背后,却隐藏着不少细节和“坑”,尤其是涉及到多字节字符(如中文、日文、韩文等)和HTML内容时。作为一名专业的程序员,理解并熟练掌握PHP字符串截取的各种方法和最佳实践至关至要。

本文将从最基础的PHP字符串截取函数`substr()`开始,逐步深入到处理多字节字符的`mb_substr()`,再到如何实现带省略号的优雅截断、如何处理单词边界,以及如何安全地截取包含HTML标签的字符串。最终,我们将整合这些技巧,提供一个功能全面的字符串截取函数,帮助您从容应对各种字符串截取场景。

一、基础截取函数:`substr()`——单字节字符的利器

`substr()`函数是PHP中最基础的字符串截取函数,适用于处理单字节字符集(如ASCII编码)的字符串。它的用法简单直观,但其对多字节字符的处理能力是有限的。

1. `substr()` 函数的语法和参数


substr(string $string, int $start, ?int $length = null): string

`$string`: 待截取的原始字符串。
`$start`: 截取起始位置。

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


`$length`: 截取长度(可选)。

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



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


以下是一些`substr()`函数的基本用法示例:<?php
$string = "Hello, world! This is PHP.";
// 1. 从开头截取指定长度
echo '<p>示例1 (从索引0开始截取5个字符): ' . substr($string, 0, 5) . '</p>'; // 输出: Hello
// 2. 从指定索引开始截取到末尾
echo '<p>示例2 (从索引7开始截取到末尾): ' . substr($string, 7) . '</p>'; // 输出: world! This is PHP.
// 3. 从指定索引开始截取指定长度
echo '<p>示例3 (从索引7开始截取5个字符): ' . substr($string, 7, 5) . '</p>'; // 输出: world
// 4. 使用负数作为起始位置
echo '<p>示例4 (从倒数5个字符开始截取): ' . substr($string, -5) . '</p>'; // 输出: is PHP.
// 5. 使用负数作为长度
echo '<p>示例5 (从索引7开始,截取到倒数5个字符之前): ' . substr($string, 7, -5) . '</p>'; // 输出: world! This
// 6. 截取超出字符串长度的范围
echo '<p>示例6 (截取超过实际长度): ' . substr($string, 0, 100) . '</p>'; // 输出: Hello, world! This is PHP. (返回原字符串)
// 7. 截取空字符串或起始位置超出范围
echo '<p>示例7 (截取空字符串): ' . substr("", 0, 5) . '</p>'; // 输出: (空字符串)
echo '<p>示例8 (起始位置超出范围): ' . substr($string, 100, 5) . '</p>'; // 输出: (空字符串)
?>

3. `substr()` 处理多字节字符的问题


`substr()` 函数是按字节进行截取的。对于UTF-8等变长编码的多字节字符,一个字符可能占用多个字节(例如,一个中文字符通常占用3个字节)。如果使用`substr()`截取包含多字节字符的字符串,可能会导致字符被“切半”,出现乱码。<?php
$chineseString = "你好,世界!PHP编程。"; // 这是一个包含中文的UTF-8字符串
echo '<p>原始字符串: ' . $chineseString . '</p>';
echo '<p>使用 substr(0, 5) 截取 (可能乱码): ' . substr($chineseString, 0, 5) . '</p>'; // 结果可能是“�好,�”或其他乱码
// 预期:前5个字符,但实际上是前5个字节
echo '<p>使用 substr(0, 6) 截取 (可能乱码): ' . substr($chineseString, 0, 6) . '</p>'; // 结果可能是“你好�”或其他乱码
// 预期:前6个字符,但实际上是前6个字节,正好是两个中文字符
?>

从上面的例子可以看出,`substr()`在处理中文字符时无法按照我们期望的“字符”数量进行截取,而是按照“字节”数量,这正是导致乱码的根本原因。

二、多字节字符截取:`mb_substr()`——国际化的选择

为了解决`substr()`在处理多字节字符时的乱码问题,PHP提供了`mb_substr()`函数(`mb_`系列函数是PHP的Multibyte String扩展的一部分,专门用于处理多字节字符串)。`mb_substr()`能够正确识别多字节字符,并按照实际的字符数量进行截取。

1. `mb_substr()` 函数的语法和参数


mb_substr(string $string, int $start, ?int $length = null, ?string $encoding = null): string

参数与`substr()`非常相似,但多了一个关键的`$encoding`参数:
`$string`: 待截取的原始字符串。
`$start`: 截取起始位置(字符数)。
`$length`: 截取长度(字符数,可选)。
`$encoding`: 指定字符串的编码(可选)。如果省略,则使用内部字符编码(`mb_internal_encoding()`设置的编码)。通常建议明确指定为`'UTF-8'`。

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


我们用`mb_substr()`来重新截取上面的中文字符串:<?php
$chineseString = "你好,世界!PHP编程。";
// 确保设置内部编码或在函数中明确指定编码,通常推荐明确指定
// mb_internal_encoding("UTF-8");
echo '<p>原始字符串: ' . $chineseString . '</p>';
// 按照字符数截取
echo '<p>使用 mb_substr(0, 2, 'UTF-8') 截取: ' . mb_substr($chineseString, 0, 2, 'UTF-8') . '</p>'; // 输出: 你好
echo '<p>使用 mb_substr(3, 2, 'UTF-8') 截取: ' . mb_substr($chineseString, 3, 2, 'UTF-8') . '</p>'; // 输出: 世界
echo '<p>使用 mb_substr(-5, null, 'UTF-8') 截取: ' . mb_substr($chineseString, -5, null, 'UTF-8') . '</p>'; // 输出: PHP编程。
// 即使内部编码未设置,明确指定编码也能正常工作
echo '<p>使用 mb_substr(0, 5, 'UTF-8') 截取: ' . mb_substr($chineseString, 0, 5, 'UTF-8') . '</p>'; // 输出: 你好,世界
?>

从结果可以看出,`mb_substr()`完美地解决了多字节字符的截取问题。在现代Web开发中,尤其是在处理国际化内容时,强烈建议始终使用`mb_substr()`来截取字符串,并明确指定编码为`'UTF-8'`,以避免潜在的乱码问题。

三、实战技巧:带省略号的优雅截断

仅仅截取字符串往往是不够的,通常我们还需要在截断后添加省略号(`...`)来提示用户内容还有更多。这需要一些简单的逻辑判断。

1. 基础的带省略号截断函数


我们可以封装一个简单的函数来实现这个功能:<?php
/
* 截取字符串并添加省略号
*
* @param string $string 原始字符串
* @param int $maxLength 最大长度(字符数)
* @param string $suffix 省略号后缀,默认为 '...'
* @return string 截取后的字符串
*/
function truncateStringWithEllipsis(string $string, int $maxLength, string $suffix = '...'): string
{
// 如果字符串为空,直接返回空
if (empty($string)) {
return '';
}
// 考虑多字节字符,使用 mb_strlen 获取字符长度
if (mb_strlen($string, 'UTF-8') > $maxLength) {
// 使用 mb_substr 截取指定长度,并拼接省略号
return mb_substr($string, 0, $maxLength, 'UTF-8') . $suffix;
}
// 字符串未超出最大长度,直接返回原始字符串
return $string;
}
$longText = "PHP是一种广泛使用的开源通用脚本语言,特别适用于Web开发,可嵌入HTML中。";
$shortText = "PHP是最好的语言。";
echo '<p>长文本截断 (20字符): ' . truncateStringWithEllipsis($longText, 20) . '</p>';
// 预期输出: PHP是一种广泛使用的开源通用脚本语言,特别适用于Web开发,可嵌入HTML中。...
echo '<p>短文本截断 (20字符): ' . truncateStringWithEllipsis($shortText, 20) . '</p>';
// 预期输出: PHP是最好的语言。
$anotherLongText = "This is a very very long string that needs to be truncated for display purposes.";
echo '<p>英文长文本截断 (20字符): ' . truncateStringWithEllipsis($anotherLongText, 20) . '</p>';
// 预期输出: This is a very ver...
?>

四、进阶技巧:保持单词边界的截断

在截取英文或其他以空格分隔单词的字符串时,如果截断位置恰好在一个单词的中间,可能会导致截断后的文本可读性下降,例如“This is a lo...”看起来就不够优雅。为了解决这个问题,我们可以在截断前尝试找到最后一个完整的单词边界。

1. 如何找到单词边界


通常,单词边界就是空格。我们可以使用`strrpos()`(或`mb_strrpos()`用于多字节字符串)函数来查找指定长度内最后一个空格的位置。

2. 保持单词边界的截断函数


<?php
/
* 截取字符串并保持单词边界,同时添加省略号
*
* @param string $string 原始字符串
* @param int $maxLength 最大长度(字符数),不包含省略号
* @param string $suffix 省略号后缀,默认为 '...'
* @return string 截取后的字符串
*/
function truncateStringWithWordBoundary(string $string, int $maxLength, string $suffix = '...'): string
{
if (empty($string)) {
return '';
}
$stringLength = mb_strlen($string, 'UTF-8');
// 如果字符串未超出最大长度,直接返回
if ($stringLength <= $maxLength) {
return $string;
}
// 截取到最大长度(不包含后缀)
$truncated = mb_substr($string, 0, $maxLength, 'UTF-8');
// 查找最后一个空格的位置
// 注意:这里的 mb_strrpos 应该在截断后的字符串中查找,而不是原始字符串
$lastSpace = mb_strrpos($truncated, ' ', 0, 'UTF-8');
// 如果找到了空格且不是在开头(避免截断为空),则从最后一个空格处截断
if ($lastSpace !== false && $lastSpace > 0) {
// 确保截断后的长度不会太短,至少包含一些字符
// 避免出现 "..." 的情况,可以设置一个最小截断长度
if (mb_strlen(mb_substr($truncated, 0, $lastSpace, 'UTF-8'), 'UTF-8') > $maxLength / 2) { // 简单判断,确保有一定长度
$truncated = mb_substr($truncated, 0, $lastSpace, 'UTF-8');
}
}

return $truncated . $suffix;
}
$longEnglishText = "This is a very very long string that needs to be truncated for display purposes and good readability.";
$anotherLongEnglishText = "HelloWorldIsAGoodGame"; // 没有空格的字符串
echo '<p>英文文本截断 (带单词边界, 20字符): ' . truncateStringWithWordBoundary($longEnglishText, 20) . '</p>';
// 预期输出: This is a very very... (保留了"very"这个单词)
echo '<p>英文文本截断 (带单词边界, 30字符): ' . truncateStringWithWordBoundary($longEnglishText, 30) . '</p>';
// 预期输出: This is a very very long string...
echo '<p>无空格英文文本截断 (20字符): ' . truncateStringWithWordBoundary($anotherLongEnglishText, 20) . '</p>';
// 预期输出: HelloWorldIsAGoodGa... (没有空格时按字符截断)
$chineseTextForWordBoundary = "PHP是最好的语言,我爱PHP,PHP世界第一。";
echo '<p>中文文本截断 (带单词边界, 10字符): ' . truncateStringWithWordBoundary($chineseTextForWordBoundary, 10) . '</p>';
// 对于中文,"单词"边界不明显,此函数行为会退化为普通截断,这通常是可接受的。
// 预期输出: PHP是最好的语言,我爱...
?>

这个函数会先按最大长度截取,然后检查截取结果中最后一个空格的位置。如果找到了,就会截断到这个空格之前,确保不会切断单词。

五、复杂场景:安全截取包含HTML的字符串

如果字符串中包含HTML标签,直接截取可能会导致HTML结构被破坏,例如 `

这是一段文字

` 截取后变成 `

这是一段文...

`,标签没有闭合,可能导致页面显示异常。

1. 简单粗暴:先去除HTML标签再截取


最简单也是最常用的方法是,在截取之前先使用`strip_tags()`函数去除所有的HTML标签。这样可以确保截取后的文本是纯文本,不会破坏HTML结构。<?php
/
* 截取包含HTML标签的字符串,先去除HTML标签
*
* @param string $htmlString 包含HTML的原始字符串
* @param int $maxLength 最大长度(字符数)
* @param string $suffix 省略号后缀,默认为 '...'
* @return string 截取后的纯文本字符串
*/
function truncateHtmlStripTags(string $htmlString, int $maxLength, string $suffix = '...'): string
{
// 1. 去除HTML标签,得到纯文本
$plainText = strip_tags($htmlString);
// 2. 使用之前实现的带省略号的截断函数处理纯文本
return truncateStringWithEllipsis($plainText, $maxLength, $suffix);
}
$htmlContent = '<p><strong>这是一段非常重要的文本</strong>,其中包含了一些<em>HTML标签</em>,我们需要对它进行截取以显示摘要。</p><a href="#">点击查看更多</a>';
echo '<p>原始HTML内容: ' . htmlspecialchars($htmlContent) . '</p>'; // 使用 htmlspecialchars 显示原始HTML避免浏览器渲染
echo '<p>去除HTML后截断 (30字符): ' . truncateHtmlStripTags($htmlContent, 30) . '</p>';
// 预期输出: 这是一段非常重要的文本,其中包含了一些HTML标签,我们需要对它进行截取以显示摘要。...
?>

这种方法虽然简单有效,但缺点是会丢失所有HTML格式。如果需要保留部分HTML格式,则需要更复杂的解决方案。

2. 复杂方案:HTML-Aware截断(提及,不详述)


如果希望在截取HTML内容时还能保留其内部的有效HTML结构(即确保所有打开的标签都能正确闭合),这是一个相当复杂的问题,通常需要:
使用DOM解析器(如PHP的DOMDocument或第三方库)来解析HTML。
遍历DOM树,计算纯文本字符数。
在达到指定长度时,截断DOM树,并递归地闭合所有未闭合的父标签。

这类解决方案通常需要引入专门的HTML处理库(例如`HTML Purifier`库有时也提供这类功能,或者需要自定义实现)。由于其复杂性远超本文范围,在此仅作提及。

六、综合实践:一个功能强大的通用截取函数

为了在实际项目中更方便地使用字符串截取功能,我们可以将上述所有逻辑封装到一个通用的函数中。这个函数可以根据参数灵活地支持多字节字符、添加省略号、保持单词边界,以及可选地去除HTML标签。<?php
/
* 通用字符串截取函数,支持多字节字符、省略号、单词边界和HTML标签处理。
*
* @param string $string 原始字符串
* @param int $maxLength 最大长度(字符数),不包含省略号
* @param string $suffix 省略号后缀,默认为 '...'
* @param bool $preserveWords 是否尝试保持单词边界(主要对英文有效)
* @param bool $stripTags 是否去除HTML标签
* @return string 截取后的字符串
*/
function smartTruncate(
string $string,
int $maxLength,
string $suffix = '...',
bool $preserveWords = false,
bool $stripTags = false
): string {
if (empty($string)) {
return '';
}
// 1. 如果需要,先去除HTML标签
if ($stripTags) {
$string = strip_tags($string);
}
$stringLength = mb_strlen($string, 'UTF-8');
// 2. 如果字符串未超出最大长度,直接返回
if ($stringLength <= $maxLength) {
return $string;
}
// 3. 截取到最大长度
$truncated = mb_substr($string, 0, $maxLength, 'UTF-8');
// 4. 如果需要,尝试保持单词边界
if ($preserveWords) {
// 查找截断后的字符串中最后一个空格的位置
$lastSpace = mb_strrpos($truncated, ' ', 0, 'UTF-8');
// 如果找到了空格且不是在开头(避免截断为空),则从最后一个空格处截断
if ($lastSpace !== false && $lastSpace > 0) {
// 确保截断后的长度不会太短,至少包含一些字符
if (mb_strlen(mb_substr($truncated, 0, $lastSpace, 'UTF-8'), 'UTF-8') > $maxLength / 2) {
$truncated = mb_substr($truncated, 0, $lastSpace, 'UTF-8');
}
}
}
// 5. 拼接省略号
return $truncated . $suffix;
}
// 示例用法
$longChineseText = "PHP是一种广泛使用的开源通用脚本语言,特别适用于Web开发,可嵌入HTML中。它的语法吸收了C语言、Java和Perl的特点,易于学习,使用广泛,主要适用于Web开发领域。";
$longEnglishText = "This is a very very long string that needs to be truncated for display purposes and good readability in a web application context.";
$htmlContentExample = '<p><strong>这是一段包含HTML的摘要</strong>,<em>非常重要</em>,需要进行安全截断以便在列表页显示。</p><a href="#">点击这里查看原文</a>。';
echo '<h3>通用截取函数示例</h3>';
echo '<p>1. 中文文本 (30字符,默认): ' . smartTruncate($longChineseText, 30) . '</p>';
// 预期输出: PHP是一种广泛使用的开源通用脚本语言,特别适用于Web开发,可嵌入HTML中。它的语法吸...
echo '<p>2. 英文文本 (20字符,保持单词边界): ' . smartTruncate($longEnglishText, 20, '...', true) . '</p>';
// 预期输出: This is a very very...
echo '<p>3. 英文文本 (20字符,不保持单词边界): ' . smartTruncate($longEnglishText, 20, '...', false) . '</p>';
// 预期输出: This is a very very... (这个例子中刚好是单词边界)
echo '<p>4. HTML内容 (40字符,去除HTML): ' . smartTruncate($htmlContentExample, 40, '...', false, true) . '</p>';
// 预期输出: 这是一段包含HTML的摘要,非常重要,需要进行安全截断以便在列表页显示。...
echo '<p>5. HTML内容 (20字符,去除HTML,短后缀): ' . smartTruncate($htmlContentExample, 20, '...', false, true) . '</p>';
// 预期输出: 这是一段包含HTML的摘要,非常重...
echo '<p>6. 空字符串处理: ' . smartTruncate("", 10) . ' (空字符串)</p>';
?>

七、最佳实践与注意事项

在实际开发中,除了掌握上述函数,还需要注意以下几点:
始终考虑编码:对于Web应用,UTF-8是事实上的标准。确保您的数据库、文件编码、PHP内部编码以及所有字符串操作都使用UTF-8。可以通过`mb_internal_encoding("UTF-8");`来设置PHP脚本的默认内部编码。
优先使用`mb_*`函数:为了兼容多字节字符,请优先使用`mb_strlen()`、`mb_substr()`、`mb_strrpos()`等`mb_`系列函数。除非您非常确定字符串只包含单字节字符(如纯ASCII),否则避免使用`strlen()`、`substr()`等非`mb_`函数。
确定最大长度的含义:截取长度是包含省略号还是不包含?通常我们指的是原始字符串的字符长度(不含省略号),最后再拼接省略号。
处理空字符串和短字符串:确保您的截取逻辑能够正确处理空字符串或长度小于最大长度的字符串,避免不必要的截取或添加省略号。
性能考虑:对于非常大的字符串或在循环中频繁截取,`mb_*`函数可能会比对应的单字节函数略慢。但在大多数Web应用场景下,这种性能差异微乎其微,不应成为放弃多字节字符支持的理由。
前端配合:有时,字符串截取也可以在前端JavaScript层面完成,但为了SEO和内容一致性,后端截取仍然是首选。


PHP字符串截取是Web开发中一项基础而重要的技能。通过本文的深入探讨,我们了解了从基础的`substr()`到处理多字节字符的`mb_substr()`,以及如何实现带省略号的优雅截断、保持单词边界的截断和处理HTML内容的复杂场景。最终提供的`smartTruncate`函数,集成了多种常用功能,为您的项目提供了强大的字符串截取解决方案。

掌握这些技巧,您将能够更专业、更健壮地处理各种字符串截取需求,确保您的Web应用在展示文本内容时既美观又不会出现乱码或格式错误,从而提升用户体验。

2025-11-07


上一篇:PHP 文件缓存深度解析:从原理到实践,优化你的Web应用性能

下一篇:PHP高效生成随机汉字:从基础到实践的全面指南