unicode 控制字符

unicode 官网:https://home.unicode.org/
unicode 各种类型详细介绍:https://www.compart.com/en/unicode/category/Cf
对零宽度字符完全没有头绪的可以先玩下这个Demo
在綫解密:https://330k.github.io/misc_tools/unicode_steganography.html

1 前言

在所有主要的Web浏览器中内存中的字符顺序(逻辑)与它们显示的顺序(可视)是不同的。

Unicode 定义了它其中每个字符的方向属性,浏览器应用的一组规则(通过这个来进行自动判断文本Unicode方向属性应该使用哪种方向)在显示时产生正确的顺序由Unicode双向算法进行描述,也可简称为BIDI算法。

控制字符,有时候也称非打印字符,是出现在特定的信息文本中,表示某一控制功能的字符。这类字符并不显示,只包含某种特定的功能。

2 Unicode方向属性

Unicode方向属性包含以下三种类型:

  • 强字符:大部分的字符都属于强字符,比如英文字母、汉字和阿拉伯字母。它们的方向性是确定的(英文字母是从左到右,而阿拉伯字母则从右到左),和其上下文的bidi属性无关。并且,强字符在bidi算法中可能会影响其前后字符的方向性;
  • 弱字符:数字和数字相关的符号属于弱字符。它们的方向性是确定的,但对其前后字符的方向性不会产生影响;
  • 中性字符:大部分的标点符号和空格属于中性字符。它们的方向性是不确定的,由上下文的bidi属性来决定其方向。
方向性 相关字符 效果
Left-to-Right (LTR) 强字符从左至右(英文字母、汉字以及世界上大部分从左到右书写的文字) 方向性确定,LTR,和上下文无关。并且可能会影响其前后字符的方向性。
Right-to-Left (RTL) 强字符从右至左(阿拉伯文字、波斯语以及大部分从右到左书写的文字) 方向性确定,RTL,和上下文无关。并且可能会影响其前后字符的方向性。
Left-to-Right (LTR) / Right-to-Left (RTL) 弱字符(数字和数字相关的符号) 和强字符一样方向性也是确定的,但是不会影响前后字符的方向性,也不会受强方向性影响。
Neutral 中性字符(大部分标点符号和空格) 方向性不确定,由上下文环境决定其方向。

2.1 全局方向(也叫基础方向)

全局方向是一个区域中的总体方向,例如一个页面、一个段落或一个句子。中英文环境一般是 (LTR) 从左至右,而阿拉伯文环境则为 (RTL) 右至左的书写顺序。我们可以通过dir属性或者direction样式,设置指定方向。

2.2 方向串

目前我们还没说到文章开始提到的 LRO 和 PDF 控制字符,下面我们先把这两个控制字符从号码中去掉,仅将 “(415)555-3695” 套用到阿拉伯文和中英文环境,观察会出现哪些问题:

1
2
3
4
5
6
<p>phone:(415)555-3695</p>
<!--书写的时候是 阿拉伯文 + (415)555-3695,但是展示的时候就变成了下面这个样子,有没有体会到神奇之处 -->
<p>هاتف:(415)555-3695</p>
<!-- 下面加上 ltr和rtl 标签试试-->
<p dir="ltr">هاتف:(415)555-3695</p>
<p dir="rtl">هاتف:(415)555-3695</p>

可以看到在中英文环境中,文本、数字和标点符号都按照从左至右的顺序书写,展示正常。

但在阿拉伯文环境中,电话号码好像按符号分割分组并方向展示了,实际上不是故意这样输入的,而是输入 阿拉伯文字+电话号码 之后自动就变成这样了。这是怎么回事?

这里要引入 方向串 (Directional Run) 的概念,是指在一段文字中具有相同方向性的连续字符,并且其前后没有相同方向性的其它方向串。

全局方向、文本中的字符强弱类型 决定了如何分割方向串,以上面的例子做分析:

文中首个强类型字符是阿拉伯文字,因此其所在的文本区域的全局方向为RTL,但弱类型的数字则保持了原方向LTR,而中性字符”-“没有被强字符包围则跟随了全局方向。

对于以上含有阿拉伯文字的段落,我们可以得到6个不同的方向串。正是因为中性符号被全局方向影响,使得原本的号码被拆分成不同的方向串,从而重新排列。

3 Unicode控制字符

为了解决上面的问题你,Unicode标准中定义了一系列方向性控制字符,这些字符在页面上并不显示,也不占用展示空间。他们像是一些标记,影响着BIDI算法对文字书写方向的判断。下面介绍一些常用的控制字符。

3.1 隐式控制字符

名称 方向 Unicode Code HTML Code
Left-To-Right Mark (LRM) 左->右 U+200E &lrm、&#x200e、&#8206
Right-To-Left Mark (RLM) 右->左 U+200F &rlm、&#x200f、&#8207

隐式控制字符的概念比较简单,可以理解为一个不会展示出来的强字符,LRM 为从左到右的强字符,而 RLM 为从右到左的强字符。

那我们如何利用隐式控制解决上面号码的问题?两种解决方案:

  • 把影响全局方向的强字符左右用 LTR 包裹起来,强制改变他们的方向
  • 把每个中性字符 -、() 左右用 LTR 包裹起来,中性字符被左至右的强字符包裹,它的方向也应该会变为从左至右。
  • 还有一种是使用显示控制字符,这里也先写一下,后面会详细介绍。使用 LRO 可以修改强字符方向。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--把影响全局方向的强字符左右用  LTR 包裹起来,强制改变他们的方向-->
