Punycode

**國際化域名編碼**（英語：Punycode）是一種表示 Unicode 碼和 ASCII 碼的有限的字符集。例如中文「上海」會被編碼為「fhqz97e」。

Punycode 的目的是在於國際化域名標籤（IDNA）的框架中，使這些（多語言）的域名可以編碼為 ASCII。編碼語法在文檔 RFC 3492 中規定。

## 編碼過程

RFC 3492 中說明：Punycode 是一種稱為 Bootstring 的更普遍的算法實例，它允許由部分基本的編碼集合組成的字符串唯一表示由更大編碼集合組成的任何字符串。配合 Unicode 文本的特性，Punycode 定義了一般 Bootstring 算法的參數。本節以德語字符串 "bücher"（書籍）為例，演示 Punycode 的編碼過程，該字符串被編碼為 "bcher-kva"。

### 分離 ASCII 字符

首先，字符串中的所有 ASCII 字符將被直接從輸入複製到輸出，任何其他字符將被跳過。例如，"bücher" 被複製為 "bcher"。如果有任何字符被複製（即輸入中至少有一個 ASCII 字符），則在輸出中附加一個 ASCII 連字符（例如，"bücher" 複製為 "bcher-"，其中 "ü" 不是 ASCII 字符，不發生複製）。

由於連字符本身是 ASCII 字符，因此可以出現在輸入中，並將被複製到輸出中。這並不會引起歧義，因為如果輸出包含連字符，被添加的連字符總是最後一個，這標誌著 ASCII 字符的結束。

### 編碼非 ASCII 字符

對於輸入的非 ASCII 字符，依 Unicode 中碼位由低至高順序，編碼器會模擬將_非 ASCII 字符_一一插入回_去除非 ASCII 字符後的字串_過程，
計算每個非 ASCII 字符對應的三個數字 _i_、_n_ 和 _h_：

* _i_ 表示該非 ASCII 字符在輸入字符串中的以 0 為開始索引的插入位置（類似於如 C 語言等的程式語言，用 0 來表示該非 ASCII 字符是輸入字符串的第一個字符）。
* _n_ 表示該非 ASCII 字符在 Unicode 中的碼位，減去 127（127 為最後一個 ASCII 字符）
* _h_ 表示該非 ASCII 字符插入時可能有的插入位置。如 "ü" 要插入回 "bcher" 中，可能有 "übcher" 到 "bcherü"6 個位置可供插入。

編碼器隨後計算乘法 _n\*h+i_ 並將得到的數字編碼為一串 36 進位的可變長度數字，將這些數字轉化為 ASCII 碼，並將結果附加到輸出字符串後。
每次計算後，該_非 ASCII 字符_會插入回_去除非 ASCII 字符後的字串_，因此 h 會增加 1。
並且在計算下一個非 ASCII 字符 Unicode 碼位時，該碼位需減去上一個非 ASCII 字符碼位。

編碼方式為：0 -> 'a', ..., 25 -> 'z', 26 -> '0', ..., 35 -> '9', 數字的數字按小端序排列。

此處的 36 進位的編碼並非通常意義下的下 36 進制整數，而是一種可變長度的整數。每個數字的最重要（most significant）的位（例如數字 "123" 中的 "1"）在沒有上下文的情況下是可以識別的。因此，多個數字可以無需分隔地連接，不會影響原始數字的識別和提取。但此處每位數字進位的門檻閥值不同，並非每位皆是 36 進位。

### 用於國際化域名的 ACE 前綴

為了防止非國際域名中的連字符被 Punycode 解碼，會在在國際化域名中的 Punycode 序列前加上字符串 `xn--`。這被稱為 ACE（ASCII Compatible Encoding）。

因此，域名 "bücher.tld" 將會以 ASCII 形式表示為 "xn--bcher-kva.tld"。

### 解碼器

解碼器是具有兩個狀態變量 _i_ 和 _n_ 的有限狀態機。

_i_ 是字符串的索引，範圍從 0（代表可能插入的開始）到擴展字符串的當前長度（代表可能插入的結束）。_i_ 從 0 開始。

