Swift中的字符串,第四篇,中文字符编码的转换。其他的几篇传送门:
- Swift2.0 中的String(一):常用属性
- Swift2.0 中的String(二):基本操作
- Swift2.0 中的String(三):类型转换
- Swift2.0 中的String(四):编码转换
- Swift2.0 中的String(五):String和NS-XXX系列的互相转换
- Swift2.0 中的String(六):正则匹配
我的关于String练习源代码可以在这里看到
不知道是不是Safari的原因,我用浏览器下载中文名文件的时候常常文件名会变成乱码,就是“%EF%77%3D%20”那种,又因为很多是电子书,名称也不能乱改,还需要自己去copy一遍重命名,很烦,于是想到用Swift自己写个函数试试纠正这个乱码。
你看到的是非授权版本!爬虫凶猛,请尊重知识产权!
转载请注明出处:http://conanwhf.github.io/2015/12/08/Swift_String_4/
访问原文「Swift2.0 中的String(四):编码转换」获取最佳阅读体验并参与讨论
最开始我的想法是在String的API里面找,所有encoding相关的都过滤了一遍,未果(后来证明其实我找对了方向,只是用错了编码参数);然后决定用自己拿手的方式,读取乱码数值填进数据块中,然后变成字符串。于是去网上搜索怎么填充字符串(顺便吐槽:我这边抽风cocoachina打不开,烦死),发现牵涉到NSData,NSString等等,同时也发现有人提供了一个字符串中文乱码的解决方案(UTF8转GBK):
NSURL *url = [NSURL URLWithString:urlStr];
NSData *data = [NSData dataWithContentsOfURL:url];
NSStringEncoding enc = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000);
NSString *retStr = [[NSString alloc] initWithData:data encoding:enc];
这一段OC又是url又是data的,看得我这个新手晕晕的,_kCFStringEncodingGB_18030_2000_在swift里面又没了,只好去看文档、头文件,试了很久还是没搞定。但这个过程让我又回到最开始的路子上了:找对应的函数,于是很快找到另外一个Swift的方案:
func UTF8ToGB2312(str: String) -> (NSData?, UInt) {
let enc = CFStringConvertEncodingToNSStringEncoding(UInt32(CFStringEncodings.GB_18030_2000.rawValue))
let data = str.dataUsingEncoding(enc, allowLossyConversion: false)
return (data, enc)
}
let (data, enc) = UTF8ToGB2312("123中文")
NSString(data: data!, encoding: enc)!
说实话这个也没有用啊!逻辑上是这个意思,但事实上input什么样output还是什么样!纠缠于GB_18030这么久却一无所获,我开始怀疑是Swift语法更新导致的差异了……(没有根据胡说而已)
好在几经胡乱努力,我终于找到了正确的API:
func addEncoding(st : String ) ->String? {
if #available(iOS 7.0, OSX 10.9, *) {
return st.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet())
}
else {
return st.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)
}
}
func rmEncoding(st : String ) ->String? {
if #available(iOS 7.0, OSX 10.9, *) {
return st.stringByRemovingPercentEncoding
}
else {
return st.stringByReplacingPercentEscapesUsingEncoding(NSUTF8StringEncoding)
}
}
其中stringByAdding(Replacing)PercentEncodingWithAllowedCharacters已经失效,iOS7以上使用两个替代品。如果说把重新编码看成是某种操作的话,上面的两个函数就是对字符串叠加和消除这种操作。经过测试,效果如下:
let s1 = "王"// 中文字符串:王
let s2 = addEncoding(s1)! // UTF8重编码后:%E7%8E%8B
let s3 = addEncoding(s2)! // 补全%25(即为空字符)后:%25E7%258E%258B
let s4 = addEncoding(s3)! // %2525E7%25258E%25258B
rmEncoding(s4) // %25E7%258E%258B == s3
rmEncoding(s3) // %E7%8E%8B == s2
rmEncoding(s2) // 王 == s1
rmEncoding(s1) // 王 == s1 == self
由此可见,(仅对UTF8编码,别的没测过)加减是相反的操作,有点像加壳脱壳的过程。已编码的字符串每继续叠加一次编码,就会用0x25填充每个字符;而有填充码时每remove一次编码就删掉一组填充码,直到最后还原为原始字符串后就不做任何操作了。于是,UTF8的中文编码转换变得很简单:
// 包含中文字符串转成utf8编码
let st = "www.google.com/测 🙃test/."
let utf8str = addEncoding(st)
// UTF8转成中文
rmEncoding(utf8str!)
至此,事情基本解决了,不过我还没有忘记最开始“手动填充数据的”梦想……😎正好在之前的研究过程中对String,NSData这些也有了一些了解,于是自己动手写了个相同功能的UTF8转中文,顺便练习String,还特意去用String中的Range:
func stConvert(var st: String) ->String{
var byte :[UInt8] = []
let start = st.startIndex
var range: Range? = Range(start: start, end: start)
while !st.isEmpty {
range = String(st.characters.dropFirst()).rangeOfString("%")
if (range != nil) {
/*still have next "%"
because the range is for dropfirst, the endIndex is the the true endof no % */
range!.startIndex = start
}
else { /*no "%" any more */
range = Range(start:start, end:st.endIndex)
}
if st.hasPrefix("%"){
var res:UInt32 = 0
range!.endIndex = range!.startIndex.advancedBy(3)
var temp = st.substringWithRange(range!)
temp = temp.stringByReplacingOccurrencesOfString("%", withString: "0x")
NSScanner.localizedScannerWithString(temp).scanHexInt(&res)
byte.append(UInt8(res))
}
else {
let temp :NSString = st.substringWithRange(range!)
for i in 0..<temp.lengthOfBytesUsingEncoding(NSUTF8StringEncoding) {
byte.append(UInt8(temp.UTF8String[i]))
}
}
st.removeRange(range!)
}
let data = NSData(bytes: byte, length: byte.count)
return String(data: data, encoding: NSUTF8StringEncoding)!
}
stConvert("1%2B12%EF%BC%9A%E9%80%9A%E5%90%91%E5%B8%B8%E8%AF%86%E7%9A%84%E9%81%93%E8%B7%AF%20%28%E6%80%9D%E4%BA%AB%E5%AE%B6%E4%B8%9B%E4%B9%A6%29%20-%20%E5%88%98%E8%8B%8F%E9%87%8C%F0%9F%90%B6.mobi")
主要的思路就是找“%”,然后将格式化的十六进制数转换为数值按次序填进NSData中,最后用RawData转换成字符串。考虑到给的字符串可能包含部分不会被重编码的部分(例如数字之类),需要判断一下,这部份字符串就转换成Ascii码填充进去。在做的过程中我碰到了一个很无语的坑:
Rang获取的范围(start, end)表示的是String的start..\<end,即[start…end-1]部分!string[end]是不包括的!
我不知道我是不是一个人,虽然文档明明白白写了,但没太注意到,用的时候又想当然了,结果死循环差点把Xcode搞死……😂话说没有找到在字符串中匹配某个字符第一个位置的API,感觉用Range还是蛮不方便的……
其实编码无非是编码和解码,所以String中的转换基本上就这样了。关于不同的编码类型NSStringEncoding,其实是一个UInt32。这里通篇都用的NSUTF8StringEncoding,按照文档的描述:
This type is used to define the constants for the built-in encodings (see Built-in String Encodings for a list) and for platform-dependent encodings (see External String Encodings). If CFString does not recognize or support the string encoding of a particular string, CFString functions will identify the string’s encoding as kCFStringEncodingInvalidId.
在Swift中Built-in的编码是有对应的类似_NSXXXXEncoding_可以作为参数直接使用,而External那些则需要申请一个NSStringEncoding类型的变量,按照前面_func UTF8ToGB2312_的方式去赋值使用了。顺手附上Build-in和Externel编码的列表供查询。
OK,编码部分结束!