UTF-8, UTF-16, UTF-32

参考文章

  1. http://unicode.org/faq/utf_bom.html
  2. 深入分析 Java 中的中文编码问题
  3. (wikipedia) Plane (Unicode)
  4. https://codepoints.net/
  5. (知乎) Unicode字符集中有哪些神奇的字符?
  6. Java Language Specification 的 3.1 小节
  7. (阮一峰) 字符编码笔记:ASCII,Unicode 和 UTF-8
  8. (MySQL 官方文档) 10.9.3 The utf8 Character Set (Alias for utf8mb3)
  9. Unicode 中有多少个 code point
  10. 👪 Families
  11. Unicode surrogate programming with the Java language
  12. (简书) Unicode和UTF-8、UTF-16、UTF-32

问题

  1. 什么是 Unicode?
  2. Unicode 中的 code point 与 Java 中的 char 有何关系?
  3. utf-8,utf-16,utf-32 是什么?
  4. 在 Java 中如何遍历 String 里的 code point?
  5. MySQL 中的 utf8 和 utf8mb4 的区别是什么?

重要名词

  1. planeBasic Multilingual Plane
  2. code pointcode unit
  3. high surrogatelow surrogate

问题1: 什么是 Unicode?

Unicode provides a unique number for every character,
no matter what the platform,
no matter what the program,
no matter what the language.

引自 https://www.unicode.org/standard/WhatIsUnicode.html

Unicode 中定义了一些自然数(包括0)和 code point 之间的映射关系

Unicode 中的平面(plane)

Unicode 标准中, 每个 plane65536code point 组成. 总共有 17plane, 编号从 016, Plane 16 里最后一个code pointU+10FFFF. Plane 0 被称为 Basic Multilingual Plane (BMP), 其中包含了最常用的 code point. Plane 1Plane 16 被称为 "supplementary planes".[1]

Unicode Planes

Unicode Planes

其中4个 plane 的分配情况(白底色表示未分配, 其他底色表示已分配)

  1. BMP(Basic Multilingual Plane, 即 Plane 0): U+0000..U+FFFF

    A map of the Basic Multilingual Plane. Each numbered box represents 256 code points.

  2. SMP(Supplementary Multilingual Plane, 即 Plane 1): U+10000..U+1FFFF

    A map of the Supplementary Multilingual Plane. Each numbered box represents 256 code points.

  3. SIP(Supplementary Ideographic Plane 即 Plane 2): U+20000..U+2FFFF

    A map of the Supplementary Ideographic Plane. Each numbered box represents 256 code points.

  4. SSP(Supplementary Special-purpose Plane, 即 Plane 14):U+E0000..U+EFFFF

    A map of the Supplementary Special-purpose Plane. Each numbered box represents 256 code points.

问题2: Unicode 中的 code point 与 Java 中的 char 有何关系?

The Unicode standard was originally designed as a fixed-width 16-bit character encoding. It has since been changed to allow for characters whose representation requires more than 16 bits. The range of legal code points is now U+0000 to U+10FFFF, using the hexadecimal U+n notation. Characters whose code points are greater than U+FFFF are called supplementary characters. To represent the complete range of characters using only 16-bit units, the Unicode standard defines an encoding called UTF-16. In this encoding, supplementary characters are represented as pairs of 16-bit code units, the first from the high-surrogates range, (U+D800 to U+DBFF), the second from the low-surrogates range (U+DC00 to U+DFFF). For characters in the range U+0000 to U+FFFF, the values of code points and UTF-16 code units are the same.

java 中的1个 char 相当于1个 code unit, 1个 code point 对应 1或2个 code unit, 具体如下

  1. code pointU+0000..U+D7FFU+E000..U+FFFF 范围内时, 用 1 个 code unit(或 1个Java中的 char)来表示这个 code point
  2. code pointU+10000..U+10FFFF 范围内时, 用 2 个 code unit(或 2个Java 中的 char)来表示这个 code point


如何用2个char来表示非BMP的code point:

  1. U+10000..U+10FFFF 范围内一共有 2^20 个 code point(0x10FFFF-0x10000+1=0x10000, 即2^20), 所以用20个bit可以区分这些 code point
  2. 高代理(high surrogate)有1024种可能取值, 低代理(low surrogate)也有1024种可能取值, 所以高代理和低代理组成的对(high, low)会有 1024 * 1024 种可能取值(而 1024 * 1024 = 2 ^ 20)
    高代理和低代理的位置
  3. 绿色边框的高代理区域共有1024个code point
  4. 蓝色边框的低代理区域共有1024个code point

