了不起的 Unicode

本文精心挑選了許多優秀的Unicode小技巧、軟件包和資源。

了不起的 Unicode

出品 | CSDN(ID:CSDNnews)

Unicode非常了不起!在Unicode出現之前,國際交流是一團糟——每個人都在ASCII碼錶的後半部分區域(稱為“代碼頁”)定義了自己的擴展和字符集,從而導致各種衝突。想想就知道,德國人要與韓國人只使用127個字符組成的代碼頁進行交流會有多麼困難。

——幸虧有了Unicode標準和統一的交流規範。

Unicode 8.0根據129多種書寫體系,標準化了超過120,000個字符,其中包括現代字符、古代字符,甚至還包括人類尚未解密的文字。Unicode能處理從左到右和從右到左兩種書寫方式,支持組合標記,還支持多種文化、政治、宗教方面的字符,甚至還有表情符號。

Unicode太了不起了,我們對它的崇拜猶如滔滔江水綿綿不絕。

了不起的 Unicode

Unicode的背景

Unicode標準支持什麼字符?

Unicode標準定義了今日所有主流的書寫語言中用到的字符。Unicode支持的書寫體系包括歐洲的語系、中東的從右至左書寫的語系,以及亞洲的多種語系。

Unicode標準還包含了標點符號、聲調符號、數學符號、科技符號、箭頭、各種圖形符號、表情符號,等等。Unicode為聲調符號(用來改變其他字符的符號,如波浪線~)單獨提供了代碼,這些代碼可以與基礎字符組合使用,來表示有聲調的字符(如ñ)。Unicode標準9.0版總共提供了128,172個字符的代碼,其中包括了全世界的字符、圖形和符號。

絕大部分的常用字符都能映射到最前面的64K個代碼點上,這一區域叫做基本多文種平面(basic multilingual plane,簡稱為BMP)。還有十六個補充平面用來編碼其他字符,目前尚有850,000個未使用的代碼點。人們還在考慮在以後的版本中添加更多的字符。

Unicode標準還保留了一些代碼點供私人使用。供應商或最終用戶可以在內部利用這些代碼點表示他們自己的字符和符號,或者通過特殊的字體來使用。BMP上有6,400個私有代碼點,如果不夠的話,補充平面上還有

131,068個私有代碼點可供使用。

Unicode字符編碼

字符編碼標準不僅定義了每個字符的唯一標識(即字符的數字值,或者叫做代碼點),也定義了怎樣用比特來表示這個值。

Unicode標準定義了三種編碼形式,允許同一個數據以一字節、兩字節或四字節的格式來傳輸(即每個代碼單元可以是8比特、16比特或32比特)。同一個字符集可以使用所有三種編碼形式,它們之間可以互相轉換,而不會丟失數據。Unicode聯盟建議根據實際需要,選擇任何一種方便的編碼方式來實現Unicode標準。

UTF-8在HTML和類似協議上非常常用。UTF-8使用變長編碼。它的優點是,對應於ASCII字符集的那些Unicode字符的字節值與它們在ASCII中的值完全相同,因此使用UTF-8編碼的Unicode字符可以在絕大多數已有軟件上使用,無需對軟件做出任何修改。

UTF-16在許多需要平衡性能和存儲效率的環境中非常常用。它足夠緊湊,所有常用的字符都可以用一個16比特的代碼單元來表示,其他字符可以使用一對16比特代碼單元來表示。

UTF-32在無需顧慮內存空間的情況下使用,它是定長編碼,每個字符只有一個代碼單元。每個Unicode字符編碼成一個32比特代碼單元。

在所有三種編碼中,每個字符最多需要4個字節(32比特)表示。

數字問題

