为啥表情符号字符像??????????????????在 Swift 字符串中被如此奇怪地对待?
Posted
技术标签:
【中文标题】为啥表情符号字符像??????????????????在 Swift 字符串中被如此奇怪地对待?【英文标题】:Why are emoji characters like ???????????????? treated so strangely in Swift strings?为什么表情符号字符像??????????????????在 Swift 字符串中被如此奇怪地对待? 【发布时间】:2017-09-22 22:18:12 【问题描述】:角色 ???????????????????? (有两个女人,一个女孩和一个男孩的家庭)编码如下:
U+1F469
WOMAN
,U+200D
ZWJ
,U+1F469
WOMAN
,U+200D
ZWJ
,U+1F467
,@9876542 U+200D
ZWJ
,U+1F466
BOY
所以它的编码非常有趣;单元测试的完美目标。然而,斯威夫特似乎并不知道如何对待它。这就是我的意思:
"????????????????".contains("????????????????") // true
"????????????????".contains("????") // false
"????????????????".contains("\u200D") // false
"????????????????".contains("????") // false
"????????????????".contains("????") // true
所以,斯威夫特说它包含自己(好)和一个男孩(好!)。但它接着说它不包含女人、女孩或零宽度细木工。 这里发生了什么?为什么 Swift 知道它包含一个男孩而不是一个女人或女孩? 我可以理解它是否将它视为一个单独的字符并且只识别它包含它自己,但是它只有一个子组件而没有其他子组件的事实让我感到困惑.
如果我使用 "????".characters.first!
之类的内容,这不会改变。
更令人困惑的是:
let manual = "\u1F469\u200D\u1F469\u200D\u1F467\u200D\u1F466"
Array(manual.characters) // ["????", "????", "????", "????"]
即使我将 ZWJ 放在那里,它们也不会反映在字符数组中。接下来的事情有点说明:
manual.contains("????") // false
manual.contains("????") // false
manual.contains("????") // true
所以我对字符数组也有同样的行为......这非常烦人,因为我知道数组是什么样子的。
如果我使用 "????".characters.first!
之类的东西,这也不会改变。
【问题讨论】:
后续问题:Is it possible to write a Swift function that replaces only part of an extended grapheme cluster? 评论不用于扩展讨论;这个对话是moved to chat。 在 Swift 4 中修复。"????????????????".contains("\u200D")
仍然返回 false,不确定这是错误还是功能。
哎呀。 Unicode 破坏了文本。它将纯文本转换为标记语言。
@Boann 是和否...很多这些更改都用于使像 Hangul Jamo(255 个代码点)这样的编码/解码内容不像汉字(13,108 个代码点)那样成为绝对的噩梦和中国表意文字(199,528 个代码点)。当然,它比 SO 评论的长度更复杂和有趣,所以我鼓励你自己检查一下:D
【参考方案1】:
这与String
类型在Swift 中的工作方式以及contains(_:)
方法的工作方式有关。
'????'是所谓的表情符号序列,它被呈现为字符串中的一个可见字符。该序列由Character
对象组成,同时又由UnicodeScalar
对象组成。
如果你检查字符串的字符数,你会看到它由四个字符组成,而如果你检查 unicode 标量数,它会显示不同的结果:
print("????".characters.count) // 4
print("????".unicodeScalars.count) // 7
现在,如果您解析字符并打印它们,您会看到看起来像普通字符的内容,但实际上前三个字符在其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
,并将其传递给contains(_:)
方法,它也将评估为false
。这与contains(_:)
与range(of:) != nil
完全相同,后者试图找到与给定参数的完全匹配。由于以零宽度连接符结尾的字符形成不完整的序列,因此该方法尝试找到参数的匹配项,同时将以零宽度连接符结尾的字符组合成完整序列。这意味着如果出现以下情况,该方法将永远找不到匹配项:
-
参数以零宽度连接符结尾,并且
要解析的字符串不包含不完整的序列(即以零宽度连接符结尾且后跟不兼容字符)。
演示:
let s = "\u1f469\u200d\u1f469\u200d\u1f467\u200d\u1f466" // ????
s.range(of: "\u1f469\u200d") != nil // false
s.range(of: "\u1f469\u200d\u1f469") != nil // false
但是,由于比较只向前看,您可以通过向后工作在字符串中找到其他几个完整的序列:
s.range(of: "\u1f466") != nil // true
s.range(of: "\u1f467\u200d\u1f466") != nil // true
s.range(of: "\u1f469\u200d\u1f467\u200d\u1f466") != nil // true
// Same as the above:
s.contains("\u1f469\u200d\u1f467\u200d\u1f466") // true
最简单的解决方案是为range(of:options:range:locale:)
方法提供特定的比较选项。选项String.CompareOptions.literal
对精确的逐个字符等效进行比较。附带说明一下,这里的字符的意思是不是 Swift Character
,而是实例和比较字符串的UTF-16 表示——然而,因为String
不允许格式错误UTF-16,这本质上相当于比较Unicode标量表示。
这里我已经重载了Foundation
方法,所以如果你需要原来的,重命名这个或者其他的:
extension String
func contains(_ string: String) -> Bool
return self.range(of: string, options: String.CompareOptions.literal) != nil
现在该方法“应该”对每个字符起作用,即使序列不完整:
s.contains("?") // true
s.contains("?\u200d") // true
s.contains("\u200d") // true
【讨论】:
@MartinR 根据当前的 UTR29 (Unicode 9.0),它是一个扩展的字素簇 (rules GB10 and GB11),但 Swift 显然使用的是旧版本。显然是fixing that is a goal for version 4 of the language,所以这种行为将来会改变。 @MichaelHomer:显然已经修复了,"????".count
在当前 Xcode 9 beta 和 Swift 4 中的计算结果为 1
。
哇。这是极好的。但现在我开始怀念过去,当时我遇到的字符串最严重的问题是它们是使用 C 还是 Pascal 风格的编码。
我理解为什么 Unicode 标准可能需要支持这一点,但是,如果有的话,这是一个过度设计的混乱:/
正确没有过度设计。【参考方案2】:
第一个问题是你用contains
(Swift 的String
不是Collection
)连接到Foundation,所以这是NSString
的行为,我不相信它处理组合表情符号的功能强大迅速。也就是说,我相信 Swift 现在正在实施 Unicode 8,这也需要围绕 Unicode 10 中的这种情况进行修改(所以当他们实施 Unicode 10 时这可能会发生变化;我还没有深入研究是否会这样做)。
为了简化事情,让我们摆脱 Foundation,并使用 Swift,它提供了更明确的视图。我们将从字符开始:
"????".characters.forEach print($0)
?
?
?
?
好的。这正是我们所期望的。但这是一个谎言。让我们看看这些角色到底是什么。
"????".characters.forEach print(String($0).unicodeScalars.map$0)
["\u0001F469", "\u200D"]
["\u0001F469", "\u200D"]
["\u0001F467", "\u200D"]
["\u0001F466"]
啊……原来是["?ZWJ", "?ZWJ", "?ZWJ", "?"]
。这让一切都变得更加清晰。 ? 不是此列表的成员(它是“?ZWJ”),但? 是成员。
问题在于Character
是一个“字素簇”,它将事物组合在一起(如附加 ZWJ)。您真正要寻找的是 unicode 标量。这完全符合您的预期:
"????".unicodeScalars.contains("?") // true
"????".unicodeScalars.contains("\u200D") // true
"????".unicodeScalars.contains("?") // true
"????".unicodeScalars.contains("?") // true
当然,我们也可以寻找其中的实际角色:
"????".characters.contains("?\u200D") // true
(这严重重复了 Ben Leggiero 的观点。我在注意到他已经回答之前发布了这个。留下以防万一有人更清楚。)
【讨论】:
ZWJ
代表什么?
零宽度连接器
@RobNapier in Swift 4, String
据称被改回集合类型。这会影响你的答案吗?
没有。这只是改变了下标之类的东西。它并没有改变 Characters 的工作方式。【参考方案3】:
似乎 Swift 认为 ZWJ
是一个扩展的字素簇,字符紧接在它前面。我们可以在将字符数组映射到它们的unicodeScalars
时看到这一点:
Array(manual.characters).map $0.description.unicodeScalars
这会从 LLDB 打印以下内容:
▿ 4 elements
▿ 0 : StringUnicodeScalarView("?")
- 0 : "\u0001F469"
- 1 : "\u200D"
▿ 1 : StringUnicodeScalarView("?")
- 0 : "\u0001F469"
- 1 : "\u200D"
▿ 2 : StringUnicodeScalarView("?")
- 0 : "\u0001F467"
- 1 : "\u200D"
▿ 3 : StringUnicodeScalarView("?")
- 0 : "\u0001F466"
此外,.contains
将扩展的字形簇组合成单个字符。例如,取韩文字符ᄒ
、ᅡ
和ᆫ
(组合成韩语中的“一”:한
):
"\u1112\u1161\u11AB".contains("\u1112") // false
这找不到ᄒ
,因为这三个代码点被分组到一个集群中,充当一个字符。同理,\u1F469\u200D
(WOMAN
ZWJ
)是一个簇,相当于一个字符。
【讨论】:
【参考方案4】:其他答案讨论了 Swift 的作用,但没有详细说明原因。
你认为“Å”等于“Å”吗?我希望你会。
其中一个是带有组合符的字母,另一个是单个组合字符。您可以将许多不同的组合器添加到基本角色中,而人类仍然会认为它是单个角色。为了处理这种差异,创建了字形的概念来表示人类对字符的看法,而不管使用的代码点如何。
多年来,短信服务一直在将字符组合成图形表情符号:)
→ ?
。因此,Unicode 中添加了各种表情符号。
这些服务也开始将表情符号组合成复合表情符号。
当然,没有合理的方法将所有可能的组合编码为单独的代码点,因此 Unicode 联盟决定扩展字素的概念以包含这些复合字符。
这归结为 "????"
如果您尝试在字素级别使用它,则应将其视为单个“字素集群”,就像 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
使这更容易,但这并不意味着没有其他事情变得更困难。
在这个级别上工作可以更容易地在复合字符中间意外拆分字符串。
您无意中问的是为什么这种更高级别的表示不像较低级别的表示那样工作。答案当然是,不应该。
如果你问自己“为什么要这么复杂”,答案当然是“人类”。
【讨论】:
你在最后一个示例行中迷失了我;rotor
和 grep
在这里做什么? 1-$l
是什么?
“字形”一词至少有 50 年的历史。 Unicode 将其引入标准是因为他们已经使用术语“字符”来表示与人们通常认为的字符完全不同的东西。我可以读到您写的内容与此一致,但怀疑其他人可能会产生错误的印象,因此(希望澄清)评论。
@BenLeggiero 首先,rotor
。代码say (1,2,3,4,5,6).rotor(3)
产生((1 2 3) (4 5 6))
。这是一个列表列表,每个长度为3
。 say (1,2,3,4,5,6).rotor(3=>-2)
产生相同的结果,除了第二个子列表以2
而不是4
开头,第三个以3
开头,依此类推,产生((1 2 3) (2 3 4) (3 4 5) (4 5 6))
。如果@match
包含"????".ords
,那么@Brad 的代码只会创建一个子列表,因此=>1-$l
位是不相关的(未使用)。仅当 @match
短于 @components
时才有意义。
grep
尝试匹配其调用者中的每个元素(在本例中为@components
的子列表列表)。它尝试将每个元素与其匹配器参数(在本例中为@match
)进行匹配。 .Bool
然后返回 True
如果 grep
产生至少一个匹配。【参考方案5】:
Swift 4.0 更新
String 在 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。这两个表情符号是表情符号序列的示例,其中几个表情符号组合在一起,有或没有零宽度连接符 @987654329 @ 它们之间。在 swift 3.0 中,此类字符串的字符数组将每个表情符号分开,并生成一个包含多个元素的数组(表情符号)。在此过程中忽略加入者。然而,在 Swift 4.0 中,字符数组将所有表情符号视为一个整体。所以任何表情符号的那个永远是1。
"??".characters.count // 1. In swift 3, this prints 2
"????".characters.count // 1. In swift 3, this prints 4
unicodeScalars
在 Swift 4 中保持不变。它提供给定字符串中唯一的 Unicode 字符。
"??".unicodeScalars.count // 2. Combination of two emoji
"????".unicodeScalars.count // 7. Combination of four emoji with joiner between them
2。包含
在 Swift 4.0 中,contains
方法会忽略表情符号中的零宽度连接符。因此,对于"????"
的四个表情符号组件中的任何一个,它都会返回 true,如果您检查加入者,则返回 false。但是,在 Swift 3.0 中,joiner 并没有被忽略,而是与前面的 emoji 组合在一起。因此,当您检查"????"
是否包含前三个组件表情符号时,结果将是错误的
"??".contains("?") // true
"??".contains("?") // true
"????".contains("????") // true
"????".contains("?") // true. In swift 3, this prints false
"????".contains("\u200D") // false
"????".contains("?") // true. In swift 3, this prints false
"????".contains("?") // true
【讨论】:
【参考方案6】:表情符号,很像 unicode 标准,看似复杂。肤色、性别、工作、人群、零宽度连接序列、标志(2 个字符 unicode)和其他复杂性可能会使表情符号解析变得混乱。一棵圣诞树、一片披萨或一堆便便都可以用一个 Unicode 代码点来表示。更不用说当引入新的表情符号时,ios 支持和表情符号发布之间存在延迟。以及不同版本的 iOS 支持不同版本的 unicode 标准这一事实。
TL;DR. 我致力于这些功能并开源了一个库,我是JKEmoji 的作者,以帮助解析带有表情符号的字符串。它使解析变得如此简单:
print("I love these emojis ??????????".emojiCount)
5
它通过从最新的 unicode 版本(最近的12.0)定期刷新所有已识别表情符号的本地数据库并通过查看将它们与正在运行的操作系统版本中被识别为有效表情符号的内容进行交叉引用来实现这一点在无法识别的表情符号字符的位图表示中。
注意
之前的答案因宣传我的图书馆而被删除,但没有明确说明我是作者。我再次承认这一点。
【讨论】:
虽然您的图书馆给我留下了深刻的印象,并且我看到它通常与手头的主题相关,但我看不出这与问题有什么直接关系以上是关于为啥表情符号字符像??????????????????在 Swift 字符串中被如此奇怪地对待?的主要内容,如果未能解决你的问题,请参考以下文章