iOS URL編碼(百分號編碼)研究

URL編碼(URL encoding)也稱為百分號編碼(Percent-encoding), 是特定上下文統(tǒng)一資源定位符(URI)的編碼機(jī)制. 實(shí)際上也使用與統(tǒng)一資源標(biāo)志符(URI)的編碼.

對于URI, 具體的結(jié)構(gòu)如下:

foo://example.com:8042/over/there?name=ferret#nose

   \_/ \______________/ \________/\_________/ \__/

    |         |              |         |        |

  scheme     authority      path      query   fragment

我們能夠看到:/?#[]@是用來分隔URI的不同的組件的.

混亂的URL編碼

具體參考: 阮一峰: 關(guān)于URL編碼.

URI的字符類型

URI所允許的字符分成保留未保留. 保留字符是那些具有特殊含義的字符. 例如, /字符用于URL不同部分的分節(jié)符(https://www.baidu.com/news). 未保留字符沒有這些特殊含義. 百分號編碼把保留字符表示為特殊字符序列, 根據(jù)URI的版本的不同略有變化, 下面是 RFC 3986保留字符, 與未保留字符:

  • 保留: ! * ' ( ) ; : @ & = + $ , / ? # [ ]

  • 未保留: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9 - _ . ~

除此之外, URI中的其它字符必須用百分號編碼.

URI的保留字符的百分號編碼

前面我們知道, URI的保留字符有特殊含義(reserved purpose), 并且URI中必須使用該字符用于其他目的, 那么該字符必須百分號編碼. 這里所說的其他目的, 我們可以這樣理解, 在URL中?key1=val1&key2=val2中的val1中如果有保留字符&或者*,那么這里保留字符用于其他目的, 需要編碼.

百分號編碼一個保留字符, 首先需要把該字符的ASCII的值表示為兩個16進(jìn)制的數(shù)字, 然后在其前面放置轉(zhuǎn)義字符("%"), 置入URI中的相應(yīng)位置. (對于非ASCII字符, 需要轉(zhuǎn)換為UTF-8字節(jié)序, 然后每個字節(jié)按照上述方式表示.)

!   #   $   &   '   (   )   *   +   ,   /   :   ;   =   ?   @   [   ]

%21 %23 %24 %26 %27 %28 %29 %2A %2B %2C %2F %3A %3B %3D %3F %40 %5B %5D

在特定上下文中沒有特殊含義的保留字符也可以被百分號編碼, 在語義上與不百分號編碼的該字符沒有差別(這個特點(diǎn)非常重要, 如果我們不知道該字符是否需要被百分號編碼, 那么最好用百分號編碼一下).

在URI的查詢部分(?字符后的部分)中, 例如/仍然是保留字符但是沒有特殊含義, 除非一個特定的URI有其它規(guī)定. 該/字符在沒有特殊含義時不需要百分號編碼.例如https://www.baidu.com/news?name=p/p&age=13, 其中name=p/p中的/保留字符但是沒有特殊含義, 在實(shí)際使用中可以不用給它進(jìn)行百分號編碼.

如果保留字符具有特殊含義, 那么該保留字符用百分號編碼的URI與該保留字符僅用其自身表示的URI具有不同的語義.

URI中百分號編碼未保留字符

未保留字符不需要百分號編碼.

兩個URI的差別如果僅在于未保留字符是用百分號編碼還是字符本身表示, 那么這兩個URI具有等價意義. 雖然是這樣規(guī)定, 但是很多瀏覽器沒有這樣去設(shè)定.因此實(shí)際開發(fā)中, 我們建議, 盡量不要將未保留字符進(jìn)行百分號編碼, 防止不同的實(shí)現(xiàn)導(dǎo)致不同的結(jié)果.

其他不安全字符也需要百分號編碼

這些字符, 當(dāng)被直接放在URL中的時候, 能會引起解析程序的歧義。這些字符被視為不安全字符,原因有很多:

  • 空格: URL在傳輸?shù)倪^程,或者用戶在排版的過程,或者文本處理程序在處理URL的過程,都有可能引入無關(guān)緊要的空格,或者將那些有意義的空格給去掉。
  • 引號以及<>: 引號和尖括號通常用于在普通文本中起到分隔Url的作用
  • #: 通常用于表示書簽或者錨點(diǎn)
  • %: 百分號本身用作對不安全字符進(jìn)行編碼時使用的特殊字符,因此本身需要編碼
  • " {}|^[]`~ ": 某一些網(wǎng)關(guān)或者傳輸代理會篡改這些字符.