code pointU+10000..U+10FFFF 范围内时, 对应的 2个code unit 的计算方法(伪代码)

delta = cp  - 0x10000 // 计算给定的 code point 和 0x10000 的差值

temp_high = (delta >> 10) // 取高10位
temp_low = (delta & 0x3FF) // 取低10位

high = temp_high + 0xD800 // 加上高代理的偏移量
low = temp_low + 0xDC00 // 加上低代理的偏移量

jdk 中高代理和低代理的计算

我们可以参考 Character.highSurrogate(int)Character.lowSurrogate(int) 的源码

高代理(high surrogate)的计算

Character.highSurrogate(int)

解释如下

public static char highSurrogate(int codePoint) {
    // MIN_HIGH_SURROGATE = '\uD800'
    // MIN_SUPPLEMENTARY_CODE_POINT = 0x10000
    // 计算步骤: 1. 计算差值; 2. 取差值高10个bit; 3. 加上高代理区域的偏移量
    // 计算步骤合在一起: ((codePoint - 0x10000) >>> 10) + 0xD800
    return (char) ((codePoint >>> 10)
            + (MIN_HIGH_SURROGATE - (MIN_SUPPLEMENTARY_CODE_POINT >>> 10)));
}

低代理(low surrogate)的计算

Character.lowSurrogate(int)

解释如下

public static char lowSurrogate(int codePoint) {
    // MIN_LOW_SURROGATE  = '\uDC00'
    // 由于 (codePoint - 0x10000) 的低10个bit和 codePoint 的低10个bit是一样的
    // 所以可以直接计算 codePoint 的低10个bit
    // 那么我们在取出 codePoint 的低10个bit后, 加上低代理区域的偏移量 0xDC00
    return (char) ((codePoint & 0x3ff) + MIN_LOW_SURROGATE);
}

如何向 StringBuilder append 一个 code point?

appendCodePoint

问题3: utf-8,utf-16,utf-32 是什么?

Q: What is a UTF?

A: A Unicode transformation format (UTF) is an algorithmic mapping from every Unicode code point (except surrogate code points) to a unique byte sequence. The ISO/IEC 10646 standard uses the term “UCS transformation format” for UTF; the two terms are merely synonyms for the same concept.

引自 https://www.unicode.org/faq/utf_bom.html
UTF 可以将 Unicode 中每个的 code point (除了U+D800..U+DFFF 范围内的 code point)映射为不同的字节序列

What are some of the differences between the UTFs?

图片来源
(wikipedia) BOM

  1. UTF-32BE
    code point 对应的整数用大端法的4个字节表示即可
    例如用 UTF-32BE 对 U+1F602 进行编码, 得到的字节序列为
0x00
0x01
0xF6
0x02



用 Java 来实现 UTF-32BE 编码并验证

package com.naive.wow;

import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Random;

public class NaiveUTF32 {
    public static void main(String[] args) throws UnsupportedEncodingException {
        Random random = new Random();

        for (int i = 0; i < 100; i++) {
            // 产生一个随机的 code point
            int cp = (int) (random.nextDouble() * 0x10FFFF);

            // U+D800..U+DFFF 上的 code point 不用验证
            if (cp >= 0xD800 && cp <= 0xDFFF) {
                continue;
            }

            String s = new String(new int[]{cp}, 0, 1);
            // bytes 中保存了正确的编码结果
            byte[] bytes = s.getBytes("UTF-32BE");

            // 将 cp 看成一个大端法表示的4字节数(不过 Java 中的 int 本来就是4字节的), 就可以得到其对应的 UTF-32BE 编码
            // calculated 中保存了我们自己计算的编码结果
            byte[] calculated = new byte[]{
                    0, // 第一个字节中的每一位都是 0
                    (byte) ((cp >> 16) & 0xFF), // 计算第二个字节
                    (byte) ((cp >> 8) & 0xFF), // 计算第三个字节
                    (byte) (cp & 0xFF), // 计算第四个字节
            };

            // 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
            assert Arrays.equals(calculated, bytes);
        }
    }
}
  1. UTF-16BE

举个例子
求 UTF-16BE 对 U+1F602 进行编码的结果
下面的计算过程是在 Python3 中生成的

>>> cp = 0x1f602
>>> delta = cp - 0x10000
>>> high = (delta >> 10) + 0xD800
>>> low = (delta & 0x3FF) + 0xDC00
>>> print(hex(high))
0xd83d
>>> print(hex(low))
0xde02
>>> 

