在Swift中使用正则表达式去匹配时,匹配的字符串长度记得要使用
str.utf16.count
,而不是str.count
。
问题
最近把OC的正则匹配类转成Swift时发现一个“奇怪”的问题,代码跟OC无异,翻译成Swift是这样的:
enum RegexCoder {
case atUser(_ uid: Int? = nil)
var pattern: String {
switch self {
case .atUser(let uid):
if let uid {
return "\\[uid=\(uid)\\]"
}
return "\\[uid=\\d+\\]"
}
}
func matchRegexRanges(_ encodeText: String) -> [NSRange] {
do {
let regex = try NSRegularExpression(pattern: pattern, options: .caseInsensitive)
let results = regex.matches(in: encodeText,
options: .reportProgress,
range: NSRange(location: 0, length: encodeText.count))
return results.map { $0.range }
} catch let error {
print("正则匹配错误 - Invalid regex: \(error.localizedDescription)")
return []
}
}
......
}
这里是解析@人的正则表达式,找出匹配“[uid=xxxxxx]”的字符串位置然后替换成“@xxx”后的富文本。
但是后来匹配下面这段文本时,却只找到一个(明明两个):
let text = "以前喜欢一个人😺[uid=123456],现在喜欢一个人💔[uid=789123]"
let ranges = RegexCoder.atUser().matchRegexRanges(text)
print("ranges.count: \(ranges.count)") // -> 1
解决方法
百思不得其解,问了GPT,给出的回答跟我的代码差不多,一开始还以为AI骗人了,不过经过对比后发现,GPT的NSRange创建方式是这样的:
NSRange(location: 0, length: encodeText.utf16.count)
而我是这样的:
NSRange(location: 0, length: encodeText.count)
那就换成encodeText.utf16.count
试试,结果反而就能匹配到两个了!
原因
顺着这个疑问接着去追问GPT,才发现原来NSRange
是基于UTF-16编码的,原文如下:
在 Swift 中,encodeText.utf16.count 和 encodeText.count 之间的区别主要在于它们计数的方式和单位不同:
- encodeText.utf16.count: 这是基于 UTF-16 编码单元的计数方式。每个字符在 UTF-16 中可能占一个或两个编码单元(例如,基本多文种平面的字符占一个编码单元,而增补字符占两个编码单元)。这种计数方式与 NSRange 中的范围匹配是一致的,因为 NSRange 也是基于 UTF-16 编码的。
- encodeText.count: 这是基于 Unicode 标量(grapheme cluster)的计数方式。每个用户可感知的字符(即人类眼睛看到的字符)计为一个,即使某些字符在内部由多个标量组成。这种计数方式与 Swift 的 String API 是一致的,更加符合人类语言学的逻辑。
在正则表达式匹配过程中,我们需要使用 NSRange,因为 NSRegularExpression 的方法要求使用基于 UTF-16 的范围。这就是为什么我们要用 encodeText.utf16.count 而不是 encodeText.count。
简单点来说就是特殊字符,例如emoji,OC一个emoji长度为2(NSString
,相对于计算机的长度),而Swift是1(String
,相对于人眼看到的长度):
// 🌰🌰🌰
NSLog(@"oc 💔.length: %zd", @"💔".length);
print("swift 💔.count: \("💔".count)")
print("swift utf16 💔.count: \("💔".utf16.count)")
print("swift as NSString 💔.length: \(("💔" as NSString).length)")
// 打印如下:
oc 💔.length: 2
swift 💔.count: 1
swift utf16 💔.count: 2
swift as NSString 💔.length: 2
而NSRegularExpression
的range参数是NSRange
类型,长度需要使用NS体系的编码方式UTF-16,所以一开始长度传入的是Swift字符串的count
,导致要去匹配的字符串长度变短了(少了两个字符),因此找不到第二个。
总而言之,在Swift中如果要用到需要字符串长度的OC类时,记得要使用str.utf16.count
,而不是str.count
。
// PS:Swift中获取整段字符串的NSRange的两种方式:
NSRange(location: 0, length: str.utf16.count)
NSRange(str.startIndex..., in: str)