_n_ 從 128 開始（128 表示第一個非 ASCII 的碼位）。

狀態遞增是一個單調函數。一個狀態轉移要麼增加 _i_，如果 _i_ 達到最大，就重置 _i_ 為零並增加 _n_。下一個狀態轉移時，繼續增加 _i_。 在每個狀態下，由 n 表示的碼位要麼被插入，要麼不被插入。

編碼器生成的數字代表在插入之前要跳過多少可能的結果。

在字符串 "bcher" 中，有六個位置可以插入一個字符（包括第一個字符前和最後一個字符後）。在最後一個 ASCII 碼位（127）和 "ü"（252，參見拉丁字母補充 - 1）之間有 124 個碼位。有一個 "ü" 的插入位置必須跳過（位置 0，即在 'b' 之前）。

因此，在到達正確位置前，解碼器將跳過共（6×124）+1=745 個可能的插入位置。在該字符插入後，將有七個可能的地方插入下一個字符。

### 將碼號重新編碼為 ASCII 序列

Punycode 使用通用變長整數來表示這些值。例如，"kva" 將用來表示碼號 745。

> 
> 小端序的數字系統允許不需要單獨的分隔符的變長編碼：低於閾值的數字標誌著它是最重要的數字，即是數字的結束。為了提高效率，閾值取決於數字的位置和以前的插入。同時，數字的權重也會變化。
> 
> 在使用 36 個符號的數字系統中，不區分大小寫的 'a'-'z' 表示十進位數字 0-25，'0' 到 '9' 表示十進位數字 26-35。因此，"kva"，表示十進位數字序列 "10 21 0"。
> 
> 

為了解碼這串符號，需要閾值序列。本例中，閾值序列為（1，1，26，26，...）。最不重要的數字（least-significant digit）的權重（或位置值）總是 1，具有權重 1 的 'k' 等於 10（1×10=10）。下一個數字的權重取決於第一個閾值：一般來說，對於任意 _n_，第 _n_+1 個數字的權重是第 _n_ 個數字的權重乘以（36 - 第 n 個數字的閾值）。所以第二個符號的權重是 36 減去前一個閾值，即為 35（36-1=35）。因此，前兩個符號 'k' 和 'v' 之和為 10×1+21×35。由於第二個符號不小於其閾值 1，因此還有更多符號。然而，由於本例中第三個符號是 'a'（a=0），可以忽略其權重的計算。因此，"kva" 代表十進位數字（10×1）+（21×35）=745。閾值本身由一個算法為每個編碼字符序列決定，使其保持在 1 和 26 之間（包括 1 和 26）。字符的大小寫可以用來提供關於原始字符串的大小寫信息。由於特殊字符按法編碼算的碼位排序，在插入 "bücher" 中特殊字符 "ü" 時，第一種可能是編碼為 "bcher-kvaa" 的 "büücher"，第二種可能是編碼為 "bcher-kvab" 的 "bücüher"，等等等等。在編碼為 "bcher-kvae" 的 "bücherü" 之後是代表插入 ý 的編碼，即 ü 之後的 Unicode 字符，從編碼為 "bcher-kvaf" 的 "ýbücher" 開始（與編碼為 "bcher-jvab" 的 "übücher" 不同），等等。

為了使編碼、解碼算法簡單，編碼時並沒有試圖阻止一些編碼不允許的 Unicode 值：然而，解碼時應該檢查這些值。

Punycode 的設計使其成可以在所有腳本中工作，並通過在操作過程中嘗試適應字符串中的字符集範圍進行自我優化。它針對字符串由包含 0 個或更多 ASCII 字符和來自其他腳本系統的字符組成的情況進行了優化，同時也能處理任意的 Unicode 字符串。在使用 DNS 時需要注意，域名字符串被認為已經使用 nameprep 進行了規範化處理，並且頂級域名在被 punycode 編碼之前被假設已經根據官方註冊的語言表進行了處理。同時，DNS 協議對輸出 Punycode 字符串的可接受長度也有所限制。

## 例子

下表展示了不同類型輸入的國際化域名編碼的例子。

## 參考文獻