我需要确定一个字符串中的字符是否为表情符号。
例如,我有这个字符:
let string = ""
let character = Array(string)[0]
我需要确定那个字符是否为表情符号。
我发现的是字符、Unicode标量和字形之间的区别。
例如,字形 由7个Unicode标量组成:
另一个例子,字形 由2个Unicode标量组成:
最后一个例子,字形 1️⃣ 包含三个Unicode字符:
因此,在呈现字符时,结果的字形非常重要。
Swift 5.0及以上版本使这个过程变得更加简单,并且消除了我们需要做出的一些猜测。 Unicode.Scalar
的新Property
类型有助于确定我们正在处理的是什么。
但是,只有在检查字形中的其他标量时,这些属性才有意义。这就是为什么我们将添加一些方便方法到Character类来帮助我们的原因。
有关更多详细信息,我写了一篇文章来解释这是如何工作的。
对于Swift 5.0,这让您得到以下结果:
extension Character {
/// A simple emoji is one scalar and presented to the user as an Emoji
var isSimpleEmoji: Bool {
guard let firstScalar = unicodeScalars.first else { return false }
return firstScalar.properties.isEmoji && firstScalar.value > 0x238C
}
/// Checks if the scalars will be merged into an emoji
var isCombinedIntoEmoji: Bool { unicodeScalars.count > 1 && unicodeScalars.first?.properties.isEmoji ?? false }
var isEmoji: Bool { isSimpleEmoji || isCombinedIntoEmoji }
}
extension String {
var isSingleEmoji: Bool { count == 1 && containsEmoji }
var containsEmoji: Bool { contains { $0.isEmoji } }
var containsOnlyEmoji: Bool { !isEmpty && !contains { !$0.isEmoji } }
var emojiString: String { emojis.map { String($0) }.reduce("", +) }
var emojis: [Character] { filter { $0.isEmoji } }
var emojiScalars: [UnicodeScalar] { filter { $0.isEmoji }.flatMap { $0.unicodeScalars } }
}
这将会给您以下结果:
"A̛͚̖".containsEmoji // false
"3".containsEmoji // false
"A̛͚̖▶️".unicodeScalars // [65, 795, 858, 790, 9654, 65039]
"A̛͚̖▶️".emojiScalars // [9654, 65039]
"3️⃣".isSingleEmoji // true
"3️⃣".emojiScalars // [51, 65039, 8419]
"".isSingleEmoji // true
"♂️".isSingleEmoji // true
"".isSingleEmoji // true
"⏰".isSingleEmoji // true
"".isSingleEmoji // true
"".isSingleEmoji // true
"".isSingleEmoji // true
"".containsOnlyEmoji // true
"".containsOnlyEmoji // true
"Hello ".containsOnlyEmoji // false
"Hello ".containsEmoji // true
" Héllo ".emojiString // ""
"".count // 1
" Héllœ ".emojiScalars // [128107, 128104, 8205, 128105, 8205, 128103, 8205, 128103]
" Héllœ ".emojis // ["", ""]
" Héllœ ".emojis.count // 2
"".isSingleEmoji // false
"".containsOnlyEmoji // true
$0.properties.generalCategory == .otherSymbol
,以便使其适用于更多的表情符号,例如⏰、🧱等。 - vicegax最简单、最清晰、也是最快速的方法是,对于字符串中的每个字符,仅需检查其 Unicode 代码点是否位于已知的表情符号和特殊符号范围之内,例如:
extension String {
var containsEmoji: Bool {
for scalar in unicodeScalars {
switch scalar.value {
case 0x1F600...0x1F64F, // Emoticons
0x1F300...0x1F5FF, // Misc Symbols and Pictographs
0x1F680...0x1F6FF, // Transport and Map
0x2600...0x26FF, // Misc symbols
0x2700...0x27BF, // Dingbats
0xFE00...0xFE0F, // Variation Selectors
0x1F900...0x1F9FF, // Supplemental Symbols and Pictographs
0x1F1E6...0x1F1FF: // Flags
return true
default:
continue
}
}
return false
}
}
0x1F900...0x1F9FF
(根据维基百科)。不确定这个范围内的所有内容都应该被视为表情符号。 - Frizlab引入了一种全新的检查方法,可以准确地检查字符串中的表情符号!
您需要将String
拆分为其Scalars
。每个Scalar
都有一个Property
值,支持isEmoji
值!
实际上,您甚至可以检查标量是否是表情符号修饰符或更多。请查看苹果的文档:https://developer.apple.com/documentation/swift/unicode/scalar/properties
您可能希望考虑检查isEmojiPresentation
而不是isEmoji
,因为苹果针对isEmoji
给出了以下说明:
此属性对于默认呈现为表情符号的标量以及在其后跟随U+FE0F VARIATION SELECTOR-16时具有非默认表情符号呈现的标量为真。这包括一些通常不被视为表情符号的标量。
这种方法实际上将表情符号分成所有修饰符,但处理起来更加简单。由于Swift现在将带有修饰符的表情符号(例如:,)视为1,因此您可以进行各种操作。
var string = " test"
for scalar in string.unicodeScalars {
let isEmoji = scalar.properties.isEmoji
print("\(scalar.description) \(isEmoji)")
}
// true
// false
// t false
// e false
// s false
// t false
NSHipster指出了一种有趣的获取所有Emoji表情符号的方法:
import Foundation
var emoji = CharacterSet()
for codePoint in 0x0000...0x1F0000 {
guard let scalarValue = Unicode.Scalar(codePoint) else {
continue
}
// Implemented in Swift 5 (SE-0221)
// https://github.com/apple/swift-evolution/blob/master/proposals/0221-character-properties.md
if scalarValue.properties.isEmoji {
emoji.insert(scalarValue)
}
}
scalar.properties.isEmoji scalar.properties.isEmojiPresentation scalar.properties.isEmojiModifier scalar.properties.isEmojiModifierBase scalar.properties.isJoinControl scalar.properties.isVariationSelector
- A Springham"6".unicodeScalars.first!.properties.isEmoji
将被计算为true
。 - Miniroo#
和*
这样的字符也会被isEmoji
检查返回为真。 isEmojiPresentation
似乎工作更好,至少它会对英文-US键盘上的0...9
,#
,*
和任何其他符号返回false
。有没有人对此有更多经验,并知道是否可以信任它进行输入验证? - JanisEmoji
属性为 true
,但 isEmojiPresentation
属性为 false
。第二个标量只有在 isVariationSelector
属性返回 true
时才会被视为表情符号。因此似乎没有直接的方式来理解什么是表情符号。 - zh.0x1F0000
处停止?最高合法的Unicode代码点(标量)值是0x10FFFF
。因此,在上面的循环中,guard
语句及其未成功构造Unicode.Scaler()的尝试会不必要地继续循环917,505次。或者你可能是想用break
而不是continue
。我错过了什么吗? - jsbox使用 Swift 5,您现在可以检查字符串中每个字符的 Unicode 属性。这为我们提供了每个字母上方便的isEmoji
变量。问题是isEmoji
将对任何可以转换为2字节表情符号(如0-9)的字符返回true。
我们可以查看变量 isEmoji
并检查是否存在表情符号修饰符以确定模棱两可的字符是否将显示为表情符号。
与此处提供的正则表达式解决方案相比,这种解决方案应该更具未来性。
extension String {
func containsEmoji() -> Bool {
contains { $0.isEmoji }
}
func containsOnlyEmojis() -> Bool {
return count > 0 && !contains { !$0.isEmoji }
}
}
extension Character {
// An emoji can either be a 2 byte unicode character or a normal UTF8 character with an emoji modifier
// appended as is the case with 3️⃣. 0x203C is the first instance of UTF16 emoji that requires no modifier.
// `isEmoji` will evaluate to true for any character that can be turned into an emoji by adding a modifier
// such as the digit "3". To avoid this we confirm that any character below 0x203C has an emoji modifier attached
var isEmoji: Bool {
guard let scalar = unicodeScalars.first else { return false }
return scalar.properties.isEmoji && (scalar.value >= 0x203C || unicodeScalars.count > 1)
}
}
给我们
"hey".containsEmoji() //false
"Hello World ".containsEmoji() //true
"Hello World ".containsOnlyEmojis() //false
"3".containsEmoji() //false
"3️⃣".containsEmoji() //true
Character("3️⃣").isEmoji // true
而
Character("3").isEmoji // false
- Paul Bextension String {
func containsEmoji() -> Bool {
for scalar in unicodeScalars {
switch scalar.value {
case 0x3030, 0x00AE, 0x00A9,// Special Characters
0x1D000...0x1F77F, // Emoticons
0x2100...0x27BF, // Misc symbols and Dingbats
0xFE00...0xFE0F, // Variation Selectors
0x1F900...0x1F9FF: // Supplemental Symbols and Pictographs
return true
default:
continue
}
}
return false
}
}
以下是使用Scalarsçš„Swift 5解决方案,å�¯åº”用äº�文本ã€�笑脸😊,心形表情â�¤ï¸�â�¤ï¸�â€�和数å—0ï¸�⃣ 1 2 3ç‰ã€‚
isEmoji
�性和isEmojiPresentation
å±�性å�¯ä»¥å¸®åŠ©æˆ‘们在给定的å—符串ä¸æ‰¾åˆ°è¡¨æƒ…符å�·ã€‚
isEmoji - 布尔值,指示这个scalar是å�¦æœ‰ä¸€ä¸ªè¡¨æƒ…符å�·, æ— è®ºæ˜¯ä¸�是默认值。
isEmojiPresentation - 布尔值,指示这个scalar是�应该以表情符�呈�, 而�是默认的文本呈�方�。
ä»�这些定义ä¸ï¼Œæˆ‘们å�¯ä»¥çœ‹å‡ºï¼Œåœ¨å—ç¬¦ä¸²çš„æ ‡é‡�上仅使用isEmoji
或isEmojiPresentation
是ä¸�够的 - è¿™ä¸�èƒ½å‘Šè¯‰æˆ‘ä»¬è¿™ä¸ªæ ‡é‡�是å�¦æ˜¯ä¸€ä¸ªçœŸæ£çš„表情符å�·ã€‚
幸�的是,Apple为我们�供了一些线索:
仅仅使用
isEmoji
æ�¥å�•ç‹¬æµ‹è¯•å�•ä¸ªæ ‡é‡�, 是ä¸�足以确定所检测到的文本å�•å…ƒæ˜¯å�¦å‘ˆç�°ä¸ºè¡¨æƒ…符å�·çš„ï¼› æ£ç¡®çš„测试需è¦�检查Characterä¸çš„å¤šä¸ªæ ‡é‡�ã€‚é™¤äº†æ£€æŸ¥åŸºç¡€æ ‡é‡�是å�¦å…·æœ‰isEmoji == true
, 还必须检查它的默认表示(请�阅isEmojiPresentation
),并确定它是���带有�以修改呈�方�的�异选择器。
所以这里是我的å®�ç�°ï¼Œé€‚用äº�æ•°å—ã€�笑脸,文本和â�¤ï¸�符å�·ï¼š
import Foundation
extension String {
func containsEmoji() -> Bool {
for character in self {
var shouldCheckNextScalar = false
for scalar in character.unicodeScalars {
if shouldCheckNextScalar {
if scalar == "\u{FE0F}" { // scalar that indicates that character should be displayed as emoji
return true
}
shouldCheckNextScalar = false
}
if scalar.properties.isEmoji {
if scalar.properties.isEmojiPresentation {
return true
}
shouldCheckNextScalar = true
}
}
}
return false
}
}
测试:
"hello ❤️".containsEmoji() // true
"1234567890".containsEmoji() // false
"numero 0️⃣".containsEmoji() // true
"abcde".containsEmoji() // false
"panda ".containsEmoji() // true
\u{FE0E}
)不在标量中,这样才能确定你的第一个测试用例是否为false
,因为我看到的心形是非表情符号版本的❤️。 - Graham Lea有一个很好的solution可以解决提到的问题。但是检查Unicode.Scalar.Properties对于单个字符非常好,而对于字符串来说则不够灵活。
我们可以使用正则表达式——更通用的方法。下面详细介绍了它的工作原理。这里是解决方案。
在Swift中,您可以使用具有此类计算属性的扩展来检查String是否为单个Emoji字符:
extension String {
var isSingleEmoji : Bool {
if self.count == 1 {
let emodjiGlyphPattern = "\\p{RI}{2}|(\\p{Emoji}(\\p{EMod}|\\x{FE0F}\\x{20E3}?|[\\x{E0020}-\\x{E007E}]+\\x{E007F})|[\\p{Emoji}&&\\p{Other_symbol}])(\\x{200D}(\\p{Emoji}(\\p{EMod}|\\x{FE0F}\\x{20E3}?|[\\x{E0020}-\\x{E007E}]+\\x{E007F})|[\\p{Emoji}&&\\p{Other_symbol}]))*"
let fullRange = NSRange(location: 0, length: self.utf16.count)
if let regex = try? NSRegularExpression(pattern: emodjiGlyphPattern, options: .caseInsensitive) {
let regMatches = regex.matches(in: self, options: NSRegularExpression.MatchingOptions(), range: fullRange)
if regMatches.count > 0 {
// if any range found — it means, that that single character is emoji
return true
}
}
}
return false
}
}
单个表情符号(图形符号)可以由多种不同的符号、序列及其组合来复制。Unicode规范定义了几种可能的表情符号字符表示。
由单个Unicode标量复制的表情符号字符。
Unicode将表情符号字符定义为:
emoji_character := \p{Emoji}
emoji_character := \p{Emoji}&&\p{Other_symbol}
一个字符,通常可以被绘制为文本或表情符号。它的外观取决于一个特殊的后续符号,即展示选择器,它指示其展示类型。\x{FE0E} 定义了文本表示。 \x{FE0F} 定义了表情符号表示。
这些符号的列表可以在此处找到(https://unicode.org/Public/emoji/12.1/emoji-variation-sequences.txt)。
Unicode 将展示序列定义如下:
emoji_presentation_sequence := emoji_character emoji_presentation_selector
它的正则表达式序列:
emoji_presentation_sequence := \p{Emoji} \x{FE0F}
该序列与展示序列非常相似,但末尾有额外的标量:\x{20E3}。用于它的可能基本标量的范围相当狭窄:0-9#* ——仅此而已。例如:1️⃣,8️⃣,*️⃣。
Unicode将键帽序列定义为:
emoji_keycap_sequence := [0-9#*] \x{FE0F 20E3}
它的正则表达式:
emoji_keycap_sequence := \p{Emoji} \x{FE0F} \x{FE0F}
一些表情符号可以具有修改外观的功能,例如肤色。例如表情符号 可以不同: 。要定义一个表情符号,在这种情况下称为“表情符号修改基础”,可以使用后续的“表情符号修改器”。
通常这样的序列看起来像这样:
emoji_modifier_sequence := emoji_modifier_base emoji_modifier
emoji_modifier_sequence := \p{Emoji} \p{EMod}
国旗是具有特定结构的表情符号。每个国旗用两个“区域指示器”符号表示。
Unicode将它们定义为:
emoji_flag_sequence := regional_indicator regional_indicator
emoji_flag_sequence := \p{RI}{2}
使用所谓的标签基础(tag_base)后面跟随自定义标签规范的序列,由符号范围\x{E0020}-\x{E007E}组成,并以标签结束标记\x{E007F}结尾。
Unicode将其定义为:
emoji_tag_sequence := tag_base tag_spec tag_end
tag_base := emoji_character
| emoji_modifier_sequence
| emoji_presentation_sequence
tag_spec := [\x{E0020}-\x{E007E}]+
tag_end := \x{E007F}
\p{Emoji} [\x{E0020}-\x{E007E}]+ \x{E007F}
零宽连接器是一个标量 \x{200D}。通过它,几个已经是表情符号的字符可以组合成新的表情符号。
例如,“有父亲、儿子和女儿的家庭”表情符号 可以由父亲、女儿和儿子表情符号用 ZWJ 符号粘合在一起重现。
允许将单个表情符号字符、表示和修饰序列粘在一起。
这种序列的正则表达式通常如下所示:
emoji_zwj_sequence := emoji_zwj_element (\x{200d} emoji_zwj_element )+
上述提到的所有Emoji表示都可以用一个正则表达式描述:
\p{RI}{2}
| ( \p{Emoji}
( \p{EMod}
| \x{FE0F}\x{20E3}?
| [\x{E0020}-\x{E007E}]+\x{E007F}
)
|
[\p{Emoji}&&\p{Other_symbol}]
)
( \x{200D}
( \p{Emoji}
( \p{EMod}
| \x{FE0F}\x{20E3}?
| [\x{E0020}-\x{E007E}]+\x{E007F}
)
| [\p{Emoji}&&\p{Other_symbol}]
)
)*
self.count == 1
? - GiorgioSwift 3 注意:
cnui_containsEmojiCharacters
方法似乎已被删除或移动到另一个动态库。但是,_containsEmoji
应该仍然可以使用。
let str: NSString = "hello"
@objc protocol NSStringPrivate {
func _containsEmoji() -> ObjCBool
}
let strPrivate = unsafeBitCast(str, to: NSStringPrivate.self)
strPrivate._containsEmoji() // true
str.value(forKey: "_containsEmoji") // 1
let swiftStr = "hello"
(swiftStr as AnyObject).value(forKey: "_containsEmoji") // 1
Swift 2.x:
我最近发现了一个在 NSString
上的私有 API,它可以用于检测字符串是否包含表情符号:
let str: NSString = "hello"
使用 Objective-C 协议和 unsafeBitCast
:
@objc protocol NSStringPrivate {
func cnui_containsEmojiCharacters() -> ObjCBool
func _containsEmoji() -> ObjCBool
}
let strPrivate = unsafeBitCast(str, NSStringPrivate.self)
strPrivate.cnui_containsEmojiCharacters() // true
strPrivate._containsEmoji() // true
valueForKey
方法:str.valueForKey("cnui_containsEmojiCharacters") // 1
str.valueForKey("_containsEmoji") // 1
使用纯Swift字符串时,您必须在使用valueForKey
之前将字符串转换为AnyObject
:
let str = "hello"
(str as AnyObject).valueForKey("cnui_containsEmojiCharacters") // 1
(str as AnyObject).valueForKey("_containsEmoji") // 1
这里是在NSString头文件中发现的方法。
多年来,随着苹果添加了新的表情符号和新方法(如通过在字符前加上额外字符构建肤色表情符号),这些检测表情符号的解决方案不断破裂。
我最终放弃了,并编写了以下方法,它适用于所有当前的表情符号,并应该适用于所有未来的表情符号。
该解决方案创建一个带有字符和黑色背景的UILabel。然后CG对标签进行快照,我扫描快照中的所有像素以查找任何非纯黑色像素。我添加黑色背景的原因是避免由于子像素渲染而导致的误色问题。
该解决方案在我的设备上运行非常快,我可以每秒检查数百个字符,但需要注意的是,这是一个CoreGraphics解决方案,不能像普通文本方法那样频繁使用。图形处理是数据密集型的,因此一次检查数千个字符可能会导致明显的延迟。
-(BOOL)isEmoji:(NSString *)character {
UILabel *characterRender = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 1, 1)];
characterRender.text = character;
characterRender.font = [UIFont fontWithName:@"AppleColorEmoji" size:12.0f];//Note: Size 12 font is likely not crucial for this and the detector will probably still work at an even smaller font size, so if you needed to speed this checker up for serious performance you may test lowering this to a font size like 6.0
characterRender.backgroundColor = [UIColor blackColor];//needed to remove subpixel rendering colors
[characterRender sizeToFit];
CGRect rect = [characterRender bounds];
UIGraphicsBeginImageContextWithOptions(rect.size,YES,0.0f);
CGContextRef contextSnap = UIGraphicsGetCurrentContext();
[characterRender.layer renderInContext:contextSnap];
UIImage *capturedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CGImageRef imageRef = [capturedImage CGImage];
NSUInteger width = CGImageGetWidth(imageRef);
NSUInteger height = CGImageGetHeight(imageRef);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
unsigned char *rawData = (unsigned char*) calloc(height * width * 4, sizeof(unsigned char));
NSUInteger bytesPerPixel = 4;//Note: Alpha Channel not really needed, if you need to speed this up for serious performance you can refactor this pixel scanner to just RGB
NSUInteger bytesPerRow = bytesPerPixel * width;
NSUInteger bitsPerComponent = 8;
CGContextRef context = CGBitmapContextCreate(rawData, width, height,
bitsPerComponent, bytesPerRow, colorSpace,
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGColorSpaceRelease(colorSpace);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGContextRelease(context);
BOOL colorPixelFound = NO;
int x = 0;
int y = 0;
while (y < height && !colorPixelFound) {
while (x < width && !colorPixelFound) {
NSUInteger byteIndex = (bytesPerRow * y) + x * bytesPerPixel;
CGFloat red = (CGFloat)rawData[byteIndex];
CGFloat green = (CGFloat)rawData[byteIndex+1];
CGFloat blue = (CGFloat)rawData[byteIndex+2];
CGFloat h, s, b, a;
UIColor *c = [UIColor colorWithRed:red green:green blue:blue alpha:1.0f];
[c getHue:&h saturation:&s brightness:&b alpha:&a];//Note: I wrote this method years ago, can't remember why I check HSB instead of just checking r,g,b==0; Upon further review this step might not be needed, but I haven't tested to confirm yet.
b /= 255.0f;
if (b > 0) {
colorPixelFound = YES;
}
x++;
}
x=0;
y++;
}
return colorPixelFound;
}
let character = string[string.index(after: string.startIndex)]
或let secondCharacter = string[string.index(string.startIndex, offsetBy: 1)]
- Paul B