天天看點

藝術鬼才!Unicode 字元還能這麼玩?

上周的時候,朋友圈的直升飛機不知道為什麼就火了,很多朋友開着各種花式飛機帶着起飛。

藝術鬼才!Unicode 字元還能這麼玩?
還沒來得及了解咋回事來着,這個直升飛機就🔥到的微網誌熱搜。
藝術鬼才!Unicode 字元還能這麼玩?
後面越來越多人開來他們的直升飛機,盤旋在朋友圈上方。于是很多朋友開來他們的坦克,專打直升飛機,一轟一個準。
藝術鬼才!Unicode 字元還能這麼玩?

好了,說回正題!

程式員朋友應該都很熟悉 Unicode (萬國碼),它幾乎包含世界上所有符号,比如組成直升飛機這幾個特殊符号對應的 Unicode 碼分别為:

藝術鬼才!Unicode 字元還能這麼玩?
藝術鬼才!Unicode 字元還能這麼玩?

ps:推薦一個網站,可以根據符号搜對應的 Unicode 碼:https://unicode.yunser.com/unicode

除了這些正常字元以外,Unicode 還包含着各種各樣的奇葩字元。

奇葩字元

除了正常的我們熟知的文字以外,Unicode 中還有一些奇怪的文字,比如下面這些文字

藝術鬼才!Unicode 字元還能這麼玩?
藝術鬼才!Unicode 字元還能這麼玩?
藝術鬼才!Unicode 字元還能這麼玩?

除了這些奇怪文字以外,Unicode 還有一些奇葩的的符号。

例如下面一整套麻将牌:

藝術鬼才!Unicode 字元還能這麼玩?

一整套的撲克牌:

藝術鬼才!Unicode 字元還能這麼玩?

一整套國際象棋:

藝術鬼才!Unicode 字元還能這麼玩?

除了這些,通過組合符合,我們還可以造出各種各樣的顔文字(๑•̀ㅂ•́)و✧、

藝術鬼才!Unicode 字元還能這麼玩?

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

藝術鬼才!Unicode 字元還能這麼玩?

除了這些之外,Unicode 中還有一些特殊字元的,利用這些字元,我們還可以玩出很多有趣的騷操作。

組合字元

Unicode 有一類字元稱為組合字元,它可以附加在前一個非組合字元上,進而使整體看起來像是一個字元。

組合字元原來目的是為了解決一些地區語言、文字特殊的需要,比如說泰文聲調符号與母音符号。

藝術鬼才!Unicode 字元還能這麼玩?
藝術鬼才!Unicode 字元還能這麼玩?

正常使用的情況下,這些組合字元數量都會有一些限制。但是在 Unicode 組合字元設計上,并沒有加這種限制,這樣使我們可以無限加這類組合字元。

利用這個特性,可以達到一些惡搞效果,比如「擊穿天花闆」與「鑿穿地闆」的效果。

藝術鬼才!Unicode 字元還能這麼玩?

上面實作原理其是利用以下兩個組合字元:

藝術鬼才!Unicode 字元還能這麼玩?
藝術鬼才!Unicode 字元還能這麼玩?

隻要複制這兩個字元相應的 HTML 代碼,跟在正常的字元後面,就可以使這兩個字元附加在普通字元上,比如下面實作效果為

黑̮̑
           
藝術鬼才!Unicode 字元還能這麼玩?
Unicode 碼值通常使用

U+N

(16 進制N 代表碼值),比如 A 的碼值為 U+0041。

在 HTML 中 Unicode 可以使用

&#N;

(十進制,N 代表碼值)表示

在 JS 中 Unicode 中需要使用]

\uN

(16 進制N 代表碼值)表示

隻要我們在普通字元多複制幾個這類附加字元,就可以形成上述「擊穿」效果。

還記得上面說的泰文嗎,曾經有一段時間貼吧,很流行一種噴射文,比如下面的效果。