Unicode字符集被分成17個核心段,稱為“平面”,每個平面又被分成若干區塊。每個平面的空間足夠容納65,536(216)個代碼點,因此總共有1,114,112個代碼點。還有兩個“私有區域”平面(#16和#17),可以按照使用者的意願定義。這兩個私有平面共包含131,072個代碼點。

了不起的 Unicode

第一個平面叫做“基本多文種平面”,或者稱為BMP。它包含代碼點U+0000到U+FFFF,這個範圍內包含了絕大部分常用字符。另外16個平面(U+010000到U+10FFFF)稱為補充平面。

UTF-16代理對

“BMP之外的字符,例如U+1D306 tetragram for centre (),在UTF-16編碼中只能編碼成兩個16比特代碼單元:0xD834 0xDF06。這種情況稱為代理對(surrogate pair)。注意代理對只表示一個字符。

“代理對的第一個字符永遠在0xD800到0xDBFF的範圍內,稱為高位代理,或者叫起始字節代理。代理對的第二個代碼單元永遠在0xDC00到0xDFFF的範圍內,稱為低位代理,或者叫末端代理。”

——Mathias Bynens

“代理對:一種表示方式,用於表示由兩個16比特代碼單元組成的單個抽象字符,其中第一個值稱為高位代理代碼單元,第二個值稱為低位代理單位。代理對僅在UTF-16中使用。”

——Unicode 8.0 第3.9章,代理對(參見Unicode編碼)

計算代理對

代理字符 Pile of Poo (U+1F4A9) 在UTF-16中必須編碼成代理對,即兩個代理。要將代碼點轉換成代理對,可以使用以下算法(用JavaScript編寫)。注意我們使用的是十六進制表示。

<code>var High_Surrogate = function(Code_Point){ return Math.floor((Code_Point - 0x10000) / 0x400) + 0xD800 };
var Low_Surrogate = function(Code_Point){ return (Code_Point - 0x10000) % 0x400 + 0xDC00 };

// Reverses The Conversion
var Code_Point = function(High_Surrogate, Low_Surrogate){
return (High_Surrogate - 0xD800) * 0x400 + Low_Surrogate - 0xDC00 + 0x10000;
};/<code>
<code> > var codepoint = 0x1F4A9; // 0x1F4A9 == 128169
> High_Surrogate(codepoint).toString(16)
"d83d" // 0xD83D == 55357
> Low_Surrogate(codepoint).toString(16)
"dca9" // 0xDCA9 == 56489

> String.fromCharCode( High_Surrogate(codepoint) , Low_Surrogate(codepoint) );
""
> String.fromCodePoint(0x1F4A9)
""
> '\\\\ud83d\\\\udca9'
""/<code>

組合和解組合

Unicode包括了一種修改字符形狀的機制,大幅擴展了Unicode支持的字符量。使用聲調符號進行組合就是其中一種方式。聲調符號寫在主字符的後面。多個聲調符號可以疊在同一個字符上。對於絕大部分常用的字母聲調組合,Unicode還包括了預先組合好的版本。

特定的字符序列也可以用單個字符表示,稱為“預組合字符”(或者叫組合字符,可以解組合的字符)。例如,字符“ü”可以編碼成單個代碼單元U+00FC “ü”,也可以編碼成基本字符U+0075 “u”後接無空白字符U+0308 “¨”。Unicode標準中設置的預組合字符是為了兼容Latin 1等標準,後者包含了許多預組合字符,如“ü”和“ñ”。

預組合字符可以進行接組合,以保持一致性,或用於分析。例如,需要將一組名稱轉換為英文字母時,可以將字符“ü”解組合為“u”後接非空白字符“¨”。解組合後的結果很容易處理,因為該組合字符可以處理成“u”後接一個修飾字符。這樣很容易進行按字母順序排序等,因為修飾字符不會影響字母順序。Unicode標準為所有預組合字符定義瞭解組合方式(https://unicode.org/versions/Unicode8.0.0/ch03.pdf#page=44)。它還定義了正規化的方式,以便為字符提供唯一的表示方法。

Unicode之謎

來自Mark Davis的《Unicode之謎》幻燈片(https://macchiato.com/slides/UnicodeMyths.pdf)。

  • Unicode只不過是16比特編碼。一些人誤認為Unicode只不過是16比特編碼,每個字符佔用16比特,因此一共有65,536個可能的字符。實際上這是不正確的。這樣是關於Unicode的最大誤解,所以也難怪一些人會這麼想。

  • 任何未分配的代碼點都可以用於內部用途?錯。最終,那些未分配的地方都會被某個字符使用。你應該使用私有用途代碼點,或非字符代碼點。

  • 每個Unicode代碼點都表示一個字符?錯。有許多非字符代碼點(FFFE,FFFF,1FFFE,……)還有許多代理代碼點、私有代碼點和未分配的代碼點,還有控制和格式“字符(RLM,ZWNJ,……)

  • 字符映射是一對一的?錯。映射關係也可能是:

  • 一對多:(ß → SS )

  • 上下文相關:(…Σ ↔ …ς 和 …ΣΤ… ↔ …στ… )

  • 語言相關:( I ↔ ı 和 İ ↔ i )

實用Unicode編碼手冊

了不起的 Unicode

編碼類型編碼

了不起的 Unicode
了不起的 Unicode

神奇的字符列表

了不起的 Unicode

特殊字符

詳情可以參照Unicode聯盟發佈的《通用標點符號表》(https://www.unicode.org/charts/PDF/U2000.pdf)。

了不起的 Unicode了不起的 Unicode
了不起的 Unicode

等等,你說什麼?

變量標識符可以包含空白!

U+3164 HANGUL FILLER 字符顯示為佔據空間的空白字符。如果渲染器不支持,則會渲染成完全不可見(也不會佔據任何空間,即“零寬度”)。這就是說,永遠不會看到醜陋的字符替代符號。

我不知道為什麼U+3164被設計成這種行為。有意思的是,U+3164是在Unicode 1.1版本(

1993年)加入的,所以聯盟一定是花了很多時間思考它。下面是幾個例子:

<code>> var ᅟ = 'foo';
undefined
> ᅟ
'foo'


> var ㅤ= alert;
undefined
> var foo = 'bar'
undefined
> if ( foo ===ㅤ`baz` ){} // alert
undefined


> var varㅤfooㅤ\\\\u{A60C}ㅤπ = 'bar';
undefined
> varㅤfooㅤꘌㅤπ
'bar'
/<code>

注意:我在Ubuntu和OSX下測試了下述程序的渲染結果:Node,PHP,Ruby,Python3.5,Scala,Vim,Cat,Chrome+GitHub gist。Atom是唯一無法正確渲染,將其顯示成空方塊的程序。我還沒有測試Emacs和Sublime。據我的理解,Unicode聯盟不會改變或重命名字符或代碼點,但有可能會改變字符屬性,如ID_Start或ID_Continue等。

修飾符

零寬度連接符(ZWJ)是個不可打印字符,用於某些複雜語系的計算機排版系統中,如阿拉伯語系、印度語系等。將ZWJ放在兩個本來不會連接的字符之間,將會導致它們以連接的形式打印。

零寬度不連接符(ZWNJ)是個不可打印字符,那些使用連接的書寫系統的計算機化。將ZWNJ放在兩個本來會連接在一起的字符之間,會導致它們以本來的形式打印。空格字符也有同樣的效果,但ZWNJ的作用是它能保證輸出的兩個字符儘可能靠近,或者連接一個詞及其語素。

<code>> 'a'
"a"

> 'a\\\\u{0308}'
"ä"

> 'a\\\\u{20DE}\\\\u{0308}'
"a⃞̈"

> 'a\\\\u{20DE}\\\\u{0308}\\\\u{20DD}'
"a⃞̈⃝"

// Modifying Invisible Characters
> '\\\\u{200E}\\\\u{200E}\\\\u{200E}\\\\u{200E}\\\\u{200E}\\\\u{200E}\\\\u{200E}\\\\u{200E}\\\\u{200E}\\\\u{200E}'
" "

> '\\\\u{200E}\\\\u{200E}\\\\u{200E}\\\\u{200E}\\\\u{200E}\\\\u{200E}\\\\u{200E}\\\\u{200E}\\\\u{200E}\\\\u{200E}'.length
10/<code>

大寫變換衝突

了不起的 Unicode

小寫變換衝突

了不起的 Unicode

奇怪現象和排查方法

  • 字符串長度通常由統計代碼點的個數來計算。這就是說,代理對會被統計成兩個字符。多個聲調符號會疊放在同一個字符上,例如a + ̈ == ̈a,從而增加長度,但它們只會產生一個字符。

  • 類似地,字符串翻轉通常非常困難。同樣,代理對和帶有聲調符號的字符必須作為整體進行翻轉。ES Reverser提供了一個非常好的解決方法。

  • 字符映射是一對一的?錯。映射關係也可能是:

  • 一對多:(ß → SS )

  • 上下文相關:(…Σ ↔ …ς 和 …ΣΤ… ↔ …στ… )

  • 語言相關:( I ↔ ı 和 İ ↔ i )

一對多映射

絕大多數字符,在大寫的時候表示一對多關係;另一些字符小寫的時候表示一對多關係。

了不起的 Unicode

優秀的軟件包和庫

  • PhantomScript::ghost: :flashlight: 不可見的JavaScript代碼執行和社會工程工具。

  • ESReverser:用JavaScript編寫的支持Unicode的字符串翻轉。

  • mimic:Unicode的惡作劇。

  • python-ftfy:輸入Unicode文本,輸出更一致、更不容易出現顯示錯誤的表現形式。

  • vim-troll-stopper:防止Unicode的搗亂字符搞亂你的代碼。

了不起的 Unicode

表情符號

  • Unicode聯盟的表情符號表(https://www.unicode.org/emoji/charts/full-emoji-list.html)

  • Emojipedia(https://emojipedia.org/):關於特定表情符號的信息,新聞博客。

  • World Translation Foundation(https://www.emojifoundation.com/):宣傳、探索,還可以將文本翻譯成用表情符號表示的形式。

  • Can I Emoji? (https://caniemoji.com/android-2/):顯示當前iOS、Android和Windows對於表情符號的原生支持情況。

  • 怎樣註冊一個表情符號URL(https://www.name.com/blog/how-tos/2015/12/want-an-emoji-url-this-is-how-you-register-one/)

多樣性

Unicode聯盟在支持人類的多樣性和多元文化方面做出了很多努力。這裡是聯盟提供的多樣性報告(https://unicode.org/reports/tr51/#Diversity)。

現在表情符號已經支持混合型別,比如同性家庭、握手、接吻等。真正轟動的是表情符號組合序列。基本上來說:

了不起的 Unicode

此外,表情符號現在還支持膚色修飾字符了。

“有五個符號修飾字符可以為Unicode 8.0版(2015年中期)中發佈的人類的表情符號提供一系列的膚色。這些字符基於Fitzpatrick度量(皮膚學上的著名標準,網上也有許多例子,比如FitzpatrickSkinType.pdf)定義的六種膚色。不同的實現的精確顏色可能不同。”

——Unicode聯盟的多樣性報告

只需要在所需的表情符號後面接上膚色修飾字符 \\\\u{1F466}\\\\u{1F3FE} 即可。

了不起的 Unicode了不起的 Unicode

有創意的變量名和方法名

示例採用JavaScript(ES6)編寫。

一般而言,帶有ID_START屬性的字符可以用在變量名開頭,而帶有ID_CONTINUE屬性的字符可以用在變量名中除了首字符之外的其他位置。

<code>function rand(μ,σ){ ... };

String.prototype.reverseⵑ = function{..};

Number.prototype.isTrueɁ = function{..};

var WhatDoesThisDoɁɁɁɁ = 42
/<code>

下面是Mathias Bynes(https://mathiasbynens.be/notes/javascript-identifiers#examples)提供的一些極富創意的變量名:

<code>// How convenient!
var π = Math.PI;

// Sometimes, you just have to use the Bad Parts of JavaScript:
var ಠ_ಠ = eval;

// Code, Y U NO WORK?!
var ლ_ಠ益ಠ_ლ = 42;

// How about a JavaScript library for functional programming?
var λ = function {};

// Obfuscate boring variable names for great justice
var \\\\u006C\\\\u006F\\\\u006C\\\\u0077\\\\u0061\\\\u0074 = 'heh';

// …or just make up random ones
var Ꙭൽↈⴱ = 'huh';

// While perfectly valid, this doesn’t work in most browsers:
var foo\\\\u200Cbar = 42;

// This is *not* a bitwise left shift (`<var 〱〱 = 2;

// This is, though:
〱〱 << 〱〱; // 8

// Give yourself a discount:
var price_9̶9̶_89 = 'cheap';

// Fun with Roman numerals
var Ⅳ = 4;
var Ⅴ = 5;
Ⅳ + Ⅴ; // 9

// Cthulhu was here
var Hͫ̆̒̐ͣ̊̄ͯ͗͏̵̗̻̰̠̬͝ͅE̴̷̬͎̱̘͇͍̾ͦ͊͒͊̓̓̐_̫̠̱̩̭̤͈̑̎̋ͮͩ̒͑̾͋͘Ç̳͕̯̭̱̲̣̠̜͋̍O̴̦̗̯̹̼ͭ̐ͨ̊̈͘͠M̶̝̠̭̭̤̻͓͑̓̊ͣͤ̎͟͠E̢̞̮̹͍̞̳̣ͣͪ͐̈T̡̯̳̭̜̠͕͌̈́̽̿ͤ̿̅̑Ḧ̱̱̺̰̳̹̘̰́̏ͪ̂̽͂̀͠ = 'Zalgo';
/<code>

下面是David Walsh提供的一些Unicode CSS類名(https://davidwalsh.name/unicode-css-classes):

<code>



You do not have access to this page.



Your changes have been saved successfully!
/<code>
<code>.ಠ_ಠ {
border: 1px solid #f00;
}

.❤ {
background: lightgreen;
}/<code>
了不起的 Unicode

遞歸的HTML標籤重命名腳本

如果你想把所有HTML標籤重命名,使之看上去像什麼都沒有,那麼可以使用以下的腳本。

但要注意,HTML並不會支持所有的Unicode字符。

<code>// U+1160 HANGUL JUNGSEONG FILLER
transformAllTags('ᅠ');

// An actual HTML element node designed to look like a comment node, using the U+01C3 LATIN LETTER RETROFLEX CLICK
// ǃ-->
transformAllTags('ǃ--');

// or even transformAllTags('\\\\u{1160}\\\\u{20dd}');

// and for a bonus, all existing tag names will have each character ensquared. h⃞t⃞m⃞l⃞
transformAllTags;


function transformAllTags(newName){
// querySelectorAll doesn't actually return an array.
Array.from(document.querySelectorAll('*'))
.forEach(function(x){
transformTag(x, newName);
});
}

functionwonky(str){

return str.split('').join('\\\\u{20de}') + '\\\\u{20de}';
}

functiontransformTag(tagIdOrElem, tagType){
var elem = (tagIdOrElem instanceof HTMLElement) ? tagIdOrElem : document.getElementById(tagIdOrElem);
if(!elem || !(elem instanceof HTMLElement))return;
var children = elem.childNodes;
var parent = elem.parentNode;
var newNode = document.createElement(tagType||wonky(elem.tagName));
for(var a=0;a<elem.attributes.length>newNode.setAttribute(elem.attributes[a].nodeName, elem.attributes[a].value);
}
for(var i= 0,clen=children.length;i<clen>newNode.appendChild(children[0]); //0...always point to the first non-moved element
}
newNode.style.cssText = elem.style.cssText;
parent.replaceChild(newNode,elem);
}/<clen>/<elem.attributes.length>/<code>

下面是確定能夠支持的字符:

<code>function testBegin(str){
try{
eval(`document.createElement( '${str}' );`)
return true;
}
catch(e){ return false; }
}

function testContinue(str){
try{
eval(`document.createElement( 'a${str}' );`)
return true;
}
catch(e){ return false; }
}/<code>

下面是一些基本的結果:

<code>// Test if dashes can start an HTML Tag
> testBegin('-')
< false

> testContinue('-')
< true

> testBegin('ᅠ-') // Prepend dash with U+1160 HANGUL JUNGSEONG FILLER
< true
/<code>
了不起的 Unicode

Unicode字體

單一的TrueType / OpenType 字體格式無法支持所有UTF-8字符,因為字體文件有最大65535個字形的限制。UTF-8的字形超過了110萬,因此你需要一個font-family才能覆蓋所有字體。

  • https://en.wikipedia.org/wiki/Unicode_font#List_of_Unicode_fonts

  • https://www.unifont.org/fontguide/

了不起的 Unicode

Unicode標準的原則

Unicode標準設定了下述基本原則:

  • 通用原則——曾經出現過的一切書寫系統都應該在標準中體現。

  • 邏輯順序——在雙向文本中,字符以邏輯順序存儲,而不是表現順序存儲。

  • 效率——文檔必須是高效的、完整的。

  • 統一——不同文化或語言使用同一個字符時,應該僅存儲一次。

  • 記錄字符而不是字形——應當對字符進行編碼,而不是字形。字形就是字符的實際圖形表示。

  • 動態組合——新的字符可以與已有的標準化後的字符進行組合。例如,字符“Ä”可以用字符“A”和分音符“¨”組合而成。

  • 語義——包含的字符必須有明確定義,必須與其他字符有明確的區別。

  • 穩定——字符一旦被定義,就永遠不能被移除,其代碼點也不能被挪作他用。如果出錯,則應該將代碼點標記為棄用。

  • 純文本——標準中的字符應當是純文本,永遠不應該包含標記或元字符。

  • 可轉換——每一種編碼都應該可以用Unicode編碼表示。

原文:https://wisdom.engineering/awesome-unicode/

本文為 CSDN 翻譯,轉載請註明來源出處。

【End】

Python系列學習成長課來了!15年經驗專家、CSDN特級講師親自授課,還等什麼?立即掃碼報名學習:


分享到:


相關文章: