JavaScript 如何正确处理 Unicode 编码问题!

JavaScript 处理 Unicode 的方式至少可以说是令人惊讶的。本文解释了 JavaScript 中的 处理 Unicode 相关的痛点,提供了常见问题的解决方案,并解释了ECMAScript 6 标准如何改进这种情况。

Unicode 基础知识

在深入研究 JavaScript 之前,先解释一下 Unicode 一些基础知识,这样在 Unicode 方面,我们至少都了解一些。

Unicode 是目前绝大多数程序使用的字符编码,定义也很简单,用一个 码位(code point) 映射一个字符。码位值的范围是从 U+0000 到 U+10FFFF,可以表示超过 110 万个字符。下面是一些字符与它们的码位。

A 的码位 U+0041

a 的码位 U+0061

© 的码位 U+00A9

☃ 的码位 U+2603

💩 的码位 U+1F4A9

码位 通常被格式化为十六进制数字,零填充至少四位数,格式为 U +前缀。

Unicode 最前面的 65536 个字符位,称为 基本多文种平面(BMP-—Basic Multilingual Plane),又简称为“零号平面”, plane 0),它的 码位 范围是从 U+0000 到 U+FFFF。最常见的字符都放在这个平面上,这是 Unicode 最先定义和公布的一个平面。

剩下的字符都放在 辅助平面(Supplementary Plane)或者 星形平面(astral planes) ,码位范围从U+010000 一直到 U+10FFFF,共 16 个辅助平面。

辅助平面内的码位很容易识别:如果需要超过 4 个十六进制数字来表示码位,那么它就是一个辅助平面内的码。

现在对 Unicode 有了基本的了解,接下来看看它如何应用于 JavaScript 字符串。

转义序列

在谷歌控制台输入如下:

>> '\x41\x42\x43'

'ABC'


>> '\x61\x62\x63'

'abc'

以下称为十六进制转义序列。它们由引用匹配码位的两个十六进制数字组成。例如,\x41 码位为U+0041 表示大写字母 A。这些转义序列可用于 U+0000 到 U+00FF 范围内的码位。

同样常见的还有以下类型的转义:

>> '\u0041\u0042\u0043'

'ABC'


>> 'I \u2661 JavaScript!'

'I ♡ JavaScript!'

这些被称为 Unicode转义序列。它们由表示码位的 4 个十六进制数字组成。例如,\u2661 表示码位为\U+2661 表示一个心。这些转义序列可以用于 U+0000 到 U+FFFF 范围内的码位,即整个基本平面。

但是其他的所有辅助平面呢? 我们需要 4 个以上的十六进制数字来表示它们的码位,那么如何转义它们呢?

在 ECMAScript 6中,这很简单,因为它引入了一种新的转义序列: Unicode 码位转义。例如:

>> '\u{41}\u{42}\u{43}'

'ABC'


>> '\u{1F4A9}'

'💩' // U+1F4A9 PILE OF POO

在大括号之间可以使用最多 6 个十六进制数字,这足以表示所有 Unicode 码位。因此,通过使用这种类型的转义序列,可以基于其代码位轻松转义任何 Unicode 码位。

为了向后兼容 ECMAScript 5 和更旧的环境,不幸的解决方案是使用代理对:

>> '\uD83D\uDCA9'

'💩' // U+1F4A9 PILE OF POO

在这种情况下,每个转义表示代理项一半的码位。两个代理项就组成一个辅助码位。

注意,代理项对码位与原始码位全不同。有公式可以根据给定的辅助码位来计算代理项对码位,反之亦然——根据代理对计算原始辅助代码位。

辅助平面(Supplementary Planes)中的码位,在 UTF-16 中被编码为一对16 比特长的码元(即32bit,4Bytes),称作代理对(surrogate pair),具体方法是:

码位减去 0x10000,得到的值的范围为 20 比特长的 0..0xFFFFF.

高位的 10 比特的值(值的范围为 0..0x3FF)被加上 0xD800 得到第一个码元或称作高位代理

