在url字符串中添加参数遇到的编码问题

这是实际工作中的一个例子,也算是一个坑吧,把过程记录一下也是好的。

问题描述

输入url:
https://host/path/index.html
输入参数,key值qresult:
http://qd.xxx.com/?paymentCode=ZA20632016080917074300000000268043
期望输出(Android):
https://host/path/index.html?qresult=http%3A%2F%2Fqd.xxx.com%2F%3FpaymentCode%3DZA20632016080917074300000000268043
实际输出:
https://host/path/index.html?qresult=http://qd.xxx.com/?paymentCode%3DZA20632016080917074300000000268043
问题:
http://qd.xxx.com/? ---- 问号前面部分没有编码

实现代码

let rawUrlString = "https://host/path/index.html"
guard let components = NSURLComponents(string: rawUrlString) else {
    return
}
let key = "qresult"
let value = "http://qd.xxx.com/?paymentCode=ZA20632016080917074300000000268043"
let item = NSURLQueryItem(name:key, value: value)
if let _ = components.queryItems {
    components.queryItems?.append(item)
} else {
    components.queryItems = [item]
}
let newUrlString = components.URL?.absoluteString
print(newUrlString)

这段代码的输出:
Optional("https://host/path/index.html?qresult=http://qd.xxx.com/?paymentCode%3DZA20632016080917074300000000268043")

真有问题吗?

  • 基本思路是利用NSURLComponents进行参数添加参数,方案很优雅

  • 整个过程不牵涉到url的解码编码等问题,全部交给系统API去完成

  • 从打印出来的结果看是有问题的,query参数中出现“://?”之类的是不符合URL标准的,从现象上看跟Android的有差异

  • 单从功能上来讲,经过验证是可行的。整体的url字符串可以形成一个合法的NSURL,用WebView能够正确加载页面。qresult后面的参数也能够正确取出,这也是个url字符串,也可以形成一个合法的NSURL,也能用WebView进行正确加载。 === 整个过程全能通,开发者不需要关心url编码和解码的事,系统API在底层能很好地工作。

  • 调试print,或者断点po出来的信息,是对象的描述信息。可以推测NSURLComponents在实现描述信息,或者实现描述协议的时候,进行了一层编解码,方便查看,但是底层实际工作还是按照标准的URL进行的。

  • 如果在NSURLQueryItem的构建过程中,对值进行编码,经过测试,最终形成的结果中,会对百分号%本身也进行编码,结果反而不对,打印信息和实际功能(形成合法的NSURL)都不对。

let item = NSURLQueryItem(name:key, value: value.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLHostAllowedCharacterSet()))

如何一致?

优雅的NSURLComponents是用不上了,那就用最原始的字符串拼接。系统函数是stringByAddingPercentEncodingWithAllowedCharacters,确定参数是一个难点。
下面这段代码,在目前这个实例下面是有效的,功能和输出都能和Android保持一致。

let rawUrlString = "https://host/path/index.html"
let key = "qresult"
let value = "http://qd.xxx.com/?paymentCode=ZA20632016080917074300000000268043"
// 这里如果用参数NSCharacterSet.URLHostAllowedCharacterSet(),会出现"="没有编码的情况
// valueEncode = "http%3A%2F%2Fqd.xxx.com%2F%3FpaymentCode=ZA20632016080917074300000000268043"  --- 这里有个等号
if let valueEncode = value.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet(charactersInString:":=\"#%/<>?@\\^`{|}").invertedSet) {
    let newUrlString = rawUrlString + "?" + key + "=" + valueEncode
    print(newUrlString)
}

这段代码的输出:
https://host/path/index.html?qresult=http%3A%2F%2Fqd.xxx.com%2F%3FpaymentCode%3DZA20632016080917074300000000268043

  • 功能正常,由这个字符串能够创建NSURL对象

  • 打印输出和Android保持一致,符合标准url格式

  • NSCharacterSet(charactersInString:":=\"#%/<>?@\\^`{|}").invertedSet比较难理解。这是反逻辑,它的意思是除了":="#%/<>?@\^`{|}"以外的其他字符的集合

  • 这是从stackoverflow上抄的代码,其中的“:”还是我加上去的。这里是OK了,其他地方呢?

