沒想到 Unicode 字符還能這樣玩?
點(diǎn)擊藍(lán)色“程序通事”關(guān)注我喲
加個(gè)“星標(biāo)”,每周來 Gank
上周的時(shí)候,朋友圈的直升飛機(jī)不知道為什么就火了,很多朋友開著各種花式飛機(jī)帶著起飛。

還沒來得及了解咋回事來著,這個(gè)直升飛機(jī)就?到的微博熱搜。

后面越來越多人開來他們的直升飛機(jī),盤旋在朋友圈上方。于是很多朋友開來他們的坦克,專打直升飛機(jī),一轟一個(gè)準(zhǔn)。

好了,說回正題!
程序員朋友應(yīng)該都很熟悉 Unicode (萬國碼),它幾乎包含世界上所有符號(hào),比如組成直升飛機(jī)這幾個(gè)特殊符號(hào)對(duì)應(yīng)的 Unicode 碼分別為:


ps:推薦一個(gè)網(wǎng)站,可以根據(jù)符號(hào)搜對(duì)應(yīng)的 Unicode 碼:https://unicode.yunser.com/unicode
除了這些正常字符以外,Unicode 還包含著各種各樣的奇葩字符。
奇葩字符
除了正常的我們熟知的文字以外,Unicode 中還有一些奇怪的文字,比如下面這些文字



除了這些奇怪文字以外,Unicode 還有一些奇葩的的符號(hào)。
例如下面一整套麻將牌:

一整套的撲克牌:

一整套國際象棋:

除了這些,通過組合符合,我們還可以造出各種各樣的顏文字(??????)??、

另外 Unicode 還收錄著我們常用的 Emoji 。

除了這些之外,Unicode 中還有一些特殊字符的,利用這些字符,我們還可以玩出很多有趣的騷操作。
組合字符
Unicode 有一類字符稱為組合字符,它可以附加在前一個(gè)非組合字符上,從而使整體看起來像是一個(gè)字符。
組合字符原來目的是為了解決一些地區(qū)語言、文字特殊的需要,比如說泰文聲調(diào)符號(hào)與母音符號(hào)。


正常使用的情況下,這些組合字符數(shù)量都會(huì)有一些限制。但是在 Unicode 組合字符設(shè)計(jì)上,并沒有加這種限制,這樣使我們可以無限加這類組合字符。
利用這個(gè)特性,可以達(dá)到一些惡搞效果,比如「擊穿天花板」與「鑿穿地板」的效果。

上面實(shí)現(xiàn)原理其是利用以下兩個(gè)組合字符:


只要復(fù)制這兩個(gè)字符相應(yīng)的 HTML 代碼,跟在正常的字符后面,就可以使這兩個(gè)字符附加在普通字符上,比如下面實(shí)現(xiàn)效果為
黑??

Unicode 碼值通常使用
U+N(16 進(jìn)制N 代表碼值),比如 A 的碼值為 U+0041。在 HTML 中 Unicode 可以使用
N;(十進(jìn)制,N 代表碼值)表示在 JS 中 Unicode 中需要使用]
\uN(16 進(jìn)制N 代表碼值)表示
只要我們?cè)谄胀ㄗ址鄰?fù)制幾個(gè)這類附加字符,就可以形成上述「擊穿」效果。
還記得上面說的泰文嗎,曾經(jīng)有一段時(shí)間貼吧,很流行一種噴射文,比如下面的效果。
?


這種噴射文實(shí)際原理就是利用泰文中聲調(diào)符號(hào)附加在其他正常符號(hào)上。

不過現(xiàn)在這個(gè)效果貌似已經(jīng)沒辦法再復(fù)現(xiàn)了,現(xiàn)在我們只能看到這樣的效果:

在一些老版本的系統(tǒng)/瀏覽器可能還能看到這種效果,知道的小伙伴留言區(qū)可以告知一下。
零寬字符
Unicode 中還有一類格式字符,不可見,不可打印,主要作用于調(diào)整字符的顯示格式,所以我們將其稱為零寬字符。
零寬字符主要有以下幾類:
零寬度空格符 (zero-width space) U+200B : 用于較長單詞的換行分隔
零寬度非斷空格符 (zero width no-break space) U+FEFF : 用于阻止特定位置的換行分隔
零寬度連字符 (zero-width joiner) U+200D : 用于阿拉伯文與印度語系等文字中,使不會(huì)發(fā)生連字的字符間產(chǎn)生連字效果
零寬度斷字符 (zero-width non-joiner) U+200C : 用于阿拉伯文,德文,印度語系等文字中,阻止會(huì)發(fā)生連字的字符間的連字效果
左至右符 (left-to-right mark) U+200E : 用于在混合文字方向的多種語言文本中(例:混合左至右書寫的英語與右至左書寫的希伯來語),規(guī)定排版文字書寫方向?yàn)樽笾劣?/p>
右至左符 (right-to-left mark) U+200F : 用于在混合文字方向的多種語言文本中,規(guī)定排版文字書寫方向?yàn)橛抑磷?/p>
利用零寬字符不不可見的特性,我們也可以玩出一些騷效果。
空白微博
發(fā)布微博的時(shí)候,如果內(nèi)容都是空格,將沒辦法發(fā)布。