低位的 10 比特的值(值的范围也是 0..0x3FF)被加上 0xDC00 得到第二个码元或称作低位代理(low surrogate),现在值的范围是 0xDC00..0xDFFF.

使用代理对,所有辅助平面中的码位(即从 U+010000 到 U+10FFFF )都可以表示,但是使用一个转义来表示基本平面的码位,以及使用两个转义来表示辅助平面中的码位,整个概念是令人困惑的,并且会产生许多恼人的后果。

使用 JavaScript 字符串方法来计算字符长度

例如,假设你想要计算给定字符串中的字符个数。你会怎么做呢?

首先想到可能是使用 length 属性。

>> 'A'.length // 码位: U+0041 表示 A

1


>> 'A' == '\u0041'

true


>> 'B'.length // 码位: U+0042 表示 B

1


>> 'B' == '\u0042'

true

在这些例子中,字符串的 length 属性恰好反映了字符的个数。这是有道理的:如果我们使用转义序列来表示字符,很明显,我们只需要对每个字符进行一次转义。但情况并非总是如此!这里有一个稍微不同的例子:

>> '𝐀'.length // 码位: U+1D400 表示 Math Bold 字体大写 A

2


>> '𝐀' == '\uD835\uDC00'

true


>> '𝐁'.length // 码位: U+1D401 表示 Math Bold 字体大写 B

2


>> '𝐁' == '\uD835\uDC01'

true


>> '💩'.length // U+1F4A9 PILE OF POO

2


>> '💩' == '\uD83D\uDCA9'

true

在内部,JavaScript 将辅助平面内的字符表示为代理对,并将单独的代理对部分开为单独的 “字符”。如果仅使用 ECMAScript 5 兼容转义序列来表示字符,将看到每个辅助平面内的字符都需要两个转义。这是令人困惑的,因为人们通常用 Unicode 字符或图形来代替。

计算辅助平面内的字符个数

回到这个问题:如何准确地计算 JavaScript 字符串中的字符个数 ? 诀窍就是如何正确地解析代理对,并且只将每对代理对作为一个字符计数。你可以这样使用:

var regexAstralSymbols = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;


function countSymbols(string) {

    return string

        // Replace every surrogate pair with a BMP symbol.

        .replace(regexAstralSymbols, '_')

        // …and *then* get the length.

        .length;

}

或者,如果你使用 Punycode.js,利用它的实用方法在 JavaScript 字符串和 Unicode 码位之间进行转换。decode 方法接受一个字符串并返回一个 Unicode 编码位数组;每个字符对应一项。

function countSymbols(string) {

    return punycode.ucs2.decode(string).length;

}

在 ES6 中,可以使用 Array.from 来做类似的事情,它使用字符串的迭代器将其拆分为一个字符串数组,每个字符串数组包含一个字符:

function countSymbols(string) {

    return Array.from(string).length;

}

或者,使用解构运算符 ... :

function countSymbols(string) {

    return [...string].length;

}

使用这些实现,我们现在可以正确地计算码位,这将导致更准确的结果:

>> countSymbols('A') // 码位:U+0041 表示 A

1


>> countSymbols('𝐀') // 码位: U+1D400 表示 Math Bold 字体大写 A

1


>> countSymbols('💩') // U+1F4A9 PILE OF POO

1

找撞脸

考虑一下这个例子:

>> 'mañana' == 'mañana'

false

JavaScript告诉我们,这些字符串是不同的,但视觉上,没有办法告诉我们!这是怎么回事?


JavaScript转义工具 会告诉你,原因如下:

>> 'ma\xF1ana' == 'man\u0303ana'

false


>> 'ma\xF1ana'.length

6


>> 'man\u0303ana'.length

7

第一个字符串包含码位 U+00F1 表示字母 n 和 n 头上波浪号,而第二个字符串使用两个单独的码位(U+006E表示字母 n 和 U+0303 表示波浪号)来创建相同的字符。这就解释了为什么它们的长度不同。

