Punycode 轉換會將像 bücher.example 這類國際化網域標籤,轉成 ASCII 形式 xn--bcher-kva.example,以便 DNS、TLS 憑證和電子郵件系統在線路上實際傳輸;而從 PHP 程式碼到正確轉換之間,最直接的做法就是依照 RFC 3492 演算法,一個標籤、一個標籤地處理。Punycode Converter 使用已公布的常數(基底 36、tmin 1、tmax 26、skew 38、damp 700、初始偏差 72、起始碼點 128)來精確實作該演算法,讓你不必自己寫編碼器,就能驗證或產生任何 IDN 標籤的 ASCII 形式。PHP 開發者通常會在這些情境遇到這個問題:寄信到 IDN 網域的地址時、把主機名稱寫入 DNS 或憑證欄位時,或是驗證來自使用者輸入的 xn-- 字串時。

為何 PHP 程式碼庫會需要 Punycode
PHP 處理文字沒問題,但底層的網路層只講 ASCII。網域名稱系統把標籤以 LDH(字母、數字、連字號)形式儲存,而 SMTP 信封、TLS SNI 延伸模組和 HTTP Host 標頭都預期收到 ASCII 主機名稱。使用者在註冊表單中輸入 mañana.example、從含有 CJK 字元的 CSV 匯入的地址,或是從其他系統貼上的 webhook URL,只要任何一個非 ASCII 標籤沒有先用 xn-- ACE 前置編碼過(這是 RFC 3492 中定義的),就無法順利通過 gethostbyname、stream_socket_client 或憑證授權單位完成來回傳遞。
PHP 為此提供了兩條主要路線。intl 擴充功能內建 idn_to_ascii 與 idn_to_utf8,它們會在原始 Bootstring 演算法之上套用 IDNA 2003 或 2008 處理流程;此外也有許多 Composer 套件(例如 rowbot/punycode)直接實作 RFC 3492。兩條路徑都正確,但也都帶有 Unicode 正規化與情境規則,可能讓你意想不到。一個小巧、具決定性、以瀏覽器為基礎的轉換器很適合作為參考:貼上同一個標籤、選擇同一個方向,再把輸出和你的 PHP 程式碼產生的結果做比對。
Bootstring 編碼的白話解說
Punycode 會把一串 Unicode 碼點編碼成由 ASCII 字母、數字與連字號組成的字串。標籤中既有的 ASCII 字元會先原樣保留;接著編碼器會插入連字號,標記非 ASCII 字元的起始位置,然後附加一串長度不一的「delta」值,依序對應剩下的碼點。Deltas 以 base-36 英數字混用的字母表寫成,並透過每個位置的偏差值進行動態調整,使常見字元保持精簡、少見字元則需要較多字元表示。
固定的常數很重要。Base 為 36、tmin 為 1、tmax 為 26、skew 為 38、damp 為 700、初始偏差為 72,第一個非基本碼點為 128。這些是 Punycode Converter 使用的值,也和所有其他合規實作一致,因此能在工具中順利往返轉換的標籤,在以相同旗標呼叫 intl 擴充功能的 PHP 程式碼中也能順利往返。各實作出現差異的地方,在於演算法前後的處理:正規化全形句點、套用 NFC 或 NFKC、捨棄 IDNA 2008 表中禁止的字元等等。
逐步轉換一個網域
這個工具把流程簡化為三個明確步驟,方便檢視輸入與輸出。
- 在輸入框中只貼上網域名稱,並選擇 Unicode-to-ASCII 或 ASCII-Punycode-to-Unicode。含有 scheme、路徑、連接埠、查詢字串或片段的完整 URL 不在處理範圍內,而 CJK 文字中使用的 Unicode 全形句點會自動正規化為 ASCII 句點。
- 轉換並檢視每個標籤。轉換器會以句點切分網域、各自獨立處理每個標籤,只有包含非 ASCII 碼點的標籤才會加上 xn-- 前置。純 ASCII 標籤會轉為小寫,其餘內容保持不變,因此輸出能保留標籤的界線。
- 使用複製按鈕複製結果(只會複製轉換後的網域,不會複製 scheme 或結尾斜線),接著在信任它之前,用目標註冊機構、URL 函式庫或憑證授權單位加以驗證。這也是檢查來自不同書寫系統、視覺相似字元的時機。
編碼模式 vs 解碼模式
兩種模式互為鏡像,但在除錯 PHP 流程時,輸入和輸出有些值得注意的差異。下表摘要說明每個模式預期的輸入與回傳的結果。
| 面向 | Unicode-to-ASCII(編碼) | ASCII-Punycode-to-Unicode(解碼) |
|---|---|---|
| 典型輸入 | 像 bücher 或 例子 這類可讀的標籤 | 以 xn-- 開頭的 ASCII 標籤 |
| 輸出 | 非 ASCII 標籤加上 xn-- 前置的 ASCII 標籤 | 原始 Unicode 標籤,前置已被消耗 |
| 前置行為 | 只有包含非基本碼點的標籤才會加上 xn-- | 待解碼的標籤必須帶有 xn--;其餘 ASCII 標籤會轉為小寫並保持不變 |
| 純 ASCII 標籤 | 轉為小寫並保持不變 | 轉為小寫並保持不變 |
| 使用時機 | 寫入 DNS、SMTP RCPT TO 或憑證簽署請求之前 | 顯示或記錄收到的 xn-- 主機名稱之前 |
PHP 特有的陷阱與轉換後的檢查事項
PHP 開發者與原始 RFC 3492 轉換器之間最大的混淆來源,在於正規化。PHP 的 idn_to_ascii 在使用 IDNA_DEFAULT 或 IDNA_UTS_46 旗標時,會在 Bootstring 編碼之前先經過 UTS #46 映射表處理,這表示含有相容字元或句點變體的標籤,最後產生的 xn-- 形式可能和工具的結果不同。這不是任一方的 bug:WHATWG URL Standard 在編碼前也會套用自己的映射。請把瀏覽器版的轉換器視為原始 RFC 3492 參考實作,再讓你的 PHP 程式碼依應用需求,在其上套用所需的 IDNA 層。
第二個陷阱是同形異義字風險。把 xn--80ak6aa92e.com 合法轉回看似西里爾字母的字串,並不代表該網域真的屬於它看起來像的那個機構。不同文字系統的字元可能在視覺上完全相同,因此在信任任何解碼後的標籤之前,請先確認其書寫系統、註冊機構與背後的組織。若想深入了解如何為 DNS 產生 ASCII 形式,請參閱「how to convert Punycode to ASCII for domain names」指南;反向操作則可參閱「converting Punycode back to readable Unicode」指南,其中示範如何逐標籤檢查可疑的 xn-- 主機名稱。
第三個陷阱是悄悄截斷。Punycode 本身沒有長度上限;63 位元組的標籤限制來自 DNS,而 IDNA 還會對字元類別加上自己的限制。一個能成功轉換的標籤,仍可能太長、被註冊機構保留,或被該註冊機構的 IDN 政策擋下。如果你的註冊商拒絕該 ASCII 形式,請依其政策處理,不要自行替換字元後重新編碼。
輸入限制與遭拒情況
這個轉換器的接受範圍刻意設計得很窄,使其接受的內容和 PHP 及 DNS 所預期的一致。下列情況會以清楚的錯誤訊息拒絕,而不是悄悄產生錯誤結果:
- 空標籤(連續句點、前導句點或結尾句點)。
- 編碼序列中含有格式錯誤的數字。
- 無效的 Unicode 純量值,例如孤立的代理項。
- 總長度超過 1,000 個碼點的輸入。
- 任何不是網域名稱的內容:含有 scheme、路徑、連接埠、查詢字串或片段的 URL。
實作中隨附八組測試資料,涵蓋拉丁附音符號、希臘字母、CJK 字元、符號,以及單標籤與多標籤輸入(包括 bücher、mañana、例子、測試、παράδειγμα,以及一個結合雪人與命令符號的範例)。負面測試資料則涵蓋空標籤與不完整的編碼序列,以便實際演練各種拒絕路徑。由於迭代過程是以 Unicode 碼點而非 UTF-16 半字進行,emoji 等輔助平面字元能在往返轉換中保持完整,而 delta 乘法與權重累加的算術運算也都有防止溢位的保護。這種決定性正是這個轉換器適合作為參考的原因:相同的輸入永遠產生相同的輸出,你可以直接複製到 PHP 的測試 fixture 中。
整合進 PHP 工作流程
一個務實的做法是把轉換器當作測試框架中的驗證步驟,而非正式上線路徑。實際轉換請使用 intl 擴充功能或經過審核的 Composer 套件,以遵守 UTS #46、IDNA 2008 與註冊機構政策;同時把同一個標籤送進 Punycode Converter 做為開發期間的健全性檢查。如果兩者在原始 xn-- 形式上出現分歧,幾乎一定是正規化決策不同,而非 Bootstring 本身的 bug,這是一個有用的訊號,提醒你在上線前先釐清 PHP 程式碼使用的是哪一種 IDNA 模式。