整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

了不起的 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标准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个代码点。

第一个平面叫做“基本多文种平面”,或者称为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编写)。注意我们使用的是十六进制表示。

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;
};
 > 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'
""

组合和解组合

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联盟发布的《通用标点符号表》(https://www.unicode.org/charts/PDF/U2000.pdf)。

等等,你说什么?

变量标识符可以包含空白!

U+3164 HANGUL FILLER 字符显示为占据空间的空白字符。如果渲染器不支持,则会渲染成完全不可见(也不会占据任何空间,即“零宽度”)。这就是说,永远不会看到丑陋的字符替代符号。

我不知道为什么U+3164被设计成这种行为。有意思的是,U+3164是在Unicode 1.1版本(1993年)加入的,所以联盟一定是花了很多时间思考它。下面是几个例子:

> 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'

注意:我在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的作用是它能保证输出的两个字符尽可能靠近,或者连接一个词及其语素。

> 'a'
"a"

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

> '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

大写变换冲突

小写变换冲突

奇怪现象和排查方法

  • 字符串长度通常由统计代码点的个数来计算。这就是说,代理对会被统计成两个字符。多个声调符号会叠放在同一个字符上,例如a + ?==?a,从而增加长度,但它们只会产生一个字符。

  • 类似地,字符串翻转通常非常困难。同样,代理对和带有声调符号的字符必须作为整体进行翻转。ES Reverser提供了一个非常好的解决方法。

  • 字符映射是一对一的?错。映射关系也可能是:

  • 一对多:(? → SS )

  • 上下文相关:(…Σ ? …? 和 …ΣΤ… ? …στ… )

  • 语言相关:( I ? ? 和 ? ? i )

一对多映射

绝大多数字符,在大写的时候表示一对多关系;另一些字符小写的时候表示一对多关系。

优秀的软件包和库

  • PhantomScript::ghost: :flashlight: 不可见的JavaScript代码执行和社会工程工具。

  • ESReverser:用JavaScript编写的支持Unicode的字符串翻转。

  • mimic:Unicode的恶作剧。

  • python-ftfy:输入Unicode文本,输出更一致、更不容易出现显示错误的表现形式。

  • vim-troll-stopper:防止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 8.0版(2015年中期)中发布的人类的表情符号提供一系列的肤色。这些字符基于Fitzpatrick度量(皮肤学上的著名标准,网上也有许多例子,比如FitzpatrickSkinType.pdf)定义的六种肤色。不同的实现的精确颜色可能不同。”

——Unicode联盟的多样性报告

只需要在所需的表情符号后面接上肤色修饰字符 \u{1F466}\u{1F3FE} 即可。

有创意的变量名和方法名

示例采用JavaScript(ES6)编写。

一般而言,带有ID_START属性的字符可以用在变量名开头,而带有ID_CONTINUE属性的字符可以用在变量名中除了首字符之外的其他位置。

function rand(μ,σ){ ... };

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

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

var WhatDoesThisDo????=42

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

// 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';

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

<!-- place this within the document head -->
<meta charset="UTF-8" />

<!-- error message -->
<div class="?_?">You do not have access to this page.</div>

<!-- success message -->
<div class="?">Your changes have been saved successfully!</div>
.?_? {
border: 1px solid #f00;
}

.? {
background: lightgreen;
}

递归的HTML标签重命名脚本

如果你想把所有HTML标签重命名,使之看上去像什么都没有,那么可以使用以下的脚本。

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

// 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
// <?-- name="viewport" content="width=device-width"></?-->
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;a++){
newNode.setAttribute(elem.attributes[a].nodeName, elem.attributes[a].value);
}
for(var i=0,clen=children.length;i<clen;i++){
newNode.appendChild(children[0]); //0...always point to the first non-moved element
}
newNode.style.cssText=elem.style.cssText;
parent.replaceChild(newNode,elem);
}

下面是确定能够支持的字符:

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; }
}

下面是一些基本的结果:

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

> testContinue('-')
< true

> testBegin('?-') // Prepend dash with U+1160 HANGUL JUNGSEONG FILLER
< true

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标准设定了下述基本原则:

  • 通用原则——曾经出现过的一切书写系统都应该在标准中体现。

  • 逻辑顺序——在双向文本中,字符以逻辑顺序存储,而不是表现顺序存储。

  • 效率——文档必须是高效的、完整的。

  • 统一——不同文化或语言使用同一个字符时,应该仅存储一次。

  • 记录字符而不是字形——应当对字符进行编码,而不是字形。字形就是字符的实际图形表示。

  • 动态组合——新的字符可以与已有的标准化后的字符进行组合。例如,字符“?”可以用字符“A”和分音符“¨”组合而成。

  • 语义——包含的字符必须有明确定义,必须与其他字符有明确的区别。

  • 稳定——字符一旦被定义,就永远不能被移除,其代码点也不能被挪作他用。如果出错,则应该将代码点标记为弃用。

  • 纯文本——标准中的字符应当是纯文本,永远不应该包含标记或元字符。

  • 可转换——每一种编码都应该可以用Unicode编码表示。

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