因此這些不安全的字符最好也進(jìn)行百分號編碼.

URI中百分號編碼的非標(biāo)準(zhǔn)實(shí)現(xiàn)

有一些不符合標(biāo)準(zhǔn)的把Unicode字符在URI中表示為: %uxxxx, 其中xxxx是用4個十六進(jìn)制數(shù)字表示的Unicode的碼位值。

任何RFC都沒有這樣的字符表示方法,并且已經(jīng)被W3C拒絕。第三版的ECMA-262仍然包含函數(shù)escape(string)使用這種語法, 但也有函數(shù)encodeURI(uri)轉(zhuǎn)換字符到UTF-8字節(jié)序列并用百分號編碼每個字節(jié)。

這里就涉及到 JS 的幾個百分號編碼函數(shù), 建議使用encodeURI(uri)

iOS中百分號編碼問題

在前序知識鋪墊以后. iOS里面如何處理百分號編碼的問題呢?

HTTP協(xié)議里面在URL中傳遞參數(shù),是在?后面使用key=value這種鍵值對方式, 如果有多個參數(shù)傳遞, 就需要用&進(jìn)行分割, 例如?key1=val1&key2=val2&key3=val3, 當(dāng)服務(wù)器收到請求以后, 會用&分割出每個key=value參數(shù), 然后用=分割出具體的鍵值.

現(xiàn)在如果在我們的參數(shù)key-value中就有=或者&怎么辦? 這樣后臺在解析參數(shù)的時候, 就會產(chǎn)生歧義. 因此解決方法就是對參數(shù)進(jìn)行百分號編碼!!!!

iOS 中,我們在請求的中經(jīng)常與百分號編碼相關(guān)的方法 -- stringByAddingPercentEscapesUsingEncoding:

Summary

Returns a representation of the receiver using a given encoding to determine the percent escapes necessary to convert the receiver into a legal URL string.
Declaration

- (NSString *)stringByAddingPercentEscapesUsingEncoding:(NSStringEncoding)enc;
Discussion

It may be difficult to use this function to "clean up" unescaped or partially escaped URL strings where sequences are unpredictable. See CFURLCreateStringByAddingPercentEscapes for more information.
Parameters

encoding    
The encoding to use for the returned string. If you are uncertain of the correct encoding you should use NSUTF8StringEncoding.
Returns

A representation of the receiver using encoding to determine the percent escapes necessary to convert the receiver into a legal URL string. Returns nil if encoding cannot encode a particular character.
Open in Developer Documentation

當(dāng)URL中有漢字時候, 用上面的方法, 會將漢字轉(zhuǎn)化成 unicode 編碼的結(jié)果, 但是對于復(fù)雜場景這個方法并不能滿足需求, 例如&符號:

NSString *queryWord = @"漢字&ss";
NSString *urlString = [NSString stringWithFormat:@"https://www.baidu.com/s?ie=UTF-8&wd=%@", queryWord];
NSString *escapedString = [urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSLog(@"%@", escapedString); // https://www.baidu.com/s?ie=UTF-8&wd=%E6%B1%89%E5%AD%97&ss

這個實(shí)例在開發(fā)中很常見(我們項(xiàng)目中是將某個人的昵稱當(dāng)做參數(shù)傳遞給后臺), 后臺在收到這種被轉(zhuǎn)義以后的URL取得的參數(shù)如下:

["ie": "UTF-8", "wd" : "漢字", "ss": nil]

即使我們做如下處理, 在請求前將每個參數(shù)都轉(zhuǎn)義, 再使用&拼接參數(shù)也無效:

NSString *queryWord = @"漢字&ss";
NSString *escapedQueryWord = [queryWord stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSString *urlString = [NSString stringWithFormat:@"https://www.baidu.com/s?ie=UTF-8&wd=%@", escapedQueryWord];
NSLog(@"%@", urlString); // https://www.baidu.com/s?ie=UTF-8&wd=%E6%B1%89%E5%AD%97&ss

這是因?yàn)?code>stringByAddingPercentEscapesUsingEncoding方法并不會對&字符進(jìn)行百分號編碼!!!!

iOS中正確的使用百分號編碼

如果要想自己控制哪些內(nèi)容被編碼, 哪些內(nèi)容不會被編碼, iOS提供了另外一個方法 -- stringByAddingPercentEncodingWithAllowedCharacters:.

這個方法會對字符串進(jìn)行更徹底的轉(zhuǎn)義,但是需要傳遞一個參數(shù): 這個參數(shù)是一個字符集,表示: 在進(jìn)行轉(zhuǎn)義過程中,不會對這個字符集中包含的字符進(jìn)行轉(zhuǎn)義, 而保持原樣保留下來。

NSString *queryWord = @"漢字&ss";
NSString *escapedQueryWord = [queryWord stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet letterCharacterSet]];
NSLog(@"%@", escapedQueryWord); // %E6%B1%89%E5%AD%97%26ss
NSString *urlString = [NSString stringWithFormat:@"https://www.baidu.com/s?ie=UTF-8&wd=%@", escapedQueryWord];
NSLog(@"%@", urlString); // https://www.baidu.com/s?ie=UTF-8&wd=%E6%B1%89%E5%AD%97%26ss

在上面的例子中傳遞參數(shù)[NSCharacterSet letterCharacterSet]來保證字母不被轉(zhuǎn)義。所以被轉(zhuǎn)義之后的參數(shù)值是:%E6%B1%89%E5%AD%97%26ss,這樣&就能夠正確被百分號編碼.

但是如果實(shí)際場景中, 可能出現(xiàn)如下情況:

https://www.baidu.com/s?person[contact]=13801001234&person[address]=北京&habit[]=游泳&habit[]=騎行

此時, 需要自己構(gòu)建 AllowedCharacters, 因?yàn)槠渲械?code>[和]是不需要轉(zhuǎn)意的.

NSMutableCharacterSet *mutableCharSet = [[NSMutableCharacterSet alloc] init];
[mutableCharSet addCharactersInString:@"[]"]; // 允許'['和']'不被轉(zhuǎn)義
NSCharacterSet *charSet = mutableCharSet.copy;

NSMutableString *mutableString = [NSMutableString string];
for (unit in queryString) {
    NSString *escapedField = [unit.field stringByAddingPercentEncodingWithAllowedCharacters:charSet];
    NSString *escapedValue = [unit.value stringByAddingPercentEncodingWithAllowedCharacters:charSet];
    [mutableString addFormat:@"%@=%@", escapedField, escapedValue];
}

準(zhǔn)確說, 步驟如下:

  1. 構(gòu)建AllowedCharactersNSCharacterSet
  2. 針對參數(shù)的k-v值, 進(jìn)行遍歷, 將針對keyvalue分別調(diào)用stringByAddingPercentEncodingWithAllowedCharacters進(jìn)行百分號編碼.
  3. @"?%@=%@&%@=%@"進(jìn)行kv參數(shù)拼接, 和不同參數(shù)的拼接.

AFNetworking中對百分號編碼的處理

對這種特定的字符進(jìn)行百分號編碼已經(jīng)能夠滿足基本需求, 但是如果我們傳遞的參數(shù)非常復(fù)雜, 我們應(yīng)該如何處理呢??

我們可以直接使用AFNetworking中的代碼, 實(shí)例如下:

NSDictionary *params = @{@"name": @"p&p",
                            @"nick_name": @"p&p[]@= =!",
                            @"father name": @"~!@#$%^&*(){}"
                            };
AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
NSMutableURLRequest *request = [serializer requestWithMethod:@"GET" URLString:@"https://www.baidu.com" parameters:params error:nil];
NSString *urlString = request.URL.absoluteString;
NSLog(@"%@", urlString);

//https://www.baidu.com?father%20name=~%21%40%23%24%25%5E%26%2A%28%29%7B%7D&name=p%26p&nick_name=p%26p%5B%5D%40%3D%20%3D%21

具體的處理方式, 建議參考AFNetworking的源碼, 或者參考文章iOS. PercentEscape是錯用的URLEncode,看看AFN和Facebook吧.

AFNetworking源碼以及編碼過程

關(guān)于AFNetworking中是如何做的:

FOUNDATION_EXPORT NSArray * AFQueryStringPairsFromDictionary(NSDictionary *dictionary);
FOUNDATION_EXPORT NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value);
FOUNDATION_EXPORT NSString * AFPercentEscapedStringFromString(NSString *string);


@interface AFQueryStringPair : NSObject
@property (readwrite, nonatomic, strong) id field;
@property (readwrite, nonatomic, strong) id value;

- (instancetype)initWithField:(id)field value:(id)value;
- (NSString *)URLEncodedStringValue;
@end

@implementation AFQueryStringPair
- (instancetype)initWithField:(id)field value:(id)value {
    self = [super init];
    if (!self) {
        return nil;
    }

    self.field = field;
    self.value = value;

    return self;
}

- (NSString *)URLEncodedStringValue {
    if (!self.value || [self.value isEqual:[NSNull null]]) {
        return AFPercentEscapedStringFromString([self.field description]);
    } else {
        return [NSString stringWithFormat:@"%@=%@", AFPercentEscapedStringFromString([self.field description]), AFPercentEscapedStringFromString([self.value description])];
    }
}


/**
 傳入 Dict -> 返回對應(yīng)的查詢的參數(shù)String

 @param parameters Dict內(nèi)部是 key-value
 @return 查詢的參數(shù)String
 */
NSString * AFQueryStringFromParameters(NSDictionary *parameters) {
    NSMutableArray *mutablePairs = [NSMutableArray array];
    // 生成一組 AFQueryStringPair 數(shù)組
    NSArray<AFQueryStringPair *> *pairs = AFQueryStringPairsFromDictionary(parameters);

    // 遍歷數(shù)組, 將每個 AFQueryStringPair 生成 "key=value", 并將結(jié)果String加入到結(jié)果數(shù)組
    for (AFQueryStringPair *pair in pairs) {
        // 將封裝的 StringPair 進(jìn)行 URLEncode 核心代碼
        [mutablePairs addObject:[pair URLEncodedStringValue]];
    }
    // 將結(jié)果數(shù)組中每個字符通過 "&" 字符鏈接, 輸出 Query 結(jié)果
    return [mutablePairs componentsJoinedByString:@"&"];
}


/**
 將dict 轉(zhuǎn)化成  NSArray<AFQueryStringPair *>
 */
NSArray<AFQueryStringPair *> * AFQueryStringPairsFromDictionary(NSDictionary *dictionary) {
    return AFQueryStringPairsFromKeyAndValue(nil, dictionary);
}

/**
 key - value 核心方法

 @param key key
 @param value value -- 可能是常用的集合類 -- NSDictionary, NSArray, NSSet,
                        以及非集合類 -- 普通的 key - value
 @return 返回NSArray<AFQueryStringPair *> *
 */
