首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 中文字符串截取完整指南:mb_substr、正則、自定義函數、編碼感知與最佳實踐

     編輯:PHP 相關知識

PHP 處理中文字符串截取是高頻需求,但 substr() 按字節切割導致亂碼。本文系統整理 mb_substr、正則提取、自定義字符級截取、編碼檢測、智能截取工具類 等多種方案,解決 UTF-8/GBK 混合環境、Emoji、HTML 實體、混合編碼等複雜場景。

核心問題:為什麼 substr 會亂碼?

<?php
$str = "你好世界"; // UTF-8: 每字 3 字節,共 18 字節

echo substr($str, 0, 6);  // "你好" (剛好 2 字符 = 6 字節,偶然正確)
echo substr($str, 0, 7);  // "你好" (第 3 字符只取 1 字節 → 截斷 → 亂碼)
echo substr($str, 0, 8);  // "你好" (同理)
echo substr($str, 0, 9);  // "你好世" (3 字符 = 9 字節,又正確了)

根因: UTF-8 中文 3 字節/字符,substr 按字節截取,非 3 的倍數位置必然截斷字符。

方案一:mb_substr (標準、推薦、PHP 內置)

<?php
// 必須指定編碼或設置 mb_internal_encoding
mb_internal_encoding('UTF-8');

$str = "你好世界";
echo mb_substr($str, 0, 4);      // "你好世" (4 字符)
echo mb_substr($str, 2, 3);      // "世界" (從第 2 字符取 3 個)
echo mb_substr($str, -3);        // "世界" (最後 3 字符,PHP 7.1+)

// 支持指定編碼
echo mb_substr($gbkStr, 0, 4, 'GBK'); // GBK 環境

優勢

  • ✅ 官方擴展,無依賴,性能好
  • ✅ 支持負數索引、多種編碼
  • ✅ 正確處理 Emoji (4 字節/字符)

方案二:正則提取字符 (無 mbstring 環境兜底)

<?php
/**
 * 使用正則匹配 UTF-8 字符序列
 * 模式解析:
 * [\x00-\x7F]              - ASCII (1 字節)
 * [\xC2-\xDF][\x80-\xBF]   - 2 字節序列
 * \xE0[\xA0-\xBF][\x80-\xBF] - 3 字節 (首字節 E0)
 * [\xE1-\xEF][\x80-\xBF]{2}   - 3 字節 (其餘)
 * \xF0[\x90-\xBF]{3}       - 4 字節 (首字節 F0, Emoji 等)
 * [\xF1-\xF7][\x80-\xBF]{3}   - 4 字節 (其餘)
 */
function utf8Chars(string $str): array
{
    preg_match_all(
        '/[\x00-\x7F]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEF][\x80-\xBF]{2}|\xF0[\x90-\xBF]{3}|[\xF1-\xF7][\x80-\xBF]{3}/',
        $str,
        $matches
    );
    return $matches[0];
}

function mbSubstrRegex(string $str, int $start, int $length = null): string
{
    $chars = utf8Chars($str);
    $slice = array_slice($chars, $start, $length ?? count($chars));
    return implode('', $slice);
}

// 測試
$str = "你好🌍世界"; // 含 Emoji
echo mbSubstrRegex($str, 0, 4); // "你好🌍世" (正確處理 4 字節 Emoji)

方案三:自定義字符級截取 (教學、特殊邏輯)

<?php
/**
 * 手動實現 UTF-8 字符級截取 (理解原理用)
 */
function manualMbSubstr(string $str, int $start, int $length = null, string $encoding = 'UTF-8'): string
{
    if ($encoding !== 'UTF-8') {
        // 簡單轉碼處理
        $str = mb_convert_encoding($str, 'UTF-8', $encoding);
    }
    
    $bytes = strlen($str);
    $charCount = 0;
    $bytePos = 0;
    $startByte = -1;
    $endByte = $bytes;
    
    for ($i = 0; $i < $bytes; ) {
        $byte = ord($str[$i]);
        
        // 判斷字符字節數
        if ($byte < 0x80) { $charBytes = 1; }           // ASCII
        elseif ($byte < 0xE0) { $charBytes = 2; }       // 2 字節
        elseif ($byte < 0xF0) { $charBytes = 3; }       // 3 字節 (中文)
        else { $charBytes = 4; }                        // 4 字節 (Emoji)
        
        // 記錄起始字節位置
        if ($charCount === $start) {
            $startByte = $i;
        }
        
        $charCount++;
        $i += $charBytes;
        
        // 記錄結束字節位置
        if ($length !== null && $charCount === $start + $length) {
            $endByte = $i;
            break;
        }
    }
    
    if ($startByte === -1) return ''; // 起始位置超出
    
    return substr($str, $startByte, $endByte - $startByte);
}

