为什么表情符号字符像???????????

Why are emoji characters like ??????????? treated so strangely in Swift strings?

角色????????????????(有两个女人、一个女孩和一个男孩的家庭)编码如下:

U+1F469WOMAN?U+200DZWJU+1F469WOMANU+200DZWJU+1F467GIRLU+200DZWJ,江户十一〔12〕江户十一〔13〕号

所以它是非常有趣的编码;是单元测试的完美目标。然而,斯威夫特似乎不知道如何治疗它。我的意思是:

1
2
3
4
5
"???????????".contains("???????????") // true
"???????????".contains("??") // false
"???????????".contains("\u{200D}") // false
"???????????".contains("??") // false
"???????????".contains("??") // true

所以,斯威夫特说它包含自己(好)和一个男孩(好!)但是它说它不包含一个女人、女孩或零宽度的木匠。这里发生了什么?为什么斯威夫特知道里面有一个男孩而不是一个女人或女孩?我可以理解,如果它把它当作一个单独的角色,只认识到它包含自己,但是它有一个子组件,没有其他的,这一事实让我困惑。

如果我使用像"??".characters.first!这样的东西,这不会改变。

更令人困惑的是:

1
2
let manual ="\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}"
Array(manual.characters) // ["???","???","???","??"]

即使我将zwjs放在其中,它们也不会反映在字符数组中。接下来是一个小问题:

1
2
3
manual.contains("??") // false
manual.contains("??") // false
manual.contains("??") // true

所以我得到了与字符数组相同的行为…这是非常恼人的,因为我知道数组是什么样子的。

如果我使用像"??".characters.first!这样的东西,这也不会改变。


这与String类型如何在swift中工作以及contains(_:)方法如何工作有关。

"?"??????????????是一个emoji序列,它在字符串中呈现为一个可见字符。序列由Character个物体组成,同时又由UnicodeScalar个物体组成。

如果检查字符串的字符数,您将看到它由四个字符组成,而如果检查Unicode标量数,它将显示不同的结果:

1
2
print("???????????".characters.count)     // 4
print("???????????".unicodeScalars.count) // 7

现在,如果您解析并打印字符,您将看到看起来像普通字符的内容,但事实上,前三个字符在它们的UnicodeScalarView中既包含一个emoji,也包含一个零宽度连接符:

4

如您所见,只有最后一个字符不包含零宽度连接符,因此在使用contains(_:)方法时,它的工作方式与您预期的一样。由于您没有与包含零宽度连接符的emoji进行比较,因此该方法只找到最后一个字符的匹配项。

