Swift2.0 中的String(四):编码转换

Swift中的字符串,第四篇,中文字符编码的转换。其他的几篇传送门:

不知道是不是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-inExternel编码的列表供查询。
OK,编码部分结束!