然而,如果我们想用我们习惯的方式来计算这些字符串中的字符个数,我们希望这两个字符串的长度都为 6,因为这是每个字符串中可视可区分的字符的个数。要怎样才能做到这一点呢?

在ECMAScript 6 中,解决方案相当简单:

function countSymbolsPedantically(string) {

    // Unicode Normalization, NFC form, to account for lookalikes:

    var normalized = string.normalize('NFC');

    // Account for astral symbols / surrogates, just like we did before:

    return punycode.ucs2.decode(normalized).length;

}

String.prototype 上的 normalize 方法执行 Unicode规范化,这解释了这些差异。 如果有一个码位表示与另一个码位后跟组合标记相同的字符,则会将其标准化为单个码位形式。

>> countSymbolsPedantically('mañana') // U+00F1

6

>> countSymbolsPedantically('mañana') // U+006E + U+0303

6

为了向后兼容 ECMAScript5 和旧环境,可以使用 String.prototype.normalize polyfill

计算其他组合标记

然而,上述方案仍然不是完美的——应用多个组合标记的码位总是导致单个可视字符,但可能没有 normalize 的形式,在这种情况下,normalize 是没有帮助。例如:

>> 'q\u0307\u0323'.normalize('NFC') // `q̣̇`

'q\u0307\u0323'


>> countSymbolsPedantically('q\u0307\u0323')

3 // not 1


>> countSymbolsPedantically('Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛Ǫ̵̹̻̝̳͂̌̌͘!͖̬̰̙̗̿̋ͥͥ̂ͣ̐́́͜͞')

74 // not 6

如果需要更精确的解决方案,可以使用正则表达式从输入字符串中删除任何组合标记。

//  将下面的正则表达式替换为经过转换的等效表达式,以使其在旧环境中工作


var regexSymbolWithCombiningMarks = /(\P{Mark})(\p{Mark}+)/gu;


function countSymbolsIgnoringCombiningMarks(string) {

    // 删除任何组合字符,只留下它们所属的字符:

    var stripped = string.replace(regexSymbolWithCombiningMarks, function($0, symbol, combiningMarks) {

        return symbol;

    });


    return punycode.ucs2.decode(stripped).length;

}

此函数删除任何组合标记,只留下它们所属的字符。任何不匹配的组合标记(在字符串开头)都保持不变。这个解决方案甚至可以在 ECMAScript3 环境中工作,并且它提供了迄今为止最准确的结果:

>> countSymbolsIgnoringCombiningMarks('q\u0307\u0323')

1

>> countSymbolsIgnoringCombiningMarks('Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛Ǫ̵̹̻̝̳͂̌̌͘!͖̬̰̙̗̿̋ͥͥ̂ͣ̐́́͜͞')

6

计算其他类型的图形集群