藝術鬼才!Unicode 字元還能這麼玩?
藝術鬼才!Unicode 字元還能這麼玩?
藝術鬼才!Unicode 字元還能這麼玩?

這種噴射文實際原理就是利用泰文中聲調符号附加在其他正常符号上。

藝術鬼才!Unicode 字元還能這麼玩?

不過現在這個效果貌似已經沒辦法再複現了,現在我們隻能看到這樣的效果:

藝術鬼才!Unicode 字元還能這麼玩?

在一些老版本的系統/浏覽器可能還能看到這種效果,知道的小夥伴留言區可以告知一下。

零寬字元

Unicode 中還有一類格式字元,不可見,不可列印,主要作用于調整字元的顯示格式,是以我們将其稱為零寬字元。

零寬字元主要有以下幾類:

零寬度空格符 (zero-width space) U+200B : 用于較長單詞的換行分隔

零寬度非斷空格符 (zero width no-break space) U+FEFF : 用于阻止特定位置的換行分隔

零寬度連字元 (zero-width joiner) U+200D : 用于阿拉伯文與印度語系等文字中,使不會發生連字的字元間産生連字效果

零寬度斷字元 (zero-width non-joiner) U+200C : 用于阿拉伯文,德文,印度語系等文字中,阻止會發生連字的字元間的連字效果

左至右符 (left-to-right mark) U+200E : 用于在混合文字方向的多種語言文本中(例:混合左至右書寫的英語與右至左書寫的希伯來語),規定排版文字書寫方向為左至右

右至左符 (right-to-left mark) U+200F : 用于在混合文字方向的多種語言文本中,規定排版文字書寫方向為右至左

利用零寬字元不不可見的特性,我們也可以玩出一些騷效果。

空白微網誌

釋出微網誌的時候,如果内容都是空格,将沒辦法釋出。

藝術鬼才!Unicode 字元還能這麼玩?

但是如果我們将零寬字元,比如說「零寬度空格符 U+200B」複制到微網誌,這樣我們就可以釋出空白微網誌。

我們可以利用 Chrome 浏覽器的控制台複制零寬字元,操作方式如下:

藝術鬼才!Unicode 字元還能這麼玩?

釋出效果如下:

藝術鬼才!Unicode 字元還能這麼玩?

隐形水印

對于一些内部論壇或者說小說網站來說,可以通過零寬字元在文章或小說内容嵌入隐形水印。

當這些内容被一些爬蟲複制到其他網站時,我們就可以通過隐形水印,輕松查找時那位使用者洩漏内容。

隐形水印主要原理就是将使用者資訊比如使用者名,通過一定算法轉成零寬字元,這樣普通使用者浏覽時完全看不到這個水印。

如果内容被複制到其他網站,隐形誰赢也被複制,隻要找到這個水印,将這些零寬字元反轉成使用者名即可。

下面展示一種轉換方法,JS 代碼主要參考以下 Github 項目:

https://github.com/umpox/zero-width-detection

隐形水印生成方法

第一步我們需要将明文字元串每個字元都轉成二進制串。

// 每個字元轉為二進制,用空格分隔
    const textToBinary = username => (
      username
      .split('')
      // charCodeAt 将字元轉成相應的 Unicode 碼值
      .map(char => char.charCodeAt(0).toString(2))
      .join(' ')
    );
           

示例如下:

藝術鬼才!Unicode 字元還能這麼玩?

第二步,将二進制串轉為零度字元串,轉換規則如下:

  • 1 轉換為 \u200b 零寬度字元(zero-width space)
  • 0 轉換為 \u200c 零寬度斷字元(zero-width non-joiner)
  • 其他(剩餘就是空格) 轉換為 \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;
};
           

使用加密方法将明文字元串加密之後,加密字元串肉眼是看不到了,但是實際還是存在的。

藝術鬼才!Unicode 字元還能這麼玩?