所以对 U+1F602 的编码结果为

0xD8
0x3D
0xDE
0x02
package com.naive.wow;

import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Random;

public class NaiveUTF16 {
    public static void main(String[] ars) throws UnsupportedEncodingException {
        Random random = new Random();

        for (int i = 0; i < 100; i++) {
            // 产生一个随机的 code point
            int cp = (int) (random.nextDouble() * 0x10FFFF);

            // U+D800..U+DFFF 上的 code point 不用验证
            if (cp >= 0xD800 && cp <= 0xDFFF) {
                continue;
            }

            String s = new String(new int[]{cp}, 0, 1);
            // bytes 中保存了正确的编码结果
            byte[] bytes = s.getBytes("UTF-16BE");

            if (cp < 0x10000) {
                // 如果 cp 在 BMP 上, 则对应1个 code unit(也就是2个byte)
                byte[] calculated = new byte[]{
                        (byte) (cp >> 8), (byte) (cp & 0xFF)
                };

                // 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
                assert Arrays.equals(calculated, bytes);
            } else {
                // 如果 cp 不在 BMP 上, 则对应2个 code unit(也就是4个byte)
                int high = ((cp - 0x10000) >> 10) + 0xD800;
                int low = ((cp - 0x10000) & 0x3FF) + 0xDC00;

                byte[] calculated = new byte[]{
                        (byte) (high >> 8), (byte) (high & 0xFF),
                        (byte) (low >> 8), (byte) (low & 0xFF)
                };

                // 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
                assert Arrays.equals(calculated, bytes);
            }
        }
    }
}
  1. UTF-8
    utf-8的计算方法

    图片来源
    简述:
    a. 蓝色框中为起始的 code point
    b. 绿色框中为终止的 code point
    c. 红色框中为需要填写的 bit

举个例子
求 UTF-8 对 U+1F602 进行编码的结果
下面的计算过程是在 Python3 中生成的

>>> cp = 0x1F602
>>> b0 = 0b11110000 + ((cp >> 18) & 0b111)
>>> print(hex(b0))
0xf0
>>> b1 = 0b10000000 + ((cp >> 12) & 0b111111)
>>> print(hex(b1))
0x9f
>>> b2 = 0b10000000 + ((cp >> 6) & 0b111111)
>>> print(hex(b2))
0x98
>>> b3 = 0b10000000 + (cp & 0b111111)
>>> print(hex(b3))
0x82
>>> 

所以对 U+1F602 的编码结果为

0xF0
0x9F
0x98
0x82
package com.naive.wow;

import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Random;

public class NaiveUTF8 {
    public static void main(String[] args) throws UnsupportedEncodingException {
        Random random = new Random();

        for (int i = 0; i < 100; i++) {
            // 产生一个随机的 code point
            int cp = (int) (random.nextDouble() * 0x10FFFF);

            // U+D800..U+DFFF 上的 code point 不用验证
            if (cp >= 0xD800 && cp <= 0xDFFF) {
                continue;
            }

            String s = new String(new int[]{cp}, 0, 1);
            // bytes 中保存了正确的编码结果
            byte[] bytes = s.getBytes("UTF-8");

            if (cp < 0x80) {
                // U+0000..U+007F: 7个bit 可以表示. 二进制表示形如 0xxxxxxx
                byte[] calculated = new byte[]{
                        (byte) cp
                };

                // 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
                assert Arrays.equals(calculated, bytes);
            } else if (cp < 0x800) {
                // U+0080..U+07FF: 11个bit 可以表示. 二进制表示形如 110xxxxx 10xxxxxx
                byte[] calculated = new byte[]{
                        (byte) (0b11000000 + (cp >> 6)),
                        (byte) (0b10000000 + (cp & 0x3F))
                };

                // 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
                assert Arrays.equals(calculated, bytes);
            } else if (cp < 0x10000) {
                // U+0800..U+FFFF: 16个bit 可以表示. 二进制表示形如 1110xxxx 10xxxxxx 10xxxxxx
                byte[] calculated = new byte[]{
                        (byte) (0b11100000 + (cp >> 12)),
                        (byte) (0b10000000 + ((cp >> 6) & 0x3F)),
                        (byte) (0b10000000 + (cp & 0x3F))
                };

                // 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
                assert Arrays.equals(calculated, bytes);
            } else {
                // U+10000..U+10FFFF: 21个bit 可以表示. 二进制表示形如 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
                byte[] calculated = new byte[]{
                        (byte) (0b11110000 + (cp >> 18)),
                        (byte) (0b10000000 + ((cp >> 12) & 0x3F)),
                        (byte) (0b10000000 + ((cp >> 6) & 0x3F)),
                        (byte) (0b10000000 + (cp & 0x3F))
                };

                // 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
                assert Arrays.equals(calculated, bytes);
            }
        }
    }
}