但是如果我們將零寬字符,比如說「零寬度空格符 U+200B」復(fù)制到微博,這樣我們就可以發(fā)布空白微博。
我們可以利用 Chrome 瀏覽器的控制臺(tái)復(fù)制零寬字符,操作方式如下:

發(fā)布效果如下:

隱形水印
對(duì)于一些內(nèi)部論壇或者說小說網(wǎng)站來說,可以通過零寬字符在帖子或小說內(nèi)容嵌入隱形水印。
當(dāng)這些內(nèi)容被一些爬蟲復(fù)制到其他網(wǎng)站時(shí),我們就可以通過隱形水印,輕松查找時(shí)那位用戶泄漏內(nèi)容。
隱形水印主要原理就是將用戶信息比如用戶名,通過一定算法轉(zhuǎn)成零寬字符,這樣普通用戶瀏覽時(shí)完全看不到這個(gè)水印。
如果內(nèi)容被復(fù)制到其他網(wǎng)站,隱形誰贏也被復(fù)制,只要找到這個(gè)水印,將這些零寬字符反轉(zhuǎn)成用戶名即可。
下面展示一種轉(zhuǎn)換方法,JS 代碼主要參考以下 Github 項(xiàng)目:
https://github.com/umpox/zero-width-detection
隱形水印生成方法
第一步我們需要將明文字符串每個(gè)字符都轉(zhuǎn)成二進(jìn)制串。
????//?每個(gè)字符轉(zhuǎn)為二進(jìn)制,用空格分隔
????const?textToBinary?=?username?=>?(
??????username
??????.split('')
??????//?charCodeAt?將字符轉(zhuǎn)成相應(yīng)的?Unicode?碼值
??????.map(char?=>?char.charCodeAt(0).toString(2))
??????.join('?')
????);
示例如下:

第二步,將二進(jìn)制串轉(zhuǎn)為零度字符串,轉(zhuǎn)換規(guī)則如下:
1 轉(zhuǎn)換為 \u200b 零寬度字符(zero-width space) 0 轉(zhuǎn)換為 \u200c 零寬度斷字符(zero-width non-joiner) 其他(剩余就是空格) 轉(zhuǎn)換為 \u200d 零寬度連字符 (zero-width joiner) 最后使用 \ufeff 零寬度非斷空格符 (zero width no-break space) 作為分隔符
const?binaryToZeroWidth?=?binary?=>?(
??binary.split('').map((binaryNum)?=>?{
????const?num?=?parseInt(binaryNum,?10);
????if?(num?===?1)?{
??????return?'\u200b';?//?\u200b?零寬度字符(zero-width?space)
????}?else?if(num===0)?{
??????return?'\u200c';?//?\u200c?零寬度斷字符(zero-width?non-joiner)
????}
????return?'\u200d';?//?\u200d?零寬度連字符?(zero-width?joiner)
??}).join('\ufeff')?//?\ufeff?零寬度非斷空格符?(zero?width?no-break?space)
);
最終加密方法如下:
const?encode?=?username?=>?{
??const?binaryUsername?=?textToBinary(username);
??const?zeroWidthUsername?=?binaryToZeroWidth(binaryUsername);
??return?zeroWidthUsername;
};
使用加密方法將明文字符串加密之后,加密字符串肉眼是看不到了,但是實(shí)際還是存在的。

實(shí)際上,如果我們將加密之后字符串復(fù)制到 ?BEJSON 網(wǎng)站,就可以看到字符。

另外你還可以把加密字符串復(fù)制到 IDEA 中,可以看到相應(yīng)的 Unicode 編碼值。