方案四:處理 HTML 實體、標籤的智能截取

<?php
/**
 * 智能截取:保護 HTML 標籤完整性、實體不被截斷
 */
function smartHtmlTruncate(string $html, int $maxLength, string $ellipsis = '...'): string
{
    // 1. 剝離標籤獲取純文本長度
    $text = strip_tags($html);
    if (mb_strlen($text) <= $maxLength) {
        return $html;
    }
    
    // 2. 使用 DOMDocument 解析並截取 (最準確)
    libxml_use_internal_errors(true);
    $dom = new DOMDocument();
    $dom->loadHTML('' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
    libxml_clear_errors();
    
    $textContent = '';
    $resultNodes = [];
    
    // 遞歸遍歷文本節點
    $traverse = function($node) use (&$traverse, &$textContent, &$resultNodes, $maxLength, $ellipsis) {
        if ($node->nodeType === XML_TEXT_NODE) {
            $nodeText = $node->textContent;
            $remaining = $maxLength - mb_strlen($textContent);
            
            if ($remaining <= 0) {
                $node->textContent = '';
                return false; // 停止遍歷
            }
            
            if (mb_strlen($nodeText) > $remaining) {
                $node->textContent = mb_substr($nodeText, 0, $remaining) . $ellipsis;
                return false;
            }
            
            $textContent .= $nodeText;
        } elseif ($node->nodeType === XML_ELEMENT_NODE) {
            $resultNodes[] = $node;
            foreach ($node->childNodes as $child) {
                if (!$traverse($child)) return false;
            }
        }
        return true;
    };
    
    $traverse($dom);
    
    // 3. 序列化結果
    $output = '';
    foreach ($dom->childNodes as $node) {
        $output .= $dom->saveHTML($node);
    }
    
    return $output;
}

方案五:統一智能截取工具類 (生產級)

<?php
class StringTruncator
{
    public function __construct(
        private string $encoding = 'UTF-8',
        private string $ellipsis = '...',
        private bool $htmlAware = false
    ) {}
    
    /**
     * 字符級截取 (核心)
     */
    public function truncate(string $string, int $maxLength): string
    {
        if (mb_strlen($string, $this->encoding) <= $maxLength) {
            return $string;
        }
        
        if ($this->htmlAware) {
            return $this->truncateHtml($string, $maxLength);
        }
        
        return mb_substr($string, 0, $maxLength, $this->encoding) . $this->ellipsis;
    }
    
    /**
     * 字節級截取 (存儲限制)
     */
    public function truncateBytes(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 truncateSmart(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->truncate($string, $maxChars);
        if (strlen($result) <= $maxBytes) {
            return $result;
        }
        
        // 字符截取後仍超字節限制,改按字節
        return $this->truncateBytes($string, $maxBytes);
    }
    
    /**
     * HTML 感知截取 (保護標籤)
     */
    private function truncateHtml(string $html, int $maxLength): string
    {
        // 簡化版:剝離標籤截取,再嘗試恢復 (不完美但實用)
        $text = strip_tags($html);
        if (mb_strlen($text) <= $maxLength) return $html;
        
        $truncatedText = mb_substr($text, 0, $maxLength, $this->encoding) . $this->ellipsis;
        
        // 簡單策略:返回純文本截取 (避免破壞標籤)
        // 完美方案需用 DOMDocument 如方案四
        return $truncatedText;
    }
    
    /**
     * 獲取字符統計信息
     */
    public function stats(string $string): array
    {
        return [
            'chars' => mb_strlen($string, $this->encoding),
            'bytes' => strlen($string),
            'words' => str_word_count($string, 0, 'áéíóúÁÉÍÓÚàèìòùÀÈÌÒÙäëïöüÄËÏÖÜñÑçÇ'), // 簡易詞數
            'lines' => substr_count($string, "\n") + 1,
        ];
    }
}

// 使用示例
$truncator = new StringTruncator(ellipsis: '…', htmlAware: true);

$title = "這是一個包含粗體和斜體的長標題內容";
echo $truncator->truncate($title, 15); 
// "這是一個包含粗體和斜體…" (HTML 標籤被 strip_tags 移除)

$content = "用戶輸入的超長內容...";
echo $truncator->truncateSmart($content, 100, 500); // 雙重保護

編碼檢測與自適應

<?php
function detectAndTruncate(string $string, int $maxChars): string
{
    // 檢測編碼 (優先級: UTF-8 > GBK > ASCII)
    $encoding = mb_detect_encoding($string, ['UTF-8', 'GBK', 'ASCII'], true);
    
    if ($encoding === false) {
        // 未知編碼,嘗試強制 UTF-8
        $encoding = 'UTF-8';
        $string = @mb_convert_encoding($string, 'UTF-8', 'auto');
    }
    
    return mb_substr($string, 0, $maxChars, $encoding);
}

// 批量處理不同編碼源
$sources = [
    'utf8' => "UTF-8 字符串",
    'gbk'  => mb_convert_encoding("GBK 字符串", 'GBK', 'UTF-8'),
];

foreach ($sources as $name => $str) {
    echo "$name: " . detectAndTruncate($str, 4) . "\n";
}

Emoji 與特殊字符處理

<?php
$str = "PHP 🐘 中文 🎉 測試";

// mb_substr 正確處理 Emoji (4 字節 = 1 字符)
echo mb_substr($str, 0, 6); // "PHP 🐘 中" (6 字符)

// 正則方案也需包含 4 字節模式
// \xF0[\x90-\xBF]{3} 已覆蓋大多數 Emoji

// 計算顯示寬度 (東亞寬度)
function displayWidth(string $str): int
{
    $width = 0;
    $chars = utf8Chars($str); // 使用方案二的正則分割
    foreach ($chars as $char) {
        $ord = ord($char[0]);
        if ($ord < 0x80) {
            $width += 1; // 半寬
        } elseif ($ord >= 0xE0 && $ord < 0xF0) {
            $width += 2; // 全寬 (中日韓)
        } else {
            $width += 1; // Emoji 等通常渲染為雙寬,但計算為 1
        }
    }
    return $width;
}

echo displayWidth("Hello世界🌍"); // 5 + 2*2 + 1 = 10

性能與選型總結

| 方案 | 依賴 | 性能 | 功能完整度 | 適用場景 | |------|------|------|------------|----------| | mb_substr | mbstring | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | **首選標準方案** | | preg_match_all 正則 | 無 | ⭐⭐⭐ | ⭐⭐⭐⭐ | 無 mbstring 環境 | | 手動解析 | 無 | ⭐⭐ | ⭐⭐ | 教學、特殊邏輯 | | StringTruncator 類 | mbstring | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | **生產項目統一封裝** | | DOMDocument HTML | DOM | ⭐⭐ | ⭐⭐⭐⭐⭐ | 富文本截取 |

最佳實踐清單

  • ✅ 統一使用 mb_substr,項目啟動時 mb_internal_encoding('UTF-8')
  • ✅ 封裝 StringTruncator,業務代碼調用 $truncator->truncate($str, 50)
  • ✅ 雙重限制:顯示用字符數、存儲用字節數,truncateSmart()
  • ✅ HTML 富文本:用 DOMDocument 保護標籤,或簡化策略 strip_tags 後截取
  • ✅ 編碼自適應:多源數據接入時 mb_detect_encoding + 轉碼
  • ✅ Emoji 測試:測試用例必須包含 🌍🎉🐘 等 4 字節字符

總結

PHP 中文字符串截取的核心是編碼感知。mb_substr 是標準答案,StringTruncator 是工程化答案。建立統一截取服務,處理「字符 vs 字節」、「純文本 vs HTML」、「單一編碼 vs 混合編碼」三組矛盾,讓截取功能從「經常出 Bug」變成「可靠的基礎設施」。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved