PHP 處理中文字符串截取時,substr() 按字節切割導致「半個漢字」亂碼。mb_substr() 和 mb_strcut() 為多字節編碼 (UTF-8, GBK 等) 提供編碼感知截取,但兩者核心邏輯不同:字符級 vs 字節級。本文深度解析原理、參數、邊界行為、性能與選型指南。
mb_substr() | mb_strcut() |
|------|------------------------|-------------------------|
| **截取單位** | **字符** (邏輯字符) | **字節** (物理字節) |
| **編碼感知** | ✅ 解碼為字符後截取 | ✅ 按字節截取但不產生半字符 |
| **邊界處理** | 精確到字符邊界 | 字節邊界,遇半字符向前回退 |
| **性能** | 較慢 (需解碼) | 較快 (直接操作字節) |
| **適用場景** | 顯示截斷、標題摘要 | 存儲限制、协议字段定長 |
mb_substr(string $string, int $start, int|null $length = null, string|null $encoding = null): string
mb_strcut(string $string, int $start, int|null $length = null, string|null $encoding = null): string
$start:起始位置 (0 基礎),負數從末尾倒數$length:截取長度,null 表示到結尾$encoding:字符編碼 (UTF-8, GBK, SJIS 等),null 使用 mb_internal_encoding()<?php
header('Content-Type: text/html; charset=utf-8');
$str = "你好世界"; // UTF-8: 6 字節/字符 = 18 字節
echo "原始: $str (長度: " . mb_strlen($str) . " 字符, " . strlen($str) . " 字節)\n";
// 原始: 你好世界 (長度: 6 字符, 18 字節)
// mb_substr: 按字符截取
echo "mb_substr(0, 4): " . mb_substr($str, 0, 4, 'UTF-8') . "\n";
// 你好世 (4 個字符)
// mb_strcut: 按字節截取 (不產生半字符)
echo "mb_strcut(0, 4): " . mb_strcut($str, 0, 4, 'UTF-8') . "\n";
// 你 (4 字節 = 1 個中文字符 + 1 字節 → 回退到字符邊界 = 1 字符)
echo "mb_strcut(0, 6): " . mb_strcut($str, 0, 6, 'UTF-8') . "\n";
// 你好 (6 字節 = 2 個中文字符)
echo "mb_strcut(0, 7): " . mb_strcut($str, 0, 7, 'UTF-8') . "\n";
// 你好 (7 字節 → 第3字符只切1字節 → 回退到6字節 = 2字符)
字符串: 你 好 世 界 字節: E4 BD A0 E5 A5 BD E4 B8 96 E7 95 8C 索引: 0 1 2 3 4 5 6 7 8 9 10 11 字符索引: 0 1 2 3 mb_substr(0, 2) → 索引 0,1 → "你好" (邏輯第 0,1 字符) mb_strcut(0, 6) → 字節 0-5 → "你好" (物理前 6 字節 = 2 字符) mb_strcut(0, 7) → 字節 0-6 → "你好" (第 7 字節屬於第 3 字符 → 回退) mb_strcut(0, 8) → 字節 0-7 → "你好" (同理) mb_strcut(0, 9) → 字節 0-8 → "你好世" (前 9 字節 = 3 字符)
<?php
$str = "abcdef";
// mb_substr 負數: 從末尾倒數字符
mb_substr($str, -3); // "def" (最後 3 字符)
mb_substr($str, -3, 2); // "de" (倒數第 3 開始取 2 個)
// mb_strcut 負數: 從末尾倒數字節
mb_strcut($str, -3); // "def" (最後 3 字節,ASCII 1字節/字符)
mb_strcut("你好世界", -6); // "世界" (最後 6 字節 = 2 中文字符)
<?php
// 1. 顯式指定編碼 (推薦,避免依賴全局設置)
mb_substr($str, 0, 10, 'UTF-8');
// 2. 統一設置內部編碼 (bootstrap 階段)
mb_internal_encoding('UTF-8');
mb_regex_encoding('UTF-8');
// 之後可省略編碼參數
// 3. 檢測編碼 (不確定時)
$encoding = mb_detect_encoding($str, ['UTF-8', 'GBK', 'ASCII'], true);
mb_substr($str, 0, 10, $encoding);
mb_substr($str, 0, 4) | mb_substr($str, 0, 4, 'UTF-8') | 依賴 mb_internal_encoding,跨環境不穩定 |
| 用 substr 處理中文 | substr($str, 0, 6) | mb_substr($str, 0, 2, 'UTF-8') | substr 按字節,UTF-8 中文 3 字節/字符 |
| 混淆字符長度與字節長度 | strlen($str) 判斷長度 | mb_strlen($str, 'UTF-8') | strlen 返回字節數 |
| mb_strcut 期望字符語義 | mb_strcut($str, 0, 4) 以為 4 字符 | mb_substr($str, 0, 4) | strcut 是字節級,4 字節 ≈ 1-2 中文字符 |
<?php
$str = str_repeat("中文測試", 1000); // 8000 字符, 24000 字節
$start = microtime(true);
for ($i = 0; $i < 1000000; $i++) {
mb_substr($str, 100, 50, 'UTF-8');
}
echo "mb_substr: " . (microtime(true) - $start) . "s\n";
// ~1.8s
$start = microtime(true);
for ($i = 0; $i < 1000000; $i++) {
mb_strcut($str, 300, 150, 'UTF-8');
}
echo "mb_strcut: " . (microtime(true) - $start) . "s\n";
// ~0.9s (快約 2 倍)
$start = microtime(true);
for ($i = 0; $i < 1000000; $i++) {
substr($str, 300, 150);
}
echo "substr: " . (microtime(true) - $start) . "s\n";
// ~0.3s (最快,但不安全)
<?php
class StringTruncator
{
public function __construct(
private string $encoding = 'UTF-8',
private string $ellipsis = '...'
) {}
/**
* 字符級截取 (顯示用)
*/
public function byChars(string $string, int $maxChars): string
{
if (mb_strlen($string, $this->encoding) <= $maxChars) {
return $string;
}
return mb_substr($string, 0, $maxChars, $this->encoding) . $this->ellipsis;
}
/**
* 字節級截取 (存儲/協議用)
*/
public function byBytes(string $string, int $maxBytes): string
{
if (strlen($string) <= $maxBytes) {
return $string;
}
// 預留省略號字節數
$ellipsisBytes = strlen($this->ellipsis);
$cutBytes = max(0, $maxBytes - $ellipsisBytes);
return mb_strcut($string, 0, $cutBytes, $this->encoding) . $this->ellipsis;
}
/**
* 智能截取:優先字符級,字節超限則字節級
*/
public function smart(string $string, int $maxChars, int $maxBytes): string
{
$charLen = mb_strlen($string, $this->encoding);
$byteLen = strlen($string);
if ($charLen <= $maxChars && $byteLen <= $maxBytes) {
return $string;
}
// 先嘗試字符級
$result = $this->byChars($string, $maxChars);
if (strlen($result) <= $maxBytes) {
return $result;
}
// 字符級結果仍超字節限制,改用字節級
return $this->byBytes($string, $maxBytes);
}
}
// 使用
$truncator = new StringTruncator();
$title = "這是一個很長的中文標題用於測試截取功能";
echo $truncator->byChars($title, 10); // "這是一個很長的中..."
echo $truncator->byBytes($title, 30); // "這是一個很長的..."
echo $truncator->smart($title, 10, 30); // 智能平衡
'UTF-8' 或統一 mb_internal_encoding('UTF-8')mb_strcut 比 mb_substr 快 ~2倍,純 ASCII 用 substr 最快理解「字符 vs 字節」本質區別,根據業務語義選擇正確函數,是 PHP 多語言應用處理文本的基本功。