在處理 legacy 系統資料、爬蟲內容清洗、或修復編碼錯誤的資料時,常需在 數值字符實體 (NCR, `〹`)、UTF-8 位元組序列與字元代碼點之間轉換。本文深度剖析一個自訂的 ascii 類別,實現 decode() (實體 → UTF-8) 與 encode() (UTF-8 → 實體) 兩大核心功能,並補充現代 PHP 內建函數的最佳實踐。
<?php
class AsciiConverter
{
/**
* 解碼:數值字符實體字串 → UTF-8 字串
* 輸入範例: "中文" → 輸出: "中文"
*
* @param string $str 包含 數字; 格式的字串
* @return string UTF-8 字串
*/
public function decode(string $str): string
{
// 提取所有 數字; 實體
preg_match_all('/(\d{2,5});/', $str, $matches);
$entities = $matches[1];
$utf = '';
foreach ($entities as $dec) {
$codepoint = (int)$dec;
if ($codepoint < 0x80) { // U+0000 ~ U+007F (ASCII)
$utf .= chr($codepoint);
} elseif ($codepoint < 0x800) { // U+0080 ~ U+07FF (2 bytes)
$utf .= chr(0xC0 | ($codepoint >> 6));
$utf .= chr(0x80 | ($codepoint & 0x3F));
} elseif ($codepoint < 0x10000) { // U+0800 ~ U+FFFF (3 bytes, BMP)
$utf .= chr(0xE0 | ($codepoint >> 12));
$utf .= chr(0x80 | (($codepoint >> 6) & 0x3F));
$utf .= chr(0x80 | ($codepoint & 0x3F));
} elseif ($codepoint < 0x110000) { // U+10000 ~ U+10FFFF (4 bytes, 輔助平面)
$utf .= chr(0xF0 | ($codepoint >> 18));
$utf .= chr(0x80 | (($codepoint >> 12) & 0x3F));
$utf .= chr(0x80 | (($codepoint >> 6) & 0x3F));
$utf .= chr(0x80 | ($codepoint & 0x3F));
}
// 忽略超出 Unicode 範圍的代碼點
}
return $utf;
}
/**
* 編碼:UTF-8 字串 → 數值字符實體字串
* 輸入: "中文" → 輸出: "中文"
*
* @param string $str UTF-8 編碼字串
* @return string 實體編碼字串
*/
public function encode(string $str): string
{
$len = strlen($str);
$i = 0;
$result = '';
while ($i < $len) {
$byte1 = ord($str[$i]);
$codepoint = 0;
$bytes = 1;
if ($byte1 < 0x80) { // 1-byte: 0xxxxxxx
$codepoint = $byte1;
$bytes = 1;
} elseif (($byte1 & 0xE0) === 0xC0) { // 2-byte: 110xxxxx 10xxxxxx
$byte2 = ord($str[$i + 1] ?? '');
$codepoint = (($byte1 & 0x1F) << 6) | ($byte2 & 0x3F);
$bytes = 2;
} elseif (($byte1 & 0xF0) === 0xE0) { // 3-byte: 1110xxxx 10xxxxxx 10xxxxxx
$byte2 = ord($str[$i + 1] ?? '');
$byte3 = ord($str[$i + 2] ?? '');
$codepoint = (($byte1 & 0x0F) << 12) | (($byte2 & 0x3F) << 6) | ($byte3 & 0x3F);
$bytes = 3;
} elseif (($byte1 & 0xF8) === 0xF0) { // 4-byte: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
$byte2 = ord($str[$i + 1] ?? '');
$byte3 = ord($str[$i + 2] ?? '');
$byte4 = ord($str[$i + 3] ?? '');
$codepoint = (($byte1 & 0x07) << 18) | (($byte2 & 0x3F) << 12) | (($byte3 & 0x3F) << 6) | ($byte4 & 0x3F);
$bytes = 4;
} else {
// 無效 UTF-8 序列,跳過單一位元組
$codepoint = 0xFFFD; // Replacement Character
$bytes = 1;
}
$result .= "{$codepoint};";
$i += $bytes;
}
return $result;
}
}
| 代碼點範圍 | 位元組數 | 位元組 1 | 位元組 2 | 位元組 3 | 位元組 4 |
|---|---|---|---|---|---|
| U+0000 ~ U+007F | 1 | 0xxxxxxx | - | - | - |
| U+0080 ~ U+07FF | 2 | 110xxxxx | 10xxxxxx | - | - |
| U+0800 ~ U+FFFF | 3 | 1110xxxx | 10xxxxxx | 10xxxxxx | - |
| U+10000 ~ U+10FFFF | 4 | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
decode() 依據代碼點大小決定輸出位元組數與位模式;encode() 透過位元組首位標記 (0x80, 0xC0, 0xE0, 0xF0) 識別序列長度,再還原代碼點。
<?php
$converter = new AsciiConverter();
// 編碼測試
$original = "Hello 中文 🚀"; // U+1F680 = 128640 (4-byte)
$encoded = $converter->encode($original);
echo "Encoded: {$encoded}\n";
// 輸出: Encoded: Hello 中文 🚀
// 解碼測試
$decoded = $converter->decode($encoded);
echo "Decoded: {$decoded}\n";
// 輸出: Decoded: Hello 中文 🚀
// 實際場景:修復資料庫中被錯誤儲存為實體的欄位
$dirtyData = "语定义标"; // "文字編碼"
$clean = $converter->decode($dirtyData);
echo "Clean: {$clean}\n";
<?php
$str = " 中文";
// 編碼為實體 (預設雙引號、單引號、<、>、&)
$encoded = htmlspecialchars($str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
// 輸出: <script>alert('XSS')</script> 中文
// 解碼實體
$decoded = html_entity_decode($encoded, ENT_QUOTES, 'UTF-8');
// 輸出: 中文
// 取得所有實體對照表
$table = get_html_translation_table(HTML_ENTITIES, ENT_QUOTES, 'UTF-8');
<?php
// 字串 → 代碼點陣列
$codepoints = mb_convert_encoding($str, 'UCS-4BE', 'UTF-8');
// 或使用 IntlChar (需 intl 擴展)
$codepoints = [];
for ($i = 0, $len = mb_strlen($str); $i < $len; $i++) {
$char = mb_substr($str, $i, 1);
$codepoints[] = IntlChar::ord($char);
}
// 代碼點 → 字串
$str = '';
foreach ($codepoints as $cp) {
$str .= IntlChar::chr($cp);
}
// 或
$str = mb_convert_encoding(pack('N*', ...$codepoints), 'UTF-8', 'UCS-4BE');
<?php
// 解碼 〹 格式 (PHP 內建無直接函數,但可用正則 + IntlChar)
$decoded = preg_replace_callback('/(\d+);/', function($m) {
$cp = (int)$m[1];
return ($cp < 0x110000) ? IntlChar::chr($cp) : '';
}, $entityString);
// 編碼為 〹 格式
$encoded = preg_replace_callback('/./u', function($m) {
return '' . IntlChar::ord($m[0]) . ';';
}, $utf8String);
| 方案 | 適用場景 | 效能 | 依賴 |
|---|---|---|---|
| 自訂 AsciiConverter 類別 | 純 PHP 環境、無 intl 擴展、需精確控制 NCR 格式 | 中等 (純 PHP 迴圈) | 無 |
htmlspecialchars/html_entity_decode | HTML 輸出防 XSS、表單資料清洗 | 極高 (C 實現) | 核心內建 |
IntlChar + mbstring | Unicode 正規化、代碼點運算、多語言處理 | 高 (C 實現) | intl 擴展 |
preg_replace_callback + IntlChar | 批次 NCR 編解碼、資料遷移腳本 | 中高 | intl 擴展 |
encode() 會產生 〹,使用前檢查 strpos($str, '') === false。ltrim($str, "\xEF\xBB\xBF")。mb_convert_encoding 或分批處理。理解 ASCII/Unicode/UTF-8/NCR 之間的轉換關係,是處理國際化、資料清洗、編碼修復的核心技能。自訂類別適合學習原理與無擴展環境,生產環境請優先使用 htmlspecialchars (HTML 語境)、IntlChar (Unicode 操作)、mbstring (多位元組字串),獲得更好效能、正確性與安全性。