问题4: 在 Java 中如何遍历 String 里的 code point?

方法1

public class Traverse {
    public static void main(String[] args) {
        // s 中的 code point 数量为 3
        // s 中的 char 数量为 5
        String s = "\uD83d\uDE02" + " " + "\uD83d\uDE02";

        int pos = 0;
        while (pos < s.length()) {
            int cp = s.codePointAt(pos);
            // 如果 cp 在 BMP, 则 pos += 1. 如果 cp 不在 BMP, 则 pos += 2
            pos += Character.isBmpCodePoint(cp) ? 1 : 2;
            System.out.println(Integer.toHexString(cp));
        }
    }
}

方法2 (Java 8 中支持)

public class Traverse {
    public static void main(String[] args) {
        String s = "\uD83d\uDE02" + " " + "\uD83d\uDE02";
        for (int cp : s.codePoints().toArray()) {
            System.out.println(Integer.toHexString(cp));
        }
    }
}

问题5: MySQL 中的 utf8 和 utf8mb4 的区别是什么?

可以参考(MySQL官网) Unicode Support的相关介绍

  1. MySQL 中的 utf8 Character Set 是 utf8mb3 Character Set 的别名, 仅支持 BMP 中的 code point (MySQL官网) The utf8mb3 Character Set (3-Byte UTF-8 Unicode Encoding)
  2. MySQL 中的 utf8mb4 Character Set 支持 BMP 和其他平面的 code point (MySQL官网) The utf8mb4 Character Set (4-Byte UTF-8 Unicode Encoding)

验证

  1. 建表(注意 CHARSET=utf8)
CREATE TABLE `Naive` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `name` varchar(4) NOT NULL COMMENT '名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
  1. 执行 insert 语句
insert into Naive(`name`)values("🐱");

执行后会看到报错


insert 时的报错
  1. 查看 utf-8 编码
    https://codepoints.net/🐱 查看 🐱 的 utf-8 编码, 与报错信息吻合
    对应的 utf-8 编码
  2. 建立支持非 BMP 字符的表
CREATE TABLE `Good` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `name` varchar(4) NOT NULL COMMENT '名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  1. 执行 insert 语句
insert into Good(`name`)values("🐱");
  1. 确认结果
    执行如下 select 语句
select * from Good;

结果为


select 语句执行结果
  1. more
    可以试试如下的两个 insert 语句
insert into Good(`name`)values("🐱🐱🐱🐱");
insert into Good(`name`)values("🐱🐱🐱🐱🐱");

彩蛋

如何查询不认识的字符

https://codepoints.net/ 可以查询字符。
例如若想查询 😂 这个 code point 的相关信息,可以访问https://codepoints.net/😂,会看到如下的信息(包括 utf-8/utf-16/utf-32 编码的结果以及各种语言中如何表示这个 code point)

相关信息

Plane 0 中的一些字符

: U+2800
可参见 codepoints 网站的相关描述
: U+2F27
可参见 codepoints 网站的相关描述
: U+4DC0
可参见 wikipedia 中的 Yijing Hexagram Symbols
: U+5DED
可参见 codepoints 网站的相关描述

Plane 1 中的一些字符

🀢: U+1F022 (位于 Plane 1)
可参见 codepoints 网站的相关描述
🂡: U+1F0A1 (位于 Plane 1)
可参见 codepoints 网站的相关描述
😂: U+1F602 (位于 Plane 1)
可参见 codepoints 网站的相关描述

Plane 2 中的一些字符