参考Android

既然要做到和Android的输出结果一致,那么参考Android的做法应该是可取的。

  • Android代码截图
在url字符串中添加参数遇到的编码问题_第1张图片
Android允许字符.jpg
  • 这段代码的意思是,url中允许的字符包括:数字,大小写字母,以及“_-!.~'()*”

  • 将上面代码的反逻辑改成和Android一样的正逻辑,应该可行,也容易理解

let rawUrlString = "https://host/path/index.html"
let key = "qresult"
let value = "http://qd.xxx.com/?paymentCode=ZA20632016080917074300000000268043"
let unreservedChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-!.~'()*"
if let valueEncode = value.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet(charactersInString: unreservedChars)) {
    let newUrlString = rawUrlString + "?" + key + "=" + valueEncode
    print(newUrlString)
}

这段代码的输出:
https://host/path/index.html?qresult=http%3A%2F%2Fqd.xxx.com%2F%3FpaymentCode%3DZA20632016080917074300000000268043

  • 功能和打印输出都是正确的

  • 和Android在标准和行为上都达到了一致

几个工具

extension String {
    func getScheme() -> String? {
        return NSURLComponents(string: self)?.scheme
    }
    
    func getHost() -> String? {
        return NSURLComponents(string: self)?.host
    }
    
    func getQueryParameter(key: String) -> String? {
        
        let urlItem = NSURLComponents(string: self)?.queryItems?.filter({ (item) -> Bool in
            return item.name == key
        }).first
        return urlItem?.value
    }
    
    func appendQueryParameter(key: String, value: String) -> String {
        guard let components = NSURLComponents(string: self) else {
            return self
        }
        let item = NSURLQueryItem(name:key, value: value)
        if let _ = components.queryItems {
            components.queryItems?.append(item)
        } else {
            components.queryItems = [item]
        }
        if let url = components.URL?.absoluteString {
            return url
        } else {
            return self
        }
    }
    
    func urlEncode() -> String? {
        let unreservedChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-!.~'()*"
        let unreservedCharset = NSCharacterSet(charactersInString: unreservedChars)
        let encodedString = stringByAddingPercentEncodingWithAllowedCharacters(unreservedCharset)
        return encodedString
    }
    
    func appendQueryParameterUseEncode(key: String, value: String) -> String {
        guard let components = NSURLComponents(string: self), url = components.URL, encodedValue = value.urlEncode() else {
            return self
        }
        if let queryItems = components.queryItems { // 带参数
            if (queryItems.count > 0) && !url.absoluteString.hasSuffix("&") {
                return url.absoluteString + "&" + key + "=" + encodedValue
            } else { // 带问号,但没有参数
                return url.absoluteString + key + "=" + encodedValue
            }
        } else { // 不带参数
            return url.absoluteString + "?" + key + "=" + encodedValue
        }
    }
}
  • NSURL和NSURLComponents都是类,结构比较大

  • NSURL还有很大一部分是文件系统的内容,这又是一个大坑

  • 在模块间传递,可能涉及到多中平台,字符串还是用得更多

  • 客户端和网络之间,大多数是在字符串的基础上组装字典,JSON编码传输。这几个工具可以方便使用。

  • 计算属性本质上也是函数,不过函数可以指定参数,感觉功能更强大一点,也更灵活一点。当然,用计算属性会显得更简洁一些。这两种都是可以考虑的

遗留问题

RFC1738里面的相关部分如下:

在url字符串中添加参数遇到的编码问题_第2张图片
RFC1738.jpg

通过比较可以知道,有两个字符“,$”漏掉了。是有原因,故意不放上去,还是某种原因遗漏,无从知晓。

例子代码

UrlParseUtility

参考文章

在线编码转换

Swift - encode URL

How to encode a URL in Swift

关于URL编码

你可能感兴趣的:(在url字符串中添加参数遇到的编码问题)