上面的算法仍然是一个简化—它还是无法正确计算像这样的字符:நி,汉语言由连体的 Jamo 组成,如 깍, 表情字符序列,如 👨‍👩‍👧‍👦 ((👨 U+200D + 👩 U+200D + 👧 + U+200D + 👦)或其他类似字符。


Unicode 文本分段上的 Unicode 标准附件#29 描述了用于确定字形簇边界的算法。 对于适用于所有 Unicode脚本的完全准确的解决方案,请在 JavaScript 中实现此算法,然后将每个字形集群计为单个字符。 有人建议将Intl.Segmenter(一种文本分段API)添加到ECMAScript中。


JavaScript 中字符串反转

下面是一个类似问题的示例:在JavaScript中反转字符串。这能有多难,对吧? 解决这个问题的一个常见的、非常简单的方法是:

function reverse(string) {

    return string.split('').reverse().join('');

}

它似乎在很多情况下都很有效:

>> reverse('abc')

'cba'


>> reverse('mañana') // U+00F1

'anañam'

然而,它完全打乱了包含组合标记或位于辅助平面字符的字符串。

>> reverse('mañana') // U+006E + U+0303

'anãnam' // note: the `~` is now applied to the `a` instead of the `n`


>> reverse('💩') // U+1F4A9

'��' // `'\uDCA9\uD83D'`, the surrogate pair for `💩` in the wrong order

要在 ES6 中正确反转位于辅助平面字符,字符串迭代器可以与 Array.from 结合使用:

function reverse(string) {

  return Array.from(string).reverse().join('');

}

但是,这仍然不能解决组合标记的问题。

幸运的是,一位名叫 Missy Elliot 的聪明的计算机科学家提出了一个防弹算法来解释这些问题。它看上去像这样:

我把丁字裤放下,翻转,然后倒过来。我把丁字裤放下,翻转,然后倒过来。

事实上:通过将任何组合标记的位置与它们所属的字符交换,以及在进一步处理字符串之前反转任何代理对,可以成功避免问题。

// 使用库 Esrever (https://mths.be/esrever)


>> esrever.reverse('mañana') // U+006E + U+0303

'anañam'


>> esrever.reverse('💩') // U+1F4A9

'💩' // U+1F4A9

字符串方法中的 Unicode 的问题

这种行为也会影响其他字符串方法。

将码位转转换为字符

String.fromCharCode 可以将一个码位转换为字符。 但它只适用于 BMP 范围内的码位 ( 即从 U+0000到U+FFFF)。如果将它用于转换超过 BMP 平面外的码位 ,将获得意想不到的结果。

>> String.fromCharCode(0x0041) // U+0041

'A' // U+0041


>> String.fromCharCode(0x1F4A9) // U+1F4A9

'' // U+F4A9, not U+1F4A9

唯一的解决方法是自己计算代理项一半的码位,并将它们作为单独的参数传递。

>> String.fromCharCode(0xD83D, 0xDCA9)

'💩' // U+1F4A9

如果不想计算代理项的一半,可以使用 Punycode.js 的实用方法:

>> punycode.ucs2.encode([ 0x1F4A9 ])

'💩' // U+1F4A9

幸运的是,ECMAScript 6 引入了 String.fromCodePoint(codePoint),它可以位于基本平面外的码位的字符。它可以用于任何 Unicode 编码点,即从 U+000000 到 U+10FFFF。

>> String.fromCodePoint(0x1F4A9) '💩' // U+1F4A9

为了向后兼容ECMAScript 5 和更旧的环境,使用 String.fromCodePoint() polyfill。

从字符串中获取字符

如果使用 String.prototype.charAt(position) 来检索包含字符串中的第一个字符,则只能获得第一个代理项而不是整个字符。

>> '💩'.charAt(0) // U+1F4A9

'\uD83D' // U+D83D, i.e. the first surrogate half for U+1F4A9

有人提议在 ECMAScript 7 中引入 String.prototype.at(position)。它类似于charAt,只不过它尽可能地处理完整的字符而不是代理项的一半。

>> '💩'.at(0) // U+1F4A9

'💩' // U+1F4A9

为了向后兼容 ECMAScript 5 和更旧的环境,可以使用 String.prototype.at() polyfill/prollyfill

从字符串中获取码位

类似地,如果使用 String.prototype.charCodeAt(position) 检索字符串中第一个字符的码位,将获得第一个代理项的码位,而不是 poo 字符堆的码位。

>> '💩'.charCodeAt(0)

0xD83D

幸运的是,ECMAScript 6 引入了 String.prototype.codePointAt(position),它类似于charCodeAt,只不过它尽可能处理完整的字符而不是代理项的一半。

>> '💩'.codePointAt(0)

0x1F4A9

为了向后兼容 ECMAScript 5 和更旧的环境,使用 String.prototype.codePointAt()_polyfill

遍历字符串中的所有字符

假设想要循环字符串中的每个字符,并对每个单独的字符执行一些操作。

在 ECMAScript 5 中,你必须编写大量的样板代码来判断代理对:

function getSymbols(string) {

    var index = 0;

    var length = string.length;

    var output = [];

    for (; index < length - 1; ++index) {

        var charCode = string.charCodeAt(index);

        if (charCode >= 0xD800 && charCode <= 0xDBFF) {

            charCode = string.charCodeAt(index + 1);

            if (charCode >= 0xDC00 && charCode <= 0xDFFF) {

                output.push(string.slice(index, index + 2));

                ++index;

                continue;

            }

        }

        output.push(string.charAt(index));

    }

    output.push(string.charAt(index));

    return output;

}


var symbols = getSymbols('💩');

symbols.forEach(function(symbol) {

    console.log(symbol == '💩');

});

或者可以使用正则表达式,如 var regexCodePoint = /[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDFFF]/g; 并迭代匹配

在 ECMAScript 6中,你可以简单地使用 for…of。字符串迭代器处理整个字符,而不是代理对。

for (const symbol of '💩') {

    console.log(symbol == '💩');

}

不幸的是,没有办法对它进行填充,因为 for…of 是一个语法级结构。

其他问题

此行为会影响几乎所有字符串方法,包括此处未明确提及的方法(如String.prototype.substring,String.prototype.slice 等),因此在使用它们时要小心。

正则表达式中的 Unicode 问题

匹配码位和 Unicode 标量值

正则表达式中的点运算符(.)只匹配一个“字符”, 但是由于JavaScript将代理半部分公开为单独的 “字符”,所以它永远不会匹配位于辅助平面上的字符。

>> /foo.bar/.test('foo💩bar')

false

让我们思考一下,我们可以使用什么正则表达式来匹配任何 Unicode字符? 什么好主意吗? 如下所示的,.这w个是不够的,因为它不匹配换行符或整个位于辅助平面上的字符。

>> /^.$/.test('💩')

false

为了正确匹配换行符,我们可以使用 [\s\S] 来代替,但这仍然不能匹配整个位于辅助平面上的字符。

>> /^[\s\S]$/.test('💩')

false

事实证明,匹配任何 Unicode 编码点的正则表达式一点也不简单:

>> /[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/.test('💩') // wtf

true

当然,你不希望手工编写这些正则表达式,更不用说调试它们了。为了生成像上面的一个正则表达式,可以使用了一个名为 Regenerate 的库,它可以根据码位或字符列表轻松地创建正则表达式:

>> regenerate().addRange(0x0, 0x10FFFF).toString()

'[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]'

从左到右,这个正则表达式匹配BMP字符、代理项对或单个代理项。

虽然在 JavaScript 字符串中技术上允许使用单独的代理,但是它们本身并不映射到任何字符,因此应该避免使用。术语 Unicode标量值 指除代理码位之外的所有码位。下面是一个正则表达式,它匹配任何 Unicode 标量值:

>> regenerate()

     .addRange(0x0, 0x10FFFF)     // all Unicode code points

     .removeRange(0xD800, 0xDBFF) // minus high surrogates

     .removeRange(0xDC00, 0xDFFF) // minus low surrogates

     .toRegExp()

/[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/

Regenerate 作为构建脚本的一部分使用的,用于创建复杂的正则表达式,同时仍然保持生成这些表达式的脚本的可读性和易于维护。

ECMAScript 6 为正则表达式引入一个 u 标志,它会使用 . 操作符匹配整个码位,而不是代理项的一半。

>> /foo.bar/.test('foo💩bar')

false


>> /foo.bar/u.test('foo💩bar')

true


注意 . 操作符仍然不会匹配换行符,设置 u 标志时,. 操作符等效于以下向后兼容的正则表达式模式:

>> regenerate()

     .addRange(0x0, 0x10FFFF) // all Unicode code points

     .remove(  // minus `LineTerminator`s (https://ecma-international.org/ecma-262/5.1/#sec-7.3):

       0x000A, // Line Feed <LF>

       0x000D, // Carriage Return <CR>

       0x2028, // Line Separator <LS>

       0x2029  // Paragraph Separator <PS>

     )

     .toString();

'[\0-\t\x0B\f\x0E-\u2027\u202A-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]'


>> /foo(?:[\0-\t\x0B\f\x0E-\u2027\u202A-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])bar/u.test('foo💩bar')

true

位于辅助平面码位上的字符

考虑到 /[a-c]/ 匹配任何字符从 码位为 U+0061 的字母 a 到 码位为 U+0063 的字母 c,似乎/[💩-💫]/ 会匹配码位 U+1F4A9 到码位 U+1F4AB,然而事实并非如此:

>> /[💩-💫]/

SyntaxError: Invalid regular expression: Range out of order in character class

发生这种情况的原因是,正则表达式等价于:

>> /[\uD83D\uDCA9-\uD83D\uDCAB]/ SyntaxError: Invalid regular expression: Range out of order in character class

事实证明,不像我们想的那样匹配码位 U+1F4A9 到码位 U+1F4AB,而是匹配正则表达式:

U+D83D(高代理位)

从 U+DCA9 到 U+D83D 的范围(无效,因为起始码位大于标记范围结束的码位)

U+DCAB(低代理位)

>> /[\uD83D\uDCA9-\uD83D\uDCAB]/u.test('\uD83D\uDCA9') // match U+1F4A9

true


>> /[\u{1F4A9}-\u{1F4AB}]/u.test('\u{1F4A9}') // match U+1F4A9

true


>> /[💩-💫]/u.test('💩') // match U+1F4A9

true


>> /[\uD83D\uDCA9-\uD83D\uDCAB]/u.test('\uD83D\uDCAA') // match U+1F4AA

true


>> /[\u{1F4A9}-\u{1F4AB}]/u.test('\u{1F4AA}') // match U+1F4AA

true


>> /[💩-💫]/u.test('💪') // match U+1F4AA

true


>> /[\uD83D\uDCA9-\uD83D\uDCAB]/u.test('\uD83D\uDCAB') // match U+1F4AB

true


>> /[\u{1F4A9}-\u{1F4AB}]/u.test('\u{1F4AB}') // match U+1F4AB

true


>> /[💩-💫]/u.test('💫') // match U+1F4AB

true

遗憾的是,这个解决方案不能向后兼容 ECMAScript 5 和更旧的环境。如果这是一个问题,应该使用 Regenerate 生成 es5兼容的正则表达式,处理辅助平面范围内的字符:

>> regenerate().addRange('💩', '💫')

'\uD83D[\uDCA9-\uDCAB]'


>> /^\uD83D[\uDCA9-\uDCAB]$/.test('💩') // match U+1F4A9

true


>> /^\uD83D[\uDCA9-\uDCAB]$/.test('💪') // match U+1F4AA

true


>> /^\uD83D[\uDCA9-\uDCAB]$/.test('💫') // match U+1F4AB

true

实战中的 bug 以及如何避免它们

这种行为会导致许多问题。例如,Twitter 每条 tweet 允许 140 个字符,而它们的后端并不介意它是什么类型的字符——是否为辅助平面内的字符。但由于JavaScript 计数在其网站上的某个时间点只是读出字符串的长度,而不考虑代理项对,因此不可能输入超过 70 个辅助平面内的字符。(这个bug已经修复。)

许多处理字符串的JavaScript库不能正确地解析辅助平面内的字符。

例如,Countable.js 它没有正确计算辅助平面内的字符。

Underscore.string 有一个 reverse 方法,它不处理组合标记或辅助平面内的字符。(改用 Missy Elliot的算法)

它还错误地解码辅助平面内的字符的 HTML 数字实体,例如 &#x1F4A9;。 许多其他 HTML 实体转换库也存在类似的问题。(在修复这些错误之前,请考虑使用 he 代替所有 HTML 编码/解码需求。)


需要上图完整资料或者更多Java全套视频的可以添加裙;712352051 小编不看大家技术怎么样 来的都欢迎,包括小编最近整理的一套资料都可以分享给大家的
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容