iOS URL编码(百分号编码)研究

URL编码(URL encoding)也称为百分号编码(Percent-encoding), 是特定上下文统一资源定位符(URI)的编码机制. 实际上也使用与统一资源标志符(URI)的编码.

对于URI, 具体的结构如下:

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

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

    |         |              |         |        |

  scheme     authority      path      query   fragment

我们能够看到:/?#[]@是用来分隔URI的不同的组件的.

混乱的URL编码

具体参考: 阮一峰: 关于URL编码.

URI的字符类型

URI所允许的字符分成保留未保留. 保留字符是那些具有特殊含义的字符. 例如, /字符用于URL不同部分的分节符(https://www.baidu.com/news). 未保留字符没有这些特殊含义. 百分号编码把保留字符表示为特殊字符序列, 根据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进制的数字, 然后在其前面放置转义字符("%"), 置入URI中的相应位置. (对于非ASCII字符, 需要转换为UTF-8字节序, 然后每个字节按照上述方式表示.)

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

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

在特定上下文中没有特殊含义的保留字符也可以被百分号编码, 在语义上与不百分号编码的该字符没有差别(这个特点非常重要, 如果我们不知道该字符是否需要被百分号编码, 那么最好用百分号编码一下).

在URI的查询部分(?字符后的部分)中, 例如/仍然是保留字符但是没有特殊含义, 除非一个特定的URI有其它规定. 该/字符在没有特殊含义时不需要百分号编码.例如https://www.baidu.com/news?name=p/p&age=13, 其中name=p/p中的/保留字符但是没有特殊含义, 在实际使用中可以不用给它进行百分号编码.

如果保留字符具有特殊含义, 那么该保留字符用百分号编码的URI与该保留字符仅用其自身表示的URI具有不同的语义.

URI中百分号编码未保留字符

未保留字符不需要百分号编码.

两个URI的差别如果仅在于未保留字符是用百分号编码还是字符本身表示, 那么这两个URI具有等价意义. 虽然是这样规定, 但是很多浏览器没有这样去设定.因此实际开发中, 我们建议, 尽量不要将未保留字符进行百分号编码, 防止不同的实现导致不同的结果.

其他不安全字符也需要百分号编码

这些字符, 当被直接放在URL中的时候, 能会引起解析程序的歧义。这些字符被视为不安全字符,原因有很多:

  • 空格: URL在传输的过程,或者用户在排版的过程,或者文本处理程序在处理URL的过程,都有可能引入无关紧要的空格,或者将那些有意义的空格给去掉。
  • 引号以及<>: 引号和尖括号通常用于在普通文本中起到分隔Url的作用
  • #: 通常用于表示书签或者锚点
  • %: 百分号本身用作对不安全字符进行编码时使用的特殊字符,因此本身需要编码
  • " {}|^[]`~ ": 某一些网关或者传输代理会篡改这些字符.

因此这些不安全的字符最好也进行百分号编码.

URI中百分号编码的非标准实现

有一些不符合标准的把Unicode字符在URI中表示为: %uxxxx, 其中xxxx是用4个十六进制数字表示的Unicode的码位值。

任何RFC都没有这样的字符表示方法,并且已经被W3C拒绝。第三版的ECMA-262仍然包含函数escape(string)使用这种语法, 但也有函数encodeURI(uri)转换字符到UTF-8字节序列并用百分号编码每个字节。

这里就涉及到 JS 的几个百分号编码函数, 建议使用encodeURI(uri)

iOS中百分号编码问题

在前序知识铺垫以后. iOS里面如何处理百分号编码的问题呢?

HTTP协议里面在URL中传递参数,是在?后面使用key=value这种键值对方式, 如果有多个参数传递, 就需要用&进行分割, 例如?key1=val1&key2=val2&key3=val3, 当服务器收到请求以后, 会用&分割出每个key=value参数, 然后用=分割出具体的键值.

现在如果在我们的参数key-value中就有=或者&怎么办? 这样后台在解析参数的时候, 就会产生歧义. 因此解决方法就是对参数进行百分号编码!!!!

iOS 中,我们在请求的中经常与百分号编码相关的方法 -- 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

当URL中有汉字时候, 用上面的方法, 会将汉字转化成 unicode 编码的结果, 但是对于复杂场景这个方法并不能满足需求, 例如&符号:

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

这个实例在开发中很常见(我们项目中是将某个人的昵称当做参数传递给后台), 后台在收到这种被转义以后的URL取得的参数如下:

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

即使我们做如下处理, 在请求前将每个参数都转义, 再使用&拼接参数也无效:

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

这是因为stringByAddingPercentEscapesUsingEncoding方法并不会对&字符进行百分号编码!!!!

iOS中正确的使用百分号编码

如果要想自己控制哪些内容被编码, 哪些内容不会被编码, iOS提供了另外一个方法 -- stringByAddingPercentEncodingWithAllowedCharacters:.

这个方法会对字符串进行更彻底的转义,但是需要传递一个参数: 这个参数是一个字符集,表示: 在进行转义过程中,不会对这个字符集中包含的字符进行转义, 而保持原样保留下来。

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

在上面的例子中传递参数[NSCharacterSet letterCharacterSet]来保证字母不被转义。所以被转义之后的参数值是:%E6%B1%89%E5%AD%97%26ss,这样&就能够正确被百分号编码.

但是如果实际场景中, 可能出现如下情况:

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

此时, 需要自己构建 AllowedCharacters, 因为其中的[]是不需要转意的.

NSMutableCharacterSet *mutableCharSet = [[NSMutableCharacterSet alloc] init];
[mutableCharSet addCharactersInString:@"[]"]; // 允许'['和']'不被转义
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];
}