實際上,如果我們将加密之後字元串複制到 BEJSON 網站,就可以看到字元。

藝術鬼才!Unicode 字元還能這麼玩?

另外你還可以把加密字元串複制到 IDEA 中,可以看到相應的 Unicode 編碼值。

藝術鬼才!Unicode 字元還能這麼玩?

解密隐形水印

知道了加密的方式,解密其實就很簡單,我們隻要按照相反步驟的來就可以了。

第一步,将隐形水印按照以下規則轉換為二進制串。轉換規則如下:

  • 使用 \ufeff 分隔字元串
  • \u200b 轉為 1
  • \u200c 轉為 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('')
);
           

調用該方法,隐形水印轉成二進制串。

藝術鬼才!Unicode 字元還能這麼玩?

第二步,将二進制再轉為相應的字元。

const binaryToText = string => (
  // fromCharCode 二進制轉化
  string.split(' ').map(num => String.fromCharCode(parseInt(num, 2))).join('')
);
           

最終解密方法如下:

const decode = zeroWidthUsername => {
  const binaryUsername = zeroWidthToBinary(zeroWidthUsername);
  const textUsername = binaryToText(binaryUsername);
  return textUsername;
};
           

解密示例如下:

藝術鬼才!Unicode 字元還能這麼玩?

短網址

我們常用的短網址,域名後面會跟上一串随機串,進而實作短網址到長網址的映射。比如以下網址:

https://sourl.cn/iLyn9S

然而我們可以利用零寬字元也可以實作短網址的效果,,比如下面這個網站,就可以生成這類短網址。

https://zws.im/

藝術鬼才!Unicode 字元還能這麼玩?

可以看到這個短網址後面看不到任何字元,實際上這後面跟着一串零寬字元。當浏覽器通路該短網址時,後端程式隻要反解密的後面零寬字元,拿到相應的網址,然後在做跳轉就可以到指定的網站。

反解密的原理可以參考上面隐形水印的代碼

小心零寬字元

日常開發過程中,我們有時需要從一些檔案中讀取文本内容,然後做相應的處理。

有時候我們可能會碰到一些詭異的現象,比如我們之前碰到的例子。

背景程式從 Excel 讀取文本内容,然後程式中判斷是讀取的文本内容是否與指定的字元串相等。

然後當我們讀取一份 Excel 内容後,返現這段比較邏輯怎麼也通過不了。本來以為是 Excel 内容存在空格什麼的,但是打開 Excel 仔細一看,跟指定字元串一模一樣,并沒有什麼其他字元。

第一次碰到這種例子,沒有什麼經驗,真的排查了很久,到最後都有點懷疑人生了。最後無意間将文本内容複制到了 IDEA 中,才發現整理混雜着零寬字元!

藝術鬼才!Unicode 字元還能這麼玩?

如果各位小夥伴也碰到這類問題,不妨将複制文本内容,然後到 IDEA 中檢視是否存在某些看不見字元~

最後(點個贊呗!)

這兩個星期一直很忙,一直都在 9106 的節奏,真的是累,是以斷更了一周!

所幸最近項目提測,稍微輕松了一點,能有點劃水時間來寫寫文章。不過再提起筆來寫文章,就有點斷節奏了!

這篇文章墨迹了很久才水出來,下周開始再次恢複周更的節奏,再忙再累,每周都來一篇。

歡迎各位小夥伴,每周來這裡蹲我,Gank 我!!!

好了,我是樓下小黑哥,下周見!!!

藝術鬼才!Unicode 字元還能這麼玩?

參考連結

  1. https://juejin.im/post/5d3f01e7f265da03c23ead69
  2. http://zero.rovelast.com/
  3. https://imweb.io/topic/5a08a5c7ef79bc941c30d8dd
歡迎關注我的公衆号:程式通事,獲得日常幹貨推送。如果您對我的專題内容感興趣,也可以關注我的部落格:studyidea.cn