为什么表情符号在Swift字符串中被如此奇怪地处理?

时间:2017-04-25 18:36:29

标签: swift string unicode emoji

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

U+1F469 WOMAN
‍U+200D ZWJ
U+1F469 WOMAN
U+200D ZWJ
U+1F467 GIRL
U+200D ZWJ
U+1F466 BOY

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

"‍‍‍".contains("‍‍‍") // true
"‍‍‍".contains("") // false
"‍‍‍".contains("\u{200D}") // false
"‍‍‍".contains("") // false
"‍‍‍".contains("") // true

所以,斯威夫特说它包含自己(好)和一个男孩(好!)。但它说它不包含女人,女孩或零宽度木匠。 这里发生了什么?为什么Swift知道它包含一个男孩而不是一个女人或女孩?我能理解它是否将它视为单个角色并且只识别它包含自己,但事实上它有一个子组件而没有其他人困惑我

如果我使用类似"".characters.first!的内容,则不会更改。

更令人困惑的是:

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

即使我将ZWJ放在那里,它们也不会反映在字符数组中。接下来是一点点说明:

manual.contains("") // false
manual.contains("") // false
manual.contains("") // true

所以我得到了与字符数组相同的行为......这非常令人讨厌,因为我知道数组的样子。

如果我使用"".characters.first!之类的内容,这也不会改变。

6 个答案:

答案 0 :(得分:389)

这与String类型在Swift中的工作方式以及contains(_:)方法的工作原理有关。

''就是所谓的表情符号序列,它被渲染为字符串中的一个可见字符。序列由Character个对象组成,同时由UnicodeScalar个对象组成。

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

print("‍‍‍".characters.count)     // 4
print("‍‍‍".unicodeScalars.count) // 7

现在,如果您解析字符并打印它们,您将看到看似普通字符的内容,但事实上,前三个字符在{{1}中包含表情符号和零宽度连接符}:

UnicodeScalarView

如您所见,只有最后一个字符不包含零宽度连接符,因此在使用for char in "‍‍‍".characters { print(char) let scalars = String(char).unicodeScalars.map({ String($0.value, radix: 16) }) print(scalars) } // ‍ // ["1f469", "200d"] // ‍ // ["1f469", "200d"] // ‍ // ["1f467", "200d"] // // ["1f466"] 方法时,它可以按预期工作。由于您没有与包含零宽度连接符的表情符号进行比较,因此该方法不会找到除最后一个字符之外的任何匹配项。

为了对此进行扩展,如果您创建一个contains(_:),它由一个以零宽度连接符结尾的表情符号字符组成,并将其传递给String方法,它也会计算为{ {1}}。这与contains(_:)false完全相同,后者试图找到与给定参数的精确匹配。由于以零宽度连接符结尾的字符形成不完整的序列,因此该方法尝试查找参数的匹配,同时将以零宽度连接符结尾的字符组合成完整的序列。这意味着如果出现以下情况,该方法将无法找到匹配项:

  1. 参数以零宽度连接符结束,
  2. 要解析的字符串不包含不完整的序列(即以零宽度的连接符结尾,后面没有兼容的字符)。
  3. 演示:

    contains(_:)

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

    range(of:) != nil

    最简单的解决方案是为range(of:options:range:locale:)方法提供特定的比较选项。选项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 完全逐个字符的等效性执行比较。作为旁注,这里的字符含义是 Swift 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 ,但是实例和比较字符串的UTF-16表示 - 但是,因为String.CompareOptions.literal没有不允许格式错误的UTF-16,这基本上等同于比较Unicode标量表示。

    这里我重载了Character方法,所以如果您需要原始方法,请重命名此方法:

    String

    现在,即使序列不完整,该方法仍然适用于每个字符:

    Foundation

答案 1 :(得分:106)

第一个问题是你使用contains(Swift' String不是Collection)与基金会建立联系,所以这是NSString行为,我不相信处理组成的表情符号像斯威夫特一样强大。也就是说,Swift我认为现在正在实施Unicode 8,这也需要在Unicode 10中围绕这种情况进行修改(因此,当它们实现Unicode 10时,这可能都会改变;我还没有考虑是否会这样做)。

为简化起见,让我们摆脱Foundation,并使用Swift,它提供更明确的视图。我们从字符开始:

"‍‍‍".characters.forEach { print($0) }
‍
‍
‍

行。这就是我们的预期。但这是个骗局。让我们看看这些角色究竟是什么。

"‍‍‍".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标量。这完全符合您的预期:

"‍‍‍".unicodeScalars.contains("") // true
"‍‍‍".unicodeScalars.contains("\u{200D}") // true
"‍‍‍".unicodeScalars.contains("") // true
"‍‍‍".unicodeScalars.contains("") // true

当然,我们也可以寻找那里的实际角色:

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

(这大大复制了Ben Leggiero的观点。我发现这一点之前注意到他已经回答了。如果对任何人都更清楚,请留下。)

答案 2 :(得分:74)

似乎Swift认为printf("hello"); fflush(stdout); 是一个扩展的字形集群,其前面有一个字符。将字符数组映射到ZWJ

时,我们可以看到这一点
unicodeScalars

这将从LLDB打印以下内容:

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

此外,▿ 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}" 组将字形集群扩展为单个字符。例如,使用hangul字符.contains(组合使韩语单词为" one":):