在此基础上,如果创建一个由以零宽度连接符结尾的emoji字符组成的String,并将其传递给contains(_:)方法,它也将对false进行评估。这与contains(_:)range(of:) != nil完全相同,后者试图找到与给定论点完全匹配的结果有关。由于以零宽度连接符结尾的字符构成不完整的序列,因此该方法尝试在将以零宽度连接符结尾的字符组合为完整序列时查找参数的匹配项。这意味着如果出现以下情况,该方法将永远找不到匹配项:

  • 参数以零宽度连接符结尾,并且
  • 要分析的字符串不包含不完整的序列(即以零宽度连接符结尾,后面不跟兼容字符)。
  • 证明:

    1
    2
    3
    4
    let s ="\u{1f469}\u{200d}\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}" // ???????????

    s.range(of:"\u{1f469}\u{200d}") != nil                            // false
    s.range(of:"\u{1f469}\u{200d}\u{1f469}") != nil                   // false

    但是,由于比较只是向前看,您可以通过向后操作在字符串中找到其他几个完整的序列:

    1
    2
    3
    4
    5
    6
    s.range(of:"\u{1f466}") != nil                                    // true
    s.range(of:"\u{1f467}\u{200d}\u{1f466}") != nil                   // true
    s.range(of:"\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}") != nil  // true

    // Same as the above:
    s.contains("\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}")          // true

    最简单的解决方案是为range(of:options:range:locale:)方法提供一个特定的比较选项。选项String.CompareOptions.literal对一个确切的字符进行逐字等价比较。作为补充说明,这里所指的字符不是Swift Character,而是实例和比较字符串的utf-16表示——但是,由于String不允许格式不正确的utf-16,这本质上等同于比较unicode标量表示。

    这里我已经重载了Foundation方法,因此如果您需要原始方法,请重命名此方法或其他方法:

    1
    2
    3
    4
    5
    extension String {
        func contains(_ string: String) -> Bool {
            return self.range(of: string, options: String.CompareOptions.literal) != nil
        }
    }

    现在,该方法对每个字符都"应该"工作,即使序列不完整:

    1
    2
    3
    s.contains("??")          // true
    s.contains("??\u{200d}")  // true
    s.contains("\u{200d}")    // true


    第一个问题是你用EDOCX1的0桥接桥接(SWIFT的EDOCX1 1)不是EDCOX1×2),所以这是EDCOX1×3的行为,我不相信ReaveStimes像SWIFT那样强有力地组成了EMOJI。也就是说,我认为Swift现在正在实现Unicode8,它还需要在Unicode10中对这种情况进行修改(因此,当它们实现Unicode10时,这一切都可能发生变化;我还没有深入研究它是否会发生变化)。

    为了简化事情,让我们去掉基础,使用SWIFT,它提供了更为明确的视图。我们将从字符开始:

    1
    2
    3
    4
    5
    "???????????".characters.forEach { print($0) }
    ???
    ???
    ???
    ??

    好啊。这正是我们所期望的。但这是个谎言。让我们看看那些角色到底是什么。

    1
    2
    3
    4
    5
    "???????????".characters.forEach { print(String($0).unicodeScalars.map{$0}) }
    ["\u{0001F469}","\u{200D}"]
    ["\u{0001F469}","\u{200D}"]
    ["\u{0001F467}","\u{200D}"]
    ["\u{0001F466}"]

    啊……所以是["??ZWJ","??ZWJ","??ZWJ","??"]。这让一切变得更加清晰了。???不是这个名单的成员吗?ZWJ"",但是?是会员。

    问题是,Character是一个"字形集群",它将事物组合在一起(比如连接zwj)。您真正要搜索的是Unicode标量。这完全符合你的预期:

    1
    2
    3
    4
    "???????????".unicodeScalars.contains("??") // true
    "???????????".unicodeScalars.contains("\u{200D}") // true
    "???????????".unicodeScalars.contains("??") // true
    "???????????".unicodeScalars.contains("??") // true

    当然,我们也可以寻找其中的实际特征:

    1
    "???????????".characters.contains("??\u{200D}") // true

    (这严重重复了本·莱基罗的观点。我在注意到他回答之前把这个贴了出来。离开,以防任何人更清楚。)


    斯威夫特似乎认为一个ZWJ是一个扩展的图形群,其前面有字符。我们可以在将字符数组映射到它们的unicodeScalars时看到这一点:

    1
    Array(manual.characters).map { $0.description.unicodeScalars }

    这将从LLDB打印以下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ? 4 elements
      ? 0 : StringUnicodeScalarView("???")
        - 0 :"\u{0001F469}"
        - 1 :"\u{200D}"
      ? 1 : StringUnicodeScalarView("???")
        - 0 :"\u{0001F469}"
        - 1 :"\u{200D}"
      ? 2 : StringUnicodeScalarView("???")
        - 0 :"\u{0001F467}"
        - 1 :"\u{200D}"
      ? 3 : StringUnicodeScalarView("??")
        - 0 :"\u{0001F466}"

    此外,.contains组将字形簇扩展为单个字符。例如,以朝鲜文字???为例(它们组合成韩文"一"字:???)。

    1
    "\u{1112}\u{1161}\u{11AB}".contains("\u{1112}") // false

    这找不到?,因为这三个代码点被分组成一个集群,作为一个字符。同样,\u{1F469}\u{200D}(WOMANZWJ是一个簇,充当一个字符。


    Swift 4.0更新

    如SE-0163所述,string在swift 4更新中接收大量修订。本演示使用两个emoji表示两种不同的结构。两者都与一系列的表情符号结合在一起。

    ????????两个emoji的组合。

    ???????????是四个emoji的组合,与零宽度连接。格式为???joiner???joiner???joiner??

    1。计数

    在斯威夫特4。emoji被算作笔迹簇。每一个表情符号都算作1。Count属性也可直接用于字符串。所以你可以直接这样称呼它。

    1
    2
    "????".count  // 1. Not available on swift 3
    "???????????".count // 1. Not available on swift 3

    在Swift4.0中,字符串的字符数组也被计算为图形簇,因此以下两个代码都打印1。这两个emoji是emoji序列的例子,其中几个emoji与它们之间的零宽度连接符\u{200d}组合在一起。在Swift 3.0中,这样的字符串的字符数组分离出每个emoji,并生成一个包含多个元素(emoji)的数组。在此过程中,将忽略联接程序。然而在Swift4.0中,字符数组将所有emoji视为一个整体。所以任何表情符号都是1。

    1
    2
    "????".characters.count  // 1. In swift 3, this prints 2
    "???????????".characters.count // 1. In swift 3, this prints 4

    在swift 4中,unicodeScalars保持不变。它提供给定字符串中唯一的Unicode字符。

    1
    2
    "????".unicodeScalars.count  // 2. Combination of two emoji
    "???????????".unicodeScalars.count // 7. Combination of four emoji with joiner between them

    2。包含

    在swift 4.0中,contains方法忽略emoji中的零宽度连接符。因此,对于"???????????"的四个emoji组件中的任何一个,它都返回true;如果检查连接符,则返回false。然而,在Swift 3.0中,Joiner不会被忽略,而是与前面的emoji组合在一起。因此,当您检查"???????????"是否包含前三个组件emoji时,结果将是错误的。

    1
    2
    3
    4
    5
    6
    7
    "????".contains("??")       // true
    "????".contains("??")       // true
    "???????????".contains("???????????")      // true
    "???????????".contains("??")      // true. In swift 3, this prints false
    "???????????".contains("\u{200D}") // false
    "???????????".contains("??")      // true. In swift 3, this prints false
    "???????????".contains("??")      // true

    其他的答案讨论了斯威夫特的所作所为,但不要详细说明原因。

    你期待"A"吗?"等于"?"?我想你会的。

    其中一个是带有合并器的字母,另一个是单个组合字符。您可以为一个基本字符添加许多不同的组合器,而一个人仍然认为它是一个单一的字符。为了处理这种差异,人们创建了一个笔迹的概念来表示一个人会认为一个字符是什么,而不管使用的是什么代码点。

    现在,文本消息服务多年来一直将字符组合成图形符号:)??。所以各种表情符号被添加到Unicode中。这些服务也开始将emoji组合成复合emoji。当然,没有合理的方法将所有可能的组合编码为单独的代码点,因此Unicode联盟决定扩展字形的概念,以包含这些复合字符。

    归根结底,如果您试图在图形级别使用它,那么应该将"???????????"视为单个"图形集群",就像swift默认情况下那样。

    如果您想检查它是否包含"??"作为其中的一部分,那么您应该降低到一个较低的级别。

    我不知道Swift语法,所以这里有一些Perl 6,它对Unicode有类似的支持级别。(Perl6支持Unicode版本9,因此可能存在差异)

    4

    我们往下走一层吧

    1
    2
    3
    4
    5
    6
    7
    # look at it as a list of NFC codepoints
    my @components :="???????????".NFC;
    say @components.elems;                     # 7

    say @components.grep("??".ord).Bool;       # True
    say @components.grep("\x[200D]".ord).Bool; # True
    say @components.grep(0x200D).Bool;         # True

    但是,降低到这个水平会使一些事情变得更困难。

    1
    2
    3
    my @match ="???????????".ords;
    my $l = @match.elems;
    say @components.rotor( $l => 1-$l ).grep(@match).Bool; # True

    我认为Swift中的.contains使这变得更容易,但这并不意味着没有其他事情变得更困难。

    例如,在这个级别上工作可以更容易地意外地在复合字符的中间拆分字符串。

    您不经意间问的是,为什么这个更高级别的表示不能像较低级别的表示那样工作。答案当然是不应该的。

    如果你问自己"为什么这件事必须如此复杂",答案当然是"人类"。


    emojis与unicode标准非常相似,其复杂程度令人难以置信。肤色、性别、工作、人群、零宽度连接序列、标志(2个字符的Unicode)和其他复杂因素会使表情分析变得混乱。一棵圣诞树、一片披萨或一堆粪便都可以用一个Unicode代码点表示。更不用说,当引入新的emoji时,iOS支持和emoji发布之间会有延迟。不同版本的iOS支持不同版本的Unicode标准。

    我已经研究了这些特性,并打开了一个源代码库,我是jkemoji的作者,帮助用emoji解析字符串。它使解析变得简单如下:

    4

    5

    它通过定期刷新最新Unicode版本(最近为12.0)中所有已识别的emoji的本地数据库,并通过查看未识别emoji字符的位图表示,将它们与正在运行的OS版本中已识别的有效emoji交叉引用来实现这一点。

    注释

    之前的一个答案因为在我的图书馆做广告而被删除,没有明确说明我是作者。我再次承认这一点。