`strlen()` 算位元組、`strrev()` 直接逐位元組反轉、`substr()` 硬切位元組——對於 UTF-8/GBK 雙位元組中文,這些函數都會產生**亂碼、斷字、長度誤判**。雖然現代 PHP 建議用 `mbstring` 或 `intl` 擴展,但在舊專案、受限環境、或需要極致效能的場景下,一套純 PHP、無依賴、處理 **GBK/Big5 雙位元組編碼** 的工具函數庫仍有實戰價值。本文整理並優化一組經典函數:`is_chinese`、`cstrrev`、`csubstr`、`cleft`、`cright`、`ctext_wrap`。
/**
* 判斷字串指定位置是否為中文字元的左/右半邊
* @param string $str 目標字串 (GBK/Big5 編碼)
* @param int $location 位元組索引 (0-based)
* @return int -1=左半部 | 0=非中文(ASCII) | 1=右半部
*/
function is_chinese(&$str, $location) {
$ch = true;
$i = $location;
// 向前掃描:連續高位元組 (>0xA0) 視為中文雙位元組序列
while (isset($str[$i]) && ord($str[$i]) > 0xA0 && $i >= 0) {
$ch = !$ch; // 翻轉標記
$i--;
}
if ($i != $location) { // 掃描過程中遇到高位元組
return $ch ? 1 : -1; // 最後狀態決定左右
}
return 0; // 全程無高位元組 = ASCII
}
原理:GBK/Big5 中文字首位元組 `> 0xA0`,次位元組也 `> 0xA0`。從目標位置向前掃描,遇到幾個高位元組就翻轉幾次旗標,最終旗標即揭示目標位置屬於雙位元組的第幾個字節。
function cstrrev(&$str) {
$len = strlen($str);
$rev = '';
$chinese = false;
for ($i = $len - 1; $i >= 0; $i--) {
if (ord($str[$i]) > 0xA0) {
$chinese = !$chinese;
if (!$chinese) { // 遇到左半部,輸出「右+左」
$rev .= $str[$i] . $str[$i+1];
}
} else {
$rev .= $str[$i]; // ASCII 直接輸出
}
}
return $rev;
}
// 測試:cstrrev('abc中文') → '文中cba'
/**
* @param string $str 來源字串
* @param int $start 起始位置 (字元數或位元組數,視 $cn_len)
* @param int $length 截取長度 (0=到尾)
* @param bool $ltor true=左→右 | false=右→左
* @param int $cn_len 2=按位元組計 | 1=按字元計(中文算1)
* @return string 截取結果
*/
function csubstr(&$str, $start=0, $length=0, $ltor=true, $cn_len=2) {
if ($length === 0) $length = strlen($str);
if (!$ltor) $str = cstrrev($str); // 反轉處理右→左
if ($cn_len == 1) { // 按字元數計算 (中文算1)
// 將 $start/$length 換算為位元組偏移
for ($i=0, $fs=0; $i < $start; $fs++) $i += (ord($str[$fs]) <= 0xA0) ? 1 : 0.5;
for ($i=0, $fe=$fs; $i < $length; $fe++) $i += (ord($str[$fe]) <= 0xA0) ? 1 : 0.5;
$length = $fe - $fs;
} else { // 按位元組計算,但避開切中中文
$fs = (is_chinese($str, $start) == 1) ? $start - 1 : $start; // 落在右半部則左移
$fe = $start + $length - 1;
$end = (is_chinese($str, $fe) == -1) ? $fe - 1 : $fe; // 落在左半部則左移
$length = $end - $fs + 1;
}
$result = substr($str, $fs, $length);
if (!$ltor) $result = cstrrev($result);
return $result;
}
// 取左 N 字元/位元組
function cleft(&$str, $length, $cn_len=2) { return csubstr($str, 0, $length, true, $cn_len); }
// 取右 N 字元/位元組
function cright(&$str, $length, $cn_len=2) {
$rev = cstrrev($str);
$cut = csubstr($rev, 0, $length, true, $cn_len);
return cstrrev($cut);
}
// 文章自動分行 (依字元寬度換行,避免斷字)
function ctext_wrap(&$text, $width=60, $br="
") {
$lines = explode("\n", $text); // 原文以 \n 分段
$out = '';
foreach ($lines as $line) {
$len = strlen($line);
for ($j=0; $j < $len; $j += $width) {
$p = $j + $width - 1;
$k = 0;
// 向前尋找安全斷點:非中文右半部、非空格
while ($p < $len && !is_chinese($line, $p) && $line[$p] != ' ' && $p > $j) {
$k++; $p--;
}
if ($p == $j) $k = 0; // 整行無安全點,硬切
$out .= csubstr($line, $j, $width - $k) . $br;
$j -= $k; // 下一輪從安全點後開始
}
}
return $out;
}
<?php
header('Content-Type: text/html; charset=gbk'); // 重要:頁面編碼需與函數假設一致
$s = "Hello世界123中文";
echo "原文: $s
";
echo "長度(bytes): ".strlen($s)."
";
echo "反轉: ".cstrrev($s)."
";
echo "左6字元: ".cleft($s, 6, 1)."
"; // Hello世
echo "右5字元: ".cright($s, 5, 1)."
"; // 123中文
echo "截取(2,6): ".csubstr($s, 2, 6, true, 1)."
"; // llo世
// 文章分行測試
$article = "這是一段很長的中英文混合文字Hello World用來測試自動分行功能是否會正確避開中文斷字問題。";
echo ctext_wrap($article, 20);
?>
| 限制 | 說明 | 現代最佳實踐 |
|---|---|---|
| 編碼綁定 | 假設 **GBK/Big5** (雙位元組,首字節 >0xA0);**UTF-8 中文首字節 ≥ 0xE0**,邏輯完全不同 | 專案全面改用 **UTF-8** + `mbstring` 函數 (`mb_strlen`, `mb_substr`, `mb_strrev` 等) |
| 無視組合字元 | 不處理 Emoji、注音符號、ZWJ 序列等多碼位字元 | 用 `grapheme_*` 函數 (需 `intl` 擴展) 或 `symfony/string` 套件 |
| 效能 | 逐位元組掃描,長字串 O(n²) 風險 | `mbstring` 底層 C 實現,快 10-100 倍 |
| 維護性 | 手寫邊界邏輯極易出錯 | 引入成熟庫:`voku/portable-utf8` (無擴展依賴)、`symfony/string` |
// 直接對應替換表
strlen($s) → mb_strlen($s, 'UTF-8')
substr($s, $a, $b)→ mb_substr($s, $a, $b, 'UTF-8')
strrev($s) → mb_strrev($s) // 需自行實現或用 voku 库
// 正則加上 u 修飾符
preg_match('/pattern/u', $s)
若受限無法安裝擴展,可引入 **`voku/portable-utf8`** (Composer: `composer require voku/portable-utf8`),它純 PHP 實現 `UTF8::strlen()`、`UTF8::substr()` 等完整 API,且自動偵測 `mbstring` 可用時優先使用原生函數,效能與相容性雙贏。
這組函數庫見證了 PHP 早期開發者在無 `mbstring` 環境下的智慧與妥協。今天,它更適合作為**理解字元編碼底層邏輯的教學素材**,而非生產環境首選。理解它,才能更珍惜 `mbstring` 與 `intl` 帶來的便利。