한

找不到"\u{1112}\u{1161}\u{11AB}".contains("\u{1112}") // false ,因为这三个代码点被分组为一个作为一个字符的集群。同样,\u{1F469}\u{200D} WOMAN)是一个群集,作为一个角色。

答案 3 :(得分:18)

其他答案讨论了Swift的作用,但没有详细说明原因。

你期望“Å”等于“Å”吗?我希望你能。

其中一个是带有组合器的字母,另一个是单个组合字符。您可以向基本角色添加许多不同的组合器,而人类仍然会将其视为单个角色。为了解决这种差异,创建了一个字母的概念来表示人类不管使用的代码点将会考虑什么角色。

现在,短信服务已将字符组合成图形表情符号多年:)。因此,Unicode中添加了各种表情符号 这些服务也开始将表情符号组合成复合表情符号 当然没有合理的方法将所有可能的组合编码到单个代码点中,因此Unicode Consortium决定扩展字形的概念以包含这些复合字符。

如果您尝试在字形级别使用它,那么"‍‍‍"应该被视为单个“字形集群”,正如Swift默认执行的那样。

如果你想检查它是否包含"",那么你应该降到更低的水平。

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

say "\c[family: woman woman girl boy]" eq "‍‍‍"; # True

# .contains is a Str method only, in Perl 6
say "‍‍‍".contains("‍‍‍")    # True
say "‍‍‍".contains("");        # False
say "‍‍‍".contains("\x[200D]");  # False

# comb with no arguments splits a Str into graphemes
my @graphemes = "‍‍‍".comb;
say @graphemes.elems;                # 1

让我们下一个级别

# 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

降低到这个水平可能会让事情变得更难。

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

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

在此级别工作可以更容易地将字符串意外地拆分到复合字符的中间。例如。

你无意中问的是为什么这种更高级别的表示不像低级表示那样工作。答案当然是,不应该这样。

如果你问自己“为什么这必须如此复杂”,答案当然是“ human ”。

答案 4 :(得分:18)

Swift 4.0更新

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

是两个表情符号的组合,

‍‍‍是四个表情符号的组合,连接零宽度的连接符。格式为‍joiner‍joiner‍joiner

<强> 1。计数

在swift 4.0中。表情符号被计为字形簇。每个表情符号都计为1. count属性也可直接用于字符串。所以你可以直接这样称呼它。

"".count  // 1. Not available on swift 3
"‍‍‍".count // 1. Not available on swift 3

字符串的字符数组也被计为swift 4.0中的字形簇,因此以下两个代码都打印1.这两个表情符号是表情符号序列的示例,其中几个表情符号组合在一起,有或没有零宽度连接符{{ 1}}他们之间。在swift 3.0中,这种字符串的字符数组分隔出每个表情符号,并产生一个包含多个元素(表情符号)的数组。在此过程中忽略了连接器。但是在swift 4.0中,字符数组将所有表情符号视为一个整体。因此任何表情符号都将是1。

\u{200d}

"".characters.count // 1. In swift 3, this prints 2 "‍‍‍".characters.count // 1. In swift 3, this prints 4 在swift 4中保持不变。它在给定的字符串中提供唯一的Unicode字符。

unicodeScalars

<强> 2。包含

在swift 4.0中,"".unicodeScalars.count // 2. Combination of two emoji "‍‍‍".unicodeScalars.count // 7. Combination of four emoji with joiner between them 方法忽略了表情符号中的零宽度连接符。因此,对于contains的四个表情符号组件中的任何一个,它都返回true,如果检查加入者,则返回false。但是,在swift 3.0中,连接器不会被忽略,并与前面的表情符号结合使用。因此,当您检查"‍‍‍"是否包含前三个组件表情符号时,结果将为false

"‍‍‍"

答案 5 :(得分:0)

表情符号很像unicode标准,看似复杂。肤色,性别,工作,人群,零宽度连接符序列,标志(2个字符的unicode)和其他复杂性会使表情符号解析变得混乱。一棵圣诞树,一片披萨或一堆便便都可以用一个Unicode代码点表示。更不用说引入新表情符号时,iOS支持和表情符号发布之间会有延迟。事实是,不同版本的iOS支持不同版本的unicode标准。

TL; DR。我已经研究了这些功能,并开放了一个我JKEmoji的作者的库,以帮助解析带有表情符号的字符串。它使解析变得简单:

print("I love these emojis ‍‍‍".emojiCount)
  

5

通过常规刷新最新的unicode版本(最近为12.0)的所有已识别表情符号的本地数据库,并将它们与正在运行的OS版本中被识别为有效表情符号的内容进行交叉引用通过查看无法识别的表情符号字符的位图表示形式。

注意

删除了先前的答案,原因是我没有在广告中明确指出我是作者,而是在宣传我的图书馆。我再次确认这一点。