𠀋: U+2000B
可参见 codepoints 网站的相关描述
成: U+2F8B2
可参见 [codepoints 网站的相关描述]
(https://codepoints.net/U+2F8B2)
衣: U+2F9C4
可参见 [codepoints 网站的相关描述]
(https://codepoints.net/U+2F9C4)

其他

3个code point拼接在一起

ಠ_ಠ: 里面有3个code point(2个U+0CA0 ( ), 1个 U+005F(_))

这是1个code point吗?

👨‍👩‍👧‍👦: 由7个 code point组成(具体如下)

  1. 👨: U+1F468
  2. zero-width joiner (ZWJ): U+200D
  3. 👩: U+0x1F469
  4. zero-width joiner (ZWJ): U+200D
  5. 👧: U+1F467
  6. zero-width joiner (ZWJ): U+200D
  7. 👦: U+1F466

看起来一样?

👪: U+1F46A
👨‍👩‍👦: 由5个 code point组成(具体如下)

  1. 👨: U+1F468
  2. zero-width joiner (ZWJ): U+200D
  3. 👩: U+0x1F469
  4. zero-width joiner (ZWJ): U+200D
  5. 👦: U+1F466

不同肤色

👧(Girl): U+1F467
👧🏻(Girl: Light Skin Tone): U+1F467, U+1F3FB
👧🏼(Girl: Medium-Light Skin Tone): U+1F467, U+1F3FC
👧🏽(Girl: Medium Skin Tone): U+1F467, U+1F3FD
👧🏾(Girl: Medium-Dark Skin Tone): U+1F467, U+1F3FE
👧🏿(Girl: Dark Skin Tone): U+1F467, U+1F3FF

它们一样吗?

与汉字相似的一些code point

  1. -: U+002D
  2. ˗: U+02D7
  3. : U+2010
  4. : U+2012
  5. : U+2013
  6. : U+2014
  7. : U+2015
    来源

a相似的一些 code point

  1. : U+FF41
  2. 𝐚: U+1D41A
  3. 𝑎: U+1D44E
  4. 𝒂: U+1D482
  5. 𝖺: U+1D5BA
  6. 𝗮: U+1D5EE
  7. 𝘢: U+1D622
    来源

Flags

🇨🇳(China): U+1F1E8, U+1F1F3
🇭🇰(Hong Kong SAR China): U+1F1ED, U+1F1F0
🇲🇴(Macau SAR China): U+1F1F2, U+1F1F4
🇺🇸(United States): U+1F1FA, U+1F1F8

可以用以下26个 code point 来组成 flag
U+1F1E6..U+1F1FF

🇦:U+1F1E6
🇧:U+1F1E7
🇨:U+1F1E8
🇩:U+1F1E9
🇪:U+1F1EA
🇫:U+1F1EB
🇬:U+1F1EC
🇭:U+1F1ED
🇮:U+1F1EE
🇯:U+1F1EF
🇰:U+1F1F0
🇱:U+1F1F1
🇲:U+1F1F2
🇳:U+1F1F3
🇴:U+1F1F4
🇵:U+1F1F5
🇶:U+1F1F6
🇷:U+1F1F7
🇸:U+1F1F8
🇹:U+1F1F9
🇺:U+1F1FA
🇻:U+1F1FB
🇼:U+1F1FC
🇽:U+1F1FD
🇾:U+1F1FE
🇿:U+1F1FF

例如中国为 cn, 将上面的 🇨🇳 放在一起就可以看到 🇨🇳

动手实战

查看一个字符的 utf-8 编码对应的字节序列

除了可以在 https://codepoints.net/ 上查询外, 也可以自己用 Python3 的程序来做到
下面是 utf8.py

#!/usr/local/bin/python3

import sys

f = open('result', 'wb')
f.write(sys.argv[1].encode('utf-8'))
f.close()

我们在命令行执行

./utf8.py '😂'

后, 😂 在 utf-8 编码下的对应的字节序列就会输出到 名为 result 的文件中
然后在命令行用od命令可以查看其中的内容(具体如下)

od -t x1 result
查看内容

查看一个字符的 utf-16/utf-32 编码对应的字节序列

而查看 utf-16/utf-32 编码对应的字节序列也是类似的. 下面是对应的 Python3 程序

utf16be.py
#!/usr/local/bin/python3

import sys

f = open('result', 'wb')
f.write(sys.argv[1].encode('utf-16be'))
f.close()

utf32be.py
#!/usr/local/bin/python3

import sys

f = open('result', 'wb')
f.write(sys.argv[1].encode('utf-32be'))
f.close()

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

推荐阅读更多精彩内容

  • 从这里开始,从这里散场。 同样的角度,那年冬天有雪。 看着背影,自己还是很高的 我没去过的景山公园,你们陪我又走一...
    斌心依旧阅读 338评论 0 0
  • 刚毕业那会儿,找一个合适的住处真的是难于上青天啊!由于汉子这个属性,很多合适的房子都因为【仅限女生】而把我拒之门外...
    运营狮训练营阅读 1,283评论 0 0
  • 电影围绕关系、如何解决问题展开,接地气、生活化,为我们家庭生活关系相处提出了警醒。女主人公李宝莉,勤快、泼辣、勇敢...
    云朵儿_e6cb阅读 343评论 0 0