本文为 CSDN 翻译,转载请注明来源出处。

【End】

Python系列学习成长课来了!15年经验专家、CSDN特级讲师亲自授课,还等什么?立即扫码报名学习:

nicode 联盟(Unicode Consortium)

Unicode 联盟(Unicode Consortium)开发了 Unicode 标准(Unicode Standard)。他们的目标是使用标准的 Unicode 转换格式(即 UTF,全称 Unicode Transformation Format)取代现有的字符集。

Unicode 标准是一个成功的创举,在 HTML、XML、Java、JavaScript、E-mail、ASP、PHP 中都得到实现。Unicode 标准也得到许多操作系统和所有现代浏览器的支持。

Unicode 联盟与领先的标准开发组织合作,这些组织有 ISO、W3C 和 ECMA。


Unicode 字符集

Unicode 可以由不同的字符集实现。最常用的编码是 UTF-8 和 UTF-16:

字符集描述
UTF-8UTF8 中的字符可以是 1 到 4 字节长。UTF-8 可以代表 Unicode 标准中的任何字符。UTF-8 向后兼容 ASCII。UTF-8 是电子邮件和网页的首选编码。
UTF-1616 位 Unicode 转换格式是一种可变长度的 Unicode 字符编码,能够编码整个 Unicode 指令表。UTF-16 主要用于操作系统和环境,如 Microsoft Windows、Java 和 .NET。

提示:Unicode 的前 128 个字符(与 ASCII 一一对应)使用一个与 ASCII二进制值相同的八位组进行编码,使有效的 ASCII 文本在进行 UTF-8 编码时也是有效的。

提示:所有的 HTML 4 处理器支持 UTF-8,所有的 HTML 5 和 XML 处理器支持 UTF-8 和 UTF-16!


HTML5 标准:Unicode UTF-8

因为 ISO-8859 中字符集大小是有限的,且在多语言环境中不兼容,所以 Unicode 联盟开发了 Unicode 标准。

Unicode 标准覆盖了(几乎)所有的字符、标点符号和符号。

Unicode 使文本的处理、存储和运输,独立于平台和语言。

HTML-5 中默认的字符编码是 UTF-8。

下面列出了一些 HTML5 支持的 UTF-8 字符集:

字符集十进制十六进制
C0 控制与基本的 Latin(C0 Controls and Basic Latin)0-1270000-007F
C1 控制与 Latin-1 的补充(C1 Controls and Latin-1 Supplement)128-2550080-00FF
Latin 扩展 A(Latin Extended-A)256-3830100-017F
Latin 扩展 B(Latin Extended-B)384-5910180-024F

如果 HTML5 网页使用不同于 UTF-8 的字符,则需要在 <meta> 标签中指定,如下:

实例

<meta charset="ISO-8859-1">

如您还有不明白的可以在下面与我留言或是与我探讨QQ群308855039,我们一起飞!

周的时候,朋友圈的直升飞机不知道为什么就火了,很多朋友开着各种花式飞机带着起飞。

图片来自网络

还没来得及了解咋回事来着,这个直升飞机就到的微博热搜。

图片来自网络

后面越来越多人开来他们的直升飞机,盘旋在朋友圈上方。于是很多朋友开来他们的坦克,专打直升飞机,一轰一个准。

图片来自网络

好了,说回正题!

程序员朋友应该都很熟悉 Unicode (万国码),它几乎包含世界上所有符号,比如组成直升飞机这几个特殊符号对应的 Unicode 码分别为:

ps:推荐一个网站,可以根据符号搜对应的 Unicode 码:https://unicode.yunser.com/unicode

除了这些正常字符以外,Unicode 还包含着各种各样的奇葩字符。

奇葩字符

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

这咋读?某少?

世代?

恩?超出认知范围

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

例如下面一整套麻将牌:

一整套的扑克牌:

一整套国际象棋:

image-20200725215319183

除了这些,通过组合符合,我们还可以造出各种各样的颜文字(??????)??、

另外 Unicode 还收录着我们常用的 Emoji

除了这些之外,Unicode 中还有一些特殊字符的,利用这些字符,我们还可以玩出很多有趣的骚操作。

组合字符

Unicode 有一类字符称为组合字符,它可以附加在前一个非组合字符上,从而使整体看起来像是一个字符。

组合字符原来目的是为了解决一些地区语言、文字特殊的需要,比如说泰文声调符号与母音符号。

正常使用的情况下,这些组合字符数量都会有一些限制。但是在 Unicode 组合字符设计上,并没有加这种限制,这样使我们可以无限加这类组合字符。

利用这个特性,可以达到一些恶搞效果,比如「击穿天花板」与「凿穿地板」的效果。

上面实现原理其是利用以下两个组合字符:

上翻字符

下翻字符

只要复制这两个字符相应的 HTML 代码,跟在正常的字符后面,就可以使这两个字符附加在普通字符上,比如下面实现效果为

黑??