<div>
&lrm;هاتف:(&lrm;415)555-3695
</div>

<!--把每个中性字符 `-、()` 左右用 LTR 包裹起来,中性字符被左至右的强字符包裹,它的方向也应该会变为从左至右。-->
<div>
هاتف:&lrm;(&lrm;415&lrm;)&lrm;555&lrm;-&lrm;3695
</div>

<!--使用显式控制字符-->
<bdo dir='ltr'>
هاتف:(415)555-3695
</bdo>

再举个例子:

1
2
3
4
5
<!--符号`@`没有被强字符包围因此使用全局方向RTL,整个文本被分为三个方向串,所以文本被重新排序。-->
<p dir="rtl">123456@qq.com</p>

<!--使用LRM强字符包围符号`@`,强制改变其方向为从左到右,此时文本只有一个LTR的方向串。即使手动设置了全局方向dir,也无法覆盖被强字符影响的中性字符。-->
<p dir="rtl">123456&lrm;@&lrm;qq.com</p>

但写这么多未免繁琐,所以 iOS 实现相同效果只用了 LRO 和 PDF 两个字符,这两个字符又有什么作用呢?

3.2 显式控制字符

显式控制字符需要成对使用,前四个字符 LER RLE LRO RLO 为开始字符,最后一个 PDF 为结束字符。

  • LRE & RLE : 接下来的文字片段内的方向变为 从左至右 / 从右至左。效果类似基础方向,将一段文本中的基础方向变更。
  • LRO & RLO : 顾名思义 override,接下来的所有 Unicode 字符的方向性将被覆盖为 从左至右强字符 / 从右至左强字符。

3.2.1 Embedding

名称 方向 Unicode Code HTML Code
Left-To-Right Embedding (LRE) 左->右 U+202A &#x202a, dir='ltr'
Right-To-Left Embedding (RLE) 右->左 U+202B &#x202b, dir='rtl'
Pop Directional Formatting (PDF) 结束标记 U+202C &#x202C

使用LRE或者RLE,可以改变控制字符后的文本区域的全局方向。但不影响强字符和弱字符的方向。

效果和 dir属性或者direction样式 相同。

3.2.2 Override

名称 方向 Unicode Code HTML Code
Left-To-Right Override (LRO) 左->右 U+202D &#x202d, <bdo dir='ltr'>..</bdo>
Right-To-Left Override (RLO) 右->左 U+202E &#x202e, <bdo dir='rtl'>..</bdo>

LRO或者RLO会强制将控制符后的字符的方向属性覆盖为对应的方向,各种字符都会被影响,包括强字符。

结束标记符使用的也是PDF,在此处PDF 表示为</bdo>

3.2.3 Isolate

名称 方向 Unicode Code HTML Code
Left-To-Right Isolate (LRI) 左->右 U+2066 &#x2066, <bdi dir='ltr'>
Right-To-Left Isolate (RLI) 右->左 U+2067 &#x2067, <bdi dir='rtl'>
First Strong Isolate (FSI) 自适应 U+2068 &#x2068, <bdi dir='auto'>
Pop Directional Isolate (PDI) 结束标记 U+2069 &#x2069, </bdi>

Isolate控制符用来在双向文字中加入脱离与其父元素的全局方向的方向串,可以使用<bdi>标签实现。<bdi>标签有点类似与<span>标签的作用,但不同的是<bdi>标签本身带有默认方向属性,dir默认值为auto。

虽然控制符和相应的HTML元素能够达到相同的控制效果,但需要注意的是,有些浏览器现阶段并不支持<bdi>等新标签,比如IE。

4 相关的CSS属性

除了上文提到的控制文本方向,我们还可以使用css中unicode-bididirection属性决定文字渲染方向、书写方向。

具体使用方法可以参考以下教程:

direction: CSS direction 属性
unicode-bidi: CSS unicode-bidi 属性

5 iOS 对通讯录号码的处理

在从右至左的书写环境,虽然作为弱字符的数字还是按照从左至右的顺序书写,但是包含中性字符标点符号的电话号码,因为受到基础方向的影响,导致算法在不同环境下生成了不同的方向串,最终展示出错。

苹果为了避免这种错误产生,使用 LRO 和 PDF 控制字符包裹号码部分,使得其中的字符始终为强字符从左至右。

所以iOS 通讯录中复制电话号码都出的两个字符,并不是什么 bug,而是有意为之的,是为了避免不同语言环境下,电话号码的展示不一致。

6 Python 代码

在Python中,可以使用unicodedata模块来判断一个字符串是否包含RLO或LRO字符,并将其删除或替换为其他字符

1
2
3
4
5
6
7
8
9
10
11
import unicodedata
def remove_RLO_LRO(s):
# 判断字符串是否包含RLO或LRO字符
has_RLO = any(unicodedata.bidirectional(c) == 'R' for c in s)
has_LRO = any(unicodedata.bidirectional(c) == 'L' for c in s)
if has_RLO or has_LRO:
# 将RLO和LRO替换为空格
s = s.replace('\u202b', '').replace('\u202e', '')
return s.strip()

return s

7 结语

虽然前面介绍了几种改变文本方向的方法,但实际情况会复杂得多。大家可根据实际情况使用。


unicode 控制字符
https://flepeng.github.io/字符集相关-unicode-控制字符/
作者
Lepeng
发布于
2021年3月8日
许可协议