NSArray<AFQueryStringPair *> * AFQueryStringPairsFromKeyAndValue(NSString *key, id value) {
    NSMutableArray *mutableQueryStringComponents = [NSMutableArray array];

    //1. key-value 會重新排序 -- 升序進(jìn)行排序
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES selector:@selector(compare:)];

    //2. 根據(jù)當(dāng)前value內(nèi)容, 分辨進(jìn)行不同的處理. 實(shí)際場景中, 我們最常用的是 Dict - 內(nèi)部是key-value
    //   場景上來說

    /*
     2.1 value -> NSDictionary

     NSDictionary *dict = @{@"phone": @{@"mobile": @"xx", @"home": @"xx"};
     -> 會進(jìn)入第一個分支 - Dict分支
     phone[mobile]=xx&phone[home]=xx
     */
    if ([value isKindOfClass:[NSDictionary class]]) {
        NSDictionary *dictionary = value;
        // Sort dictionary keys to ensure consistent ordering in query string, which is important when deserializing potentially ambiguous sequences, such as an array of dictionaries
        // 先將dictionarys 內(nèi)容排序
        for (id nestedKey in [dictionary.allKeys sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
            id nestedValue = dictionary[nestedKey];
            if (nestedValue) {
                // 遞歸調(diào)用.
                [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)];
            }
        }

        /*
         2.2 value -> NSArray

         NSDictionary *dict = @{"members": @[@"pp", @"brownfeng"]};
         -> Array分支
         members[]=pp&members[]=brownfeng
         */
    } else if ([value isKindOfClass:[NSArray class]]) {
        NSArray *array = value;
        for (id nestedValue in array) {
            [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)];
        }
        /*
         2.3 value -> NSSet

         NSDictionary *dict = @{@"counts": [NSSet setWithObjects:@"1", @"2", nil]};
         -> NSSet分支
         counts=1&counts=2
         */
    } else if ([value isKindOfClass:[NSSet class]]) {
        NSSet *set = value;
        for (id obj in [set sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
            [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue(key, obj)];
        }
    } else {
        /*
         2.4 value -> NSString

         普通的 key-value類型. 直接生成 AFQueryStringPair

         NSDictionary *dict = @{@"name": @"pp"};
         -> 其他分支
         name=p
         */
        [mutableQueryStringComponents addObject:[[AFQueryStringPair alloc] initWithField:key value:value]];
    }

    return mutableQueryStringComponents;
}

/**
 百分號編碼的核心代碼!!!!!

 Returns a percent-escaped string following RFC 3986 for a query string key or value.
 RFC 3986 states that the following characters are "reserved" characters.
 - General Delimiters: ":", "#", "[", "]", "@", "?", "/"   -> 常見的分隔符
 - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="  -> 其他分隔符

 In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow
 query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/"
 should be percent-escaped in the query string.
 - parameter string: The string to be percent-escaped.
 - returns: The percent-escaped string.

 上面注釋寫的很清楚:

 "?"和"/"兩個符號在query必須進(jìn)行百分號編碼, 因?yàn)閝uery部分不允許包含URL!!!!
 */