Unicode 码值通常使用 U+N(16 进制N 代表码值),比如 A 的码值为 U+0041。

在 HTML 中 Unicode 可以使用 &#N;(十进制,N 代表码值)表示

在 JS 中 Unicode 中需要使用] \uN(16 进制N 代表码值)表示

只要我们在普通字符多复制几个这类附加字符,就可以形成上述「击穿」效果。

还记得上面说的泰文吗,曾经有一段时间贴吧,很流行一种喷射文,比如下面的效果。

向左喷

向右喷

左右互喷

这种喷射文实际原理就是利用泰文中声调符号附加在其他正常符号上。

不过现在这个效果貌似已经没办法再复现了,现在我们只能看到这样的效果:

在一些老版本的系统/浏览器可能还能看到这种效果,知道的小伙伴留言区可以告知一下。

字符

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 : 用于在混合文字方向的多种语言文本中,规定排版文字书写方向为右至左

利用零宽字符不不可见的特性,我们也可以玩出一些骚效果。

空白微博

发布微博的时候,如果内容都是空格,将没办法发布。

但是如果我们将零宽字符,比如说「零宽度空格符 U+200B」复制到微博,这样我们就可以发布空白微博。

我们可以利用 Chrome 浏览器的控制台复制零宽字符,操作方式如下:

发布效果如下:

真的没有改 HTML 导致的.jpg

隐形水印

对于一些内部论坛或者说小说网站来说,可以通过零宽字符在帖子或小说内容嵌入隐形水印。

当这些内容被一些爬虫复制到其他网站时,我们就可以通过隐形水印,轻松查找时那位用户泄漏内容。

隐形水印主要原理就是将用户信息比如用户名,通过一定算法转成零宽字符,这样普通用户浏览时完全看不到这个水印。

如果内容被复制到其他网站,隐形谁赢也被复制,只要找到这个水印,将这些零宽字符反转成用户名即可。

下面展示一种转换方法,JS 代码主要参考以下 Github 项目:

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

隐形水印生成方法

第一步我们需要将明文字符串每个字符都转成二进制串。

    // 每个字符转为二进制,用空格分隔
    const textToBinary = username => (
      username
      .split('')
      // charCodeAt 将字符转成相应的 Unicode 码值
      .map(char => char.charCodeAt(0).toString(2))
      .join(' ')
    );

示例如下:

第二步,将二进制串转为零度字符串,转换规则如下:

  • 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;
};

使用加密方法将明文字符串加密之后,加密字符串肉眼是看不到了,但是实际还是存在的。

实际上,如果我们将加密之后字符串复制到 BEJSON 网站,就可以看到字符。

image-20200722083507869

另外你还可以把加密字符串复制到 IDEA 中,可以看到相应的 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('')
);

调用该方法,隐形水印转成二进制串。

第二步,将二进制再转为相应的字符。

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;
};

解密示例如下:

?

短网址

我们常用的短网址,域名后面会跟上一串随机串,从而实现短网址到长网址的映射。比如以下网址:

https://sourl.cn/iLyn9S

然而我们可以利用零宽字符也可以实现短网址的效果,,比如下面这个网站,就可以生成这类短网址。

https://zws.im/

可以看到这个短网址后面看不到任何字符,实际上这后面跟着一串零宽字符。当浏览器访问该短网址时,后端程序只要反解密的后面零宽字符,拿到相应的网址,然后在做跳转就可以到指定的网站。

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

小心零宽字符

日常开发过程中,我们有时需要从一些文件中读取文本内容,然后做相应的处理。

有时候我们可能会碰到一些诡异的现象,比如我们之前碰到的例子。

后台程序从 Excel 读取文本内容,然后程序中判断是读取的文本内容是否与指定的字符串相等。

然后当我们读取一份 Excel 内容后,返现这段比较逻辑怎么也通过不了。本来以为是 Excel 内容存在空格什么的,但是打开 Excel 仔细一看,跟指定字符串一模一样,并没有什么其他字符。

第一次碰到这种例子,没有什么经验,真的排查了很久,到最后都有点怀疑人生了。最后无意间将文本内容复制到了 IDEA 中,才发现整理混杂着零宽字符!

如果各位小伙伴也碰到这类问题,不妨将复制文本内容,然后到 IDEA 中查看是否存在某些看不见字符~

最后(点个赞呗!)

这两个星期一直很忙,一直都在 9106 的节奏,真的是累,所以断更了一周!

所幸最近项目提测,稍微轻松了一点,能有点划水时间来写写文章。不过再提起笔来写文章,就有点断节奏了!

这篇文章墨迹了很久才水出来,下周开始再次恢复周更的节奏,再忙再累,每周都来一篇。

欢迎各位小伙伴,每周来这里蹲我,Gank 我!!!

好了,我是楼下小黑哥,下周见!!!

参考链接

  1. https://juejin.im/post/5d3f01e7f265da03c23ead69
  2. http://zero.rovelast.com/
  3. https://zws.im/
  4. https://imweb.io/topic/5a08a5c7ef79bc941c30d8dd