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

PHP 多位元組安全截取中文字串:mb_substr、mb_strimwidth 與手寫 UTF-8 解析器對比

     編輯:PHP 相關知識

PHP 多位元組安全截取中文字串:mb_substr、mb_strimwidth 與手寫 UTF-8 解析器對比

在 UTF-8 編碼中,中文字元佔 3 位元組,英文 1 位元組,Emoji 4 位元組。直接使用 substr 按位元組截取極易產生亂碼。本文對比三種安全截取方案:mbstring 標準函式、mb_strimwidth 一步到位、手寫 UTF-8 位元組解析器 (理解底層原理/無擴展環境)。

方案一:mb_substr (標準、通用、推薦)

/**
 * 多位元組安全截取
 * @param string $str 輸入字串
 * @param int $start 起始字元索引 (0基礎,支援負數)
 * @param int|null $length 截取字元數 (null 截取到結束)
 * @param string $encoding 編碼 (預設 UTF-8)
 * @return string
 */
function mbSubstr(string $str, int $start, int $length = null, string $encoding = 'UTF-8'): string {
    return mb_substr($str, $start, $length, $encoding) ?? '';
}

// 測試
$str = 'Hello世界😀PHP';
echo mbSubstr($str, 0, 5) . "\n";    // Hello
echo mbSubstr($str, 5, 2) . "\n";    // 世界
echo mbSubstr($str, -4) . "\n";      // 😀PHP
echo mbSubstr($str, 5, -1) . "\n";   // 世界😀 (PHP 7.1+ 支援負長度)

方案二:mb_strimwidth (截取+補省略號,最實用)

/**
 * 截取指定寬度並自動加省略號
 * @param string $str
 * @param int $start 起始字元
 * @param int $width 目標寬度 (字元數)
 * @param string $trimmarker 省略標記
 * @param string $encoding
 * @return string
 */
function mbTrimWidth(string $str, int $start, int $width, string $trimmarker = '...', string $encoding = 'UTF-8'): string {
    return mb_strimwidth($str, $start, $width, $trimmarker, $encoding);
}

// 實戰:文章摘要
$article = '這是一篇很長的文章內容,包含中文、English、數字123、符號!@#、Emoji😀🎉...';
echo mbTrimWidth($article, 0, 30, '...') . "\n";
// 輸出:這是一篇很長的文章內容,包含中文、Eng... (寬度含省略號)

方案三:手寫 UTF-8 位元組解析器 (教學/無擴展環境)

原理:UTF-8 編碼規則決定字元邊界:

  • 0xxxxxxx (0x00-0x7F) : 1 位元組 (ASCII)
  • 110xxxxx (0xC0-0xDF) : 2 位元組
  • 1110xxxx (0xE0-0xEF) : 3 位元組 (中日韓)
  • 11110xxx (0xF0-0xF7) : 4 位元組 (Emoji/補充平面)
  • 10xxxxxx (0x80-0xBF) : 延續位元組 (非首位元組)
/**
 * 純 PHP UTF-8 安全截取 (按字元)
 * @param string $str
 * @param int $length 截取字元數
 * @return string
 */
function utf8Substr(string $str, int $length): string {
    if ($length <= 0) return '';
    $bytes = strlen($str);
    $result = '';
    $charCount = 0;
    $offset = 0;

    while ($charCount < $length && $offset < $bytes) {
        $byte = ord($str[$offset]);
        // 判斷字元長度
        if (($byte & 0x80) === 0x00) {        // 1 byte
            $charLen = 1;
        } elseif (($byte & 0xE0) === 0xC0) {  // 2 bytes
            $charLen = 2;
        } elseif (($byte & 0xF0) === 0xE0) {  // 3 bytes
            $charLen = 3;
        } elseif (($byte & 0xF8) === 0xF0) {  // 4 bytes
            $charLen = 4;
        } else {
            // 非法/延續位元組,單字節處理避免死循環
            $charLen = 1;
        }

        // 邊界檢查
        if ($offset + $charLen > $bytes) $charLen = $bytes - $offset;

        $result .= substr($str, $offset, $charLen);
        $offset += $charLen;
        $charCount++;
    }
    return $result;
}