解密隱形水印
知道了加密的方式,解密其實(shí)就很簡單,我們只要按照相反步驟的來就可以了。
第一步,將隱形水印按照以下規(guī)則轉(zhuǎn)換為二進(jìn)制串。轉(zhuǎn)換規(guī)則如下:
使用 \ufeff 分隔字符串 \u200b 轉(zhuǎn)為 1 \u200c 轉(zhuǎn)為 0 其他字符使用空格
const?zeroWidthToBinary?=?string?=>?(
??string.split('\ufeff').map((char)?=>?{?//?\ufeff?零寬度非斷空格符?(zero?width?no-break?space)
????if?(char?===?'\u200b')?{?//?\u200b?零寬度字符(zero-width?space)
??????return?'1';
????}?else?if(char?===?'\u200c')?{?//?\u200c?零寬度斷字符(zero-width?non-joiner)
??????return?'0';
????}
????return?'?';
??}).join('')
);
調(diào)用該方法,隱形水印轉(zhuǎn)成二進(jìn)制串。

第二步,將二進(jìn)制再轉(zhuǎn)為相應(yīng)的字符。
const?binaryToText?=?string?=>?(
??//?fromCharCode?二進(jìn)制轉(zhuǎn)化
??string.split('?').map(num?=>?String.fromCharCode(parseInt(num,?2))).join('')
);
最終解密方法如下:
const?decode?=?zeroWidthUsername?=>?{
??const?binaryUsername?=?zeroWidthToBinary(zeroWidthUsername);
??const?textUsername?=?binaryToText(binaryUsername);
??return?textUsername;
};
解密示例如下:

短網(wǎng)址
我們常用的短網(wǎng)址,域名后面會(huì)跟上一串隨機(jī)串,從而實(shí)現(xiàn)短網(wǎng)址到長網(wǎng)址的映射。比如以下網(wǎng)址:
https://sourl.cn/iLyn9S
然而我們可以利用零寬字符也可以實(shí)現(xiàn)短網(wǎng)址的效果,,比如下面這個(gè)網(wǎng)站,就可以生成這類短網(wǎng)址。
https://zws.im/

可以看到這個(gè)短網(wǎng)址后面看不到任何字符,實(shí)際上這后面跟著一串零寬字符。當(dāng)瀏覽器訪問該短網(wǎng)址時(shí),后端程序只要反解密的后面零寬字符,拿到相應(yīng)的網(wǎng)址,然后在做跳轉(zhuǎn)就可以到指定的網(wǎng)站。
反解密的原理可以參考上面隱形水印的代碼
小心零寬字符
日常開發(fā)過程中,我們有時(shí)需要從一些文件中讀取文本內(nèi)容,然后做相應(yīng)的處理。
有時(shí)候我們可能會(huì)碰到一些詭異的現(xiàn)象,比如我們之前碰到的例子。
后臺(tái)程序從 Excel 讀取文本內(nèi)容,然后程序中判斷是讀取的文本內(nèi)容是否與指定的字符串相等。
然后當(dāng)我們讀取一份 Excel 內(nèi)容后,返現(xiàn)這段比較邏輯怎么也通過不了。本來以為是 Excel 內(nèi)容存在空格什么的,但是打開 Excel 仔細(xì)一看,跟指定字符串一模一樣,并沒有什么其他字符。
第一次碰到這種例子,沒有什么經(jīng)驗(yàn),真的排查了很久,到最后都有點(diǎn)懷疑人生了。最后無意間將文本內(nèi)容復(fù)制到了 IDEA 中,才發(fā)現(xiàn)整理混雜著零寬字符!

如果各位小伙伴也碰到這類問題,不妨將復(fù)制文本內(nèi)容,然后到 IDEA 中查看是否存在某些看不見字符~
最后(點(diǎn)個(gè)贊唄?。?/span>
這兩個(gè)星期一直很忙,一直都在 9106 的節(jié)奏,真的是累,所以斷更了一周!
所幸最近項(xiàng)目提測,稍微輕松了一點(diǎn),能有點(diǎn)劃水時(shí)間來寫寫文章。不過再提起筆來寫文章,就有點(diǎn)斷節(jié)奏了!
這篇文章墨跡了很久才水出來,下周開始再次恢復(fù)周更的節(jié)奏,再忙再累,每周都來一篇。
歡迎各位小伙伴,每周來這里蹲我,Gank 我?。?!
好了,我是樓下小黑哥,下周見!??!

參考鏈接
https://juejin.im/post/5d3f01e7f265da03c23ead69 http://zero.rovelast.com/ https://zws.im/ https://imweb.io/topic/5a08a5c7ef79bc941c30d8dd