NSString * AFPercentEscapedStringFromString(NSString *string) {
    // 需要被百分號編碼 - @":#[]@"
    static NSString * const kAFCharactersGeneralDelimitersToEncode = @":#[]@"; // does not include "?" or "/" due to RFC 3986 - Section 3.4
    // 需要被百分號編碼 - @"!$&'()*+,;="
    static NSString * const kAFCharactersSubDelimitersToEncode = @"!$&'()*+,;=";
    // "?", "/" - 沒有被百分號編碼!!!!

    // 1. 創(chuàng)建字符集 - URL Query 部分允許的字符集
    NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
    // 2. 從允許字符集中刪掉不允許的字符集, 因此編碼時候,
    [allowedCharacterSet removeCharactersInString:[kAFCharactersGeneralDelimitersToEncode stringByAppendingString:kAFCharactersSubDelimitersToEncode]];

    // 解決iOS7,8中可能導(dǎo)致的crash問題

    // FIXME: https://github.com/AFNetworking/AFNetworking/pull/3028
    // return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];

    static NSUInteger const batchSize = 50;

    NSUInteger index = 0;
    NSMutableString *escaped = @"".mutableCopy;

    while (index < string.length) {
        NSUInteger length = MIN(string.length - index, batchSize);
        NSRange range = NSMakeRange(index, length);

        // To avoid breaking up character sequences such as ????????
        range = [string rangeOfComposedCharacterSequencesForRange:range];

        NSString *substring = [string substringWithRange:range];
        NSString *encoded = [substring stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
        [escaped appendString:encoded];

        index += range.length;
    }

    return escaped;
}


因此如果比較復(fù)雜的query內(nèi)容比如如下:

{
    NSDictionary *params = @{
                            @"name": @"小A", // 標(biāo)準(zhǔn) key-value, 漢字需要被編碼
                            @"phone": @{@"mobile": @"xx", @"home": @"xx"}, // key - Dict
                            @"families": @[@"father", @"mother"], // key - Array
                            @"nums": [NSSet setWithObjects:@"1", @"2", nil], // key - set
                            @"does_not_include": @"/?", // 不會被編碼    (注意: OC中的"\\"才能表示"\")
                            @"space": @" ", //需要被編碼  (空格)
                            @"GeneralDelimitersToEncode": @":#[]@", // 需要完全被編碼
                            @"SubDelimitersToEncode": @"!$&'()*+,;=", // 需要完全被編碼
                            };
    AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
    NSMutableURLRequest *request = [serializer requestWithMethod:@"GET" URLString:@"https://www.baidu.com" parameters:params error:nil];
    NSString *urlString = request.URL.absoluteString;
    NSLog(@"%@", urlString);
    /*
https://www.baidu.com?GeneralDelimitersToEncode=%3A%23%5B%5D%40&SubDelimitersToEncode=%21%24%26%27%28%29%2A%2B%2C%3B%3D&does_not_include=/?&families%5B%5D=father&families%5B%5D=mother&name=%E5%B0%8FA&nums=1&nums=2&phone%5Bhome%5D=xx&phone%5Bmobile%5D=xx&space=%20

https://www.baidu.com?GeneralDelimitersToEncode=:#[]@&SubDelimitersToEncode=!$&'()*+,;=&does_not_include=/?&families[]=father&families[]=mother&name=小A&nums=1&nums=2&phone[home]=xx&phone[mobile]=xx&space=
     */
}

AFNetworking中的參數(shù)解析過程如下:

第一塊, key-value的模式

@{
    @"name": @"小A",
    @"phone": @{@"mobile": @"xx", @"home": @"xx"},
    @"families": @[@"father", @"mother"],
    @"nums": [NSSet setWithObjects:@"1", @"2", nil],
};
->
@[
     field: @"name", value: @"小A",
     field: @"phone[mobile]", value: @"xx",
     field: @"phone[home]", value: @"xx",
     field: @"families[]", value: @"father",
     field: @"families[]", value: @"mother",
     field: @"nums", value: @"1",
     field: @"nums", value: @"2",
]
->
name=%E5%B0%8FA&phone[mobile]=xx&phone[home]=xx&families[]=father&families[]=mother&nums=1&num=2

第二部分: 哪些內(nèi)容需要編碼

@{
    @"does_not_include": @"/?",
    @"space": @" ", 
    @"GeneralDelimitersToEncode": @":#[]@",
    @"SubDelimitersToEncode": @"!$&'()*+,;=",
};
->
@[
     field: @"does_not_include", value: @"/?",
     field: @"space", value: @" ",
     field: @"GeneralDelimitersToEncode", value: @":#[]@",
     field: @"SubDelimitersToEncode", value: @"!$&'()*+,;=",
]
->
https://www.baidu.com?GeneralDelimitersToEncode=%3A%23%5B%5D%40&SubDelimitersToEncode=%21%24%26%27%28%29%2A%2B%2C%3B%3D&does_not_include=/?&space=%20


-> URL Decode以后
https://www.baidu.com?GeneralDelimitersToEncode=:#[]@&SubDelimitersToEncode=!$&'()*+,;=&does_not_include=/?&space= 

參考文章

  1. 為什么要進(jìn)行URL編碼
  2. 阮一峰: 關(guān)于URL編碼
  3. wiki: 百分號編碼
  4. iOS. PercentEscape是錯用的URLEncode,看看AFN和Facebook吧
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內(nèi)容