准确说, 步骤如下:

  1. 构建AllowedCharactersNSCharacterSet
  2. 针对参数的k-v值, 进行遍历, 将针对keyvalue分别调用stringByAddingPercentEncodingWithAllowedCharacters进行百分号编码.
  3. @"?%@=%@&%@=%@"进行kv参数拼接, 和不同参数的拼接.

AFNetworking中对百分号编码的处理

对这种特定的字符进行百分号编码已经能够满足基本需求, 但是如果我们传递的参数非常复杂, 我们应该如何处理呢??

我们可以直接使用AFNetworking中的代码, 实例如下:

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源码以及编码过程

关于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 -> 返回对应的查询的参数String

 @param parameters Dict内部是 key-value
 @return 查询的参数String
 */
NSString * AFQueryStringFromParameters(NSDictionary *parameters) {
    NSMutableArray *mutablePairs = [NSMutableArray array];
    // 生成一组 AFQueryStringPair 数组
    NSArray<AFQueryStringPair *> *pairs = AFQueryStringPairsFromDictionary(parameters);

    // 遍历数组, 将每个 AFQueryStringPair 生成 "key=value", 并将结果String加入到结果数组
    for (AFQueryStringPair *pair in pairs) {
        // 将封装的 StringPair 进行 URLEncode 核心代码
        [mutablePairs addObject:[pair URLEncodedStringValue]];
    }
    // 将结果数组中每个字符通过 "&" 字符链接, 输出 Query 结果
    return [mutablePairs componentsJoinedByString:@"&"];
}


/**
 将dict 转化成  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 会重新排序 -- 升序进行排序
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES selector:@selector(compare:)];

    //2. 根据当前value内容, 分辨进行不同的处理. 实际场景中, 我们最常用的是 Dict - 内部是key-value
    //   场景上来说

    /*
     2.1 value -> NSDictionary

     NSDictionary *dict = @{@"phone": @{@"mobile": @"xx", @"home": @"xx"};
     -> 会进入第一个分支 - 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 内容排序
        for (id nestedKey in [dictionary.allKeys sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
            id nestedValue = dictionary[nestedKey];
            if (nestedValue) {
                // 递归调用.
                [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必须进行百分号编码, 因为query部分不允许包含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. 创建字符集 - URL Query 部分允许的字符集
    NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
    // 2. 从允许字符集中删掉不允许的字符集, 因此编码时候,
    [allowedCharacterSet removeCharactersInString:[kAFCharactersGeneralDelimitersToEncode stringByAppendingString:kAFCharactersSubDelimitersToEncode]];

    // 解决iOS7,8中可能导致的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;
}


因此如果比较复杂的query内容比如如下:

{
    NSDictionary *params = @{
                            @"name": @"小A", // 标准 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中的参数解析过程如下:

第一块, 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

第二部分: 哪些内容需要编码

@{
    @"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. 为什么要进行URL编码
  2. 阮一峰: 关于URL编码
  3. wiki: 百分号编码
  4. iOS. PercentEscape是错用的URLEncode,看看AFN和Facebook吧
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容