// 測試
$str = '中文English😀';
echo utf8Substr($str, 5) . "\n"; // 中文Eng
echo utf8Substr($str, 10) . "\n"; // 中文English😀

// 進階版:支援 start offset
function utf8SubstrAdvanced(string $str, int $start, int $length = null): string {
    $bytes = strlen($str);
    // 先定位到 start 字元位置
    $offset = 0;
    $charCount = 0;
    while ($charCount < $start && $offset < $bytes) {
        $byte = ord($str[$offset]);
        $charLen = (($byte & 0x80) === 0) ? 1 :
                   (($byte & 0xE0) === 0xC0) ? 2 :
                   (($byte & 0xF0) === 0xE0) ? 3 :
                   (($byte & 0xF8) === 0xF0) ? 4 : 1;
        if ($offset + $charLen > $bytes) break;
        $offset += $charLen;
        $charCount++;
    }
    // 再截取 length 字元
    $result = '';
    $charCount = 0;
    $maxChars = $length ?? PHP_INT_MAX;
    while ($charCount < $maxChars && $offset < $bytes) {
        $byte = ord($str[$offset]);
        $charLen = (($byte & 0x80) === 0) ? 1 :
                   (($byte & 0xE0) === 0xC0) ? 2 :
                   (($byte & 0xF0) === 0xE0) ? 3 :
                   (($byte & 0xF8) === 0xF0) ? 4 : 1;
        if ($offset + $charLen > $bytes) $charLen = $bytes - $offset;
        $result .= substr($str, $offset, $charLen);
        $offset += $charLen;
        $charCount++;
    }
    return $result;
}

方案對比與選型指南

維度mb_substr / mb_strimwidth手寫解析器
正確性✅ 完整 Unicode 支援 (含組合字元需 intl)✅ 基本字元邊界正確 / ❌ 組合字元/ZWJ 不支援
效能C 層實現,極快PHP 迴圈,慢 10-50 倍
依賴mbstring (PHP 核心內建,幾乎必裝)零依賴
功能豐富度mb_strimwidth、mb_strpos、mb_strtolower 等完整生態僅截取,需自行擴展
維護性標準庫,社區維護自行維護,易漏邊界情況
適用場景所有生產環境極簡容器、嵌入式、教學理解 UTF-8 結構

常見業務封裝建議

class TextHelper {
    // 統一入口,自動選擇最佳實現
    public static function substr(string $str, int $start, int $length = null): string {
        return mb_substr($str, $start, $length, 'UTF-8') ?? '';
    }

    public static function trimWidth(string $str, int $width, string $marker = '...'): string {
        return mb_strimwidth($str, 0, $width, $marker, 'UTF-8');
    }

    public static function words(string $str, int $maxWords): string {
        // 簡易按空白分詞 (中文無空白需分詞庫)
        $words = preg_split('/\s+/u', $str, $maxWords + 1);
        if (count($words) > $maxWords) {
            array_pop($words);
            return implode(' ', $words) . '...';
        }
        return implode(' ', $words);
    }

    // 移除 Emoji (可選)
    public static function stripEmoji(string $str): string {
        return preg_replace('/[\x{1F600}-\x{1F64F}\x{1F300}-\x{1F5FF}\x{1F680}-\x{1F6FF}\x{1F1E0}-\x{1F1FF}]/u', '', $str);
    }
}

靜態分析規則 (PHPStan/Psalm)

# phpstan.neon
rules:
  - PHPStan\Rules\Functions\FunctionCallParametersCheckRule
parameters:
  forbiddenFunctions:
    substr: 'Use TextHelper::substr() for UTF-8 strings'
    strlen: 'Use mb_strlen() or TextHelper::length()'
    strpos: 'Use mb_strpos()'
    strtolower: 'Use mb_strtolower()'
    # ... 其他位元組函式

中文字串截取的核心原則:**業務層字串操作一律使用 mb_* 函式族,嚴禁直接使用 substr/strlen/strpos 等位元組函式**。配合靜態分析強制執行,能從架構層面消除亂碼 Bug。

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