2.1.6 电子海图系统解析及开发 海图解析 -- 数据集内部结构 字段区

数据描述记录[DDR]

DDR的字段区(Field area)包含了各字段的相关信息,每一字段包含相应标签,以单元终止符(UT)为分隔的控制内容,最后以字段终止符(FT)作为字段的结束标志。通过查询S-57附录中的产品说明,即可知道字段各标签的具体含义。
字段间并不是扁平结构,而是存在上下级关系的树状结构,被存储在0000标签里,也被称为字段控制字段,树状结构的根节点为0001,是被S-57文件强制规定,表示该文件满足ISO/IEC 8211编码格式。而树状结构中每一节点的描述信息,则被存储在数据描述字段里。

字段控制字段

字段控制字段由0000标签开始,接下为五个字符;&□□□的控制部分,然后以UT分隔,接着是字段树状结构对信息,并以FT作为结束标志。

字段标识 0000 ;&□□□ UT 字段的树状结构对 FT

通过二进制解析实际文件,可清晰了解其字段控制字段的内容:

字段控制字段

由图中可知该文件字段结构对为:0001->DSID, DSID->DSSI, 0001->DSPM, 0001->VRID, VRID->ATTV, VRID->VRPT, VRID->SG2D, VRID->SG3D, 0001->FRID, FRID->FOID, FRID->ATTF, FRID->NATF, FRID->FFPT, FRID->FSPT。该结构对可还原为如下树状结构:

字段间的树状结构

了解了字段控制字段的数据格式后,新建类S57Tag类存储树状图:

    public class S57Tag
    {
        public string Tag;
        public S57Tag Parent;
        public List<S57Tag> Children;
    }

新建S57FieldControlField用以解析字段控制字段:

    public class S57FieldControlField
    {
        //根Tag为0001
        public S57Tag Tag { get; set; } = new S57Tag { Tag = "0001" };

        /// <summary>
        /// 字段控制字段的构造函数
        /// </summary>
        /// <param name="fieldControlSize">该字段的大小</param>
        /// <param name="tagSize">标识所占用的字符数,一般为4</param>
        public S57FieldControlField(int fieldControlSize, int tagSize)
        {
            // 初始的当前节点为根节点
            var curr = Tag;

            var controlField = BytesHelper.GetString(fieldControlSize);
            if (string.IsNullOrWhiteSpace(controlField)) return;

            // 字段区由0000开始,到第一个单元终止符结束
            var tag0000 = controlField.Substring(0, tagSize);
            var firstUT = controlField.IndexOf(Helper.UT);

            // 一次取一对字段标识
            for (int i = firstUT + 1; i < controlField.Length - tagSize * 2; i += tagSize * 2)
            {
                // 前tagSize个字符为父节点
                var tagParent = controlField.Substring(i, tagSize);
                // 后tagSize个字符为子节点
                var tagChild = controlField.Substring(i + tagSize, tagSize);

                // 当前节点与父节点不同时,往上回溯直到当前节点为父节点
                while (curr.Tag != tagParent)
                {
                    if (curr.Parent == null) break;
                    curr = curr.Parent;
                }

                 // 当前节点为父节点时,添加相应子节点,并将子节点设为当前节点
                if (tagParent == curr.Tag)
                {
                    var child = new S57Tag() { Tag = tagChild };
                    child.Parent = curr;

                    if (curr.Children == null) curr.Children = new List<S57Tag>();
                    curr.Children.Add(child);

                    curr = child;
                }
            }
        }
    }

添加验证代码如下:

    // 利用递归显示树状图
    public static void DisplayTag(S57Tag tag, int level)
    {
        if (level == 0)
        {
            Console.WriteLine(tag.Tag);
        }
        else
        {
            Console.WriteLine("".PadLeft(level*2) + "|-" + tag.Tag);
        }
            
        if(tag.Children != null && tag.Children.Count > 0)
        {
            foreach (var child in tag.Children)
            {
                DisplayTag(child, level + 1);
            }
        }
    }

    ......

    var tag0000 = dir.Items[0];
    var fcf = new S57FieldControlField(tag0000.FieldLength, dl.FieldTagSize);
    DisplayTag(fcf.Tag, 0);
字段控制字段解析验证结果

由验证结果可知,字段控制字段部分的代码解析正确。

数据描述字段

数据描述字段由字段控制、字段名、属性列表及格式控制列表四部分组成,并以FT作为结束标志,对应类S57DataDescriptiveField。全部数据描述字段对应类S57DataDescriptiveFields

DSID字段的数据描述字段
字段控制说明 字段名 UT 属性列表 UT 格式控制列表 FT
  • 字段控制说明由数据描述字段定义的数据字段的级别和数据类型,对应类S57FieldControl,其结构如下:

    起始位 长度 项目名 内容 示例中内容
    0 1 数据结构代码 “0”:单数据项
    “1”:线性结构
    “2”:多维结构
    “1”
    1 1 数据类型代码 “0”:字符串
    “1”:整型
    “5”:二进制形式
    “6”:混合类型
    “6”
    2 2 辅助控制码 “00” “00”
    4 2 可打印字符 “;&” “;&”
    6 3 截取转义序列 词汇级别0:□□□
    词汇级别1:-A□
    词汇级别2:%/A
    "□□□"

    不同的词汇级别,对应的UT和FT的长度不一样。

  • 字段名是字段标签相对应的名称,对应类中属性``````,示例中值为:Data set identification field

  • 属性列表:字符所包含的属性的列表,各属性之间用“!”分隔;对于0001标签,属性列表为空,但默认存在一个RCID属性。

    示例中值为:
    RCNM!RCID!EXPP!INTU!DSNM!EDTN!UPDN!UADT!ISDT!STED!PRSP!PSDN!PRED!PROF!AGEN!COMT,表示DSID标签中包含16个属性,每一四字符属性所表达的具体含义见S-57产品说明。
    若属性为重复属性(如地理位置中的经纬度),则以*号表示(*YCOO!XCOO)

  • 格式控制列表:存储属性列表相对应的格式,即字段存在多少属性,每一属性就有其相应的格式作为解析二进制文件的依据。

    示例中值为:
    (b11,b14,2b11,3A,2A(8),R(4),b11,2A,b11,b12,A),括号中格式依次为1个b11,1个b14,2个b11,3个A,2个A(8),1个R(4),1个b11,2个A,1个b11,1个b12,1个A,正好16个格式,与DSID的16个属性一一对应。

仿照数据描述字段的结构,新建如下类:

    // 数据描述字段区
    public class S57DataDescriptiveFields
    {
        public List<S57DataDescriptiveField> Fields;        //所有描述字段
    }

    // 数据描述字段
    public class S57DataDescriptiveField
    {
        public string Tag;                                  //字段标签
        public S57FieldControl FieldControl;                //字段控制说明
        public string FieldName = “”;                       //字段名称
        public List<S57FieldAttrFormat> FieldAttrFormats;   //字段属性格式列表
    }

    // 字段控制说明
    public class S57FieldControl
    {
        public string DataStructureCode;                    //数据结构代码
        public string DataTypeCode;                         //数据类型代码
        public string AuxiliaryCode;                        //辅助控制码
        public string PrintableGraphics;                    //可打印字符
        public string TruncatedEscapeSequence;              //截取转义字符

        public byte LexicalLevel
        {
            get
            {
                if (TruncatedEscapeSequence == "%/A") return 2;
                if (TruncatedEscapeSequence == "-A ") return 1;
                return 0;
            }
        }
    }

    // 字段属性及格式
    public class S57FieldAttrFormat
    {
        public string AttrName;                             //属性名
        public string Format;                               //基础格式, b1,b2,A,R等
        public int FormatLength;                            //格式的长度
        public bool IsRepeated;                             //是否为重复属性
    }

下面着手解析数据描述字段,补充相关代码:

        // S57DataDescriptiveFields构造函数
        public S57DataDescriptiveFields(S57Directory dir)
        {
            Fields = new List<S57DataDescriptiveField>();

            foreach (var item in dir.Items)
            {
                if (item.Tag == "0000") continue;

                Fields.Add(new S57DataDescriptiveField(item));
            }
        }

        // S57DataDescriptiveField构造函数
        public S57DataDescriptiveField(S57DirectoryItem item)
        {
            Tag = item.Tag;
            FieldAttrFormats = new List<S57FieldAttrFormat>();

            //解析字段控制说明
            FieldControl = new S57FieldControl();

            //解析字段名,直到遇到UT时结束
            var index = BytesHelper.Position;
            var chr = BytesHelper.GetChar();
            while(chr != Helper.UT)
            {
                FieldName += chr;
                chr = BytesHelper.GetChar();
            }

            //解析属性列表,直到遇到UT时结束
            var isRepeated = false;                         //默认重复属性为False
            var attrName = "";
            chr = BytesHelper.GetChar();
            while (chr != Helper.UT)
            {
                if(chr == '*') //重复属性
                {
                    isRepeated = true;
                    chr = BytesHelper.GetChar();
                    continue;
                }
                
                if(chr == '!') //属性之间以!号分隔
                {
                    FieldAttrFormats.Add(new S57FieldAttrFormat
                    {
                        AttrName = attrName,
                        IsRepeated = isRepeated
                    });

                    attrName = "";
                    chr = BytesHelper.GetChar();
                    continue;
                }
                
                attrName += chr;
                chr = BytesHelper.GetChar();
            }

            if (attrName != "") //最后一个属性
            {
                FieldAttrFormats.Add(new S57FieldAttrFormat
                {
                    AttrName = attrName,
                    IsRepeated = isRepeated
                });
            }

            //解析属性对应的格式,直到遇到FT时结束。并将结果拆分成基础格式与长度
            //基础格式包含:A, I, R, B, b1, b2, @
            //格式以包含在括号中,并以,号为分隔,相邻且相同的格式可缩写,如下:
            //(b11,b14,2b11,3A,2A(8),R(4),b11,2A,b11,b12,A)
            chr = BytesHelper.GetChar();
            var parenthesesNo = 0;
            var formatIndex = -1;    //当前的格式序号
            var repeatedNo = 0;     //格式的重复度
            var formatLength = 0;   //格式的长度
            var format = "";        //基础格式
            while (chr != Helper.FT)
            {
                if(chr == '(')
                {
                    parenthesesNo += 1;
                    chr = BytesHelper.GetChar();
                    continue;
                }
                if (chr == ')')
                {
                    parenthesesNo -= 1;
                    chr = BytesHelper.GetChar();
                    if(parenthesesNo > 0) continue;
                }
                if(chr == ',' ||
                   parenthesesNo == 0) //最后一个格式
                {
                    if(FieldAttrFormats.Count == 0) //存在0001标签属性列表为空的情况
                    {
                        FieldAttrFormats.Add(new S57FieldAttrFormat
                        {
                            AttrName = "RCID"
                        });
                    }

                    if (repeatedNo == 0) repeatedNo = 1;        //至少重复一次
                    if (format == "B") formatLength = formatLength / 8;
                    if (formatLength == 0) formatLength = -1;   //长度为-1,表明长度不定

                    for (int i = 0; i < repeatedNo; i++)
                    {
                        formatIndex++;
                        FieldAttrFormats[formatIndex].Format = format;
                        FieldAttrFormats[formatIndex].FormatLength = formatLength;
                    }

                    //重置已存储的数据
                    repeatedNo = 0;
                    formatLength = 0;
                    format = "";

                    if(chr != Helper.FT) chr = BytesHelper.GetChar();
                    continue;
                }

                if(chr >= 48 && chr <= 57) //数字,要么是长度,要么是重复度
                {
                    if(format == "") //重复度
                    {
                        repeatedNo = repeatedNo * 10 + chr - 48;
                    }
                    else //格式长度
                    {
                        formatLength = repeatedNo * 10 + chr - 48;
                    }
                }
                else //为字符
                {
                    if(chr == 'b')
                    {
                        format = chr.ToString() + BytesHelper.GetChar();
                    }
                    else
                    {
                        format = chr.ToString();
                    }
                }

                chr = BytesHelper.GetChar();
            }
        }

        // S57FieldControl构造函数
        public S57FieldControl()
        {
            DataStructureCode = BytesHelper.GetChar();
            DataTypeCode = BytesHelper.GetChar();
            AuxiliaryCode = BytesHelper.GetString(2);
            PrintableGraphics = BytesHelper.GetString(2);
            TruncatedEscapeSequence = BytesHelper.GetString(3);
        }

添加验证代码如下:

        var ddfs = new S57DataDescriptiveFields(dir);
        for (int i = 0; i < ddfs.Fields.Count; i++)
        {
            var f = ddfs.Fields[i];
            Console.WriteLine($"{i.ToString().PadLeft(2)} {f.Tag} {f.FieldControl.DataStructureCode}{f.FieldControl.DataTypeCode}{f.FieldControl.AuxiliaryCode}{f.FieldControl.PrintableGraphics}{f.FieldControl.TruncatedEscapeSequence}  {f.FieldName}");
            foreach (var af in f.FieldAttrFormats)
            {
                Console.WriteLine($"    |- {af.AttrName} {af.Format.PadLeft(2)} {af.FormatLength.ToString().PadLeft(3)} {af.IsRepeated}");
            }
        }

显示结果如下图,由图可知,标签DSID拥有16个属性,其中属性RCNM格式为b1(无符号整型),长度为1个字节,为非重复字段。

数据描述字段解析验证结果

数据记录[DR]

数据记录[DR]的字段区必须以DDR中定义的前序遍历顺序编码,其结构由DDR中的数据描述字段定义。DR的头标区目录区的解析过程与DDR一样。

顺序解析文件中的第一个DR:

        //DR 头标区
        Console.WriteLine();
        Console.WriteLine("DR 头标区");
        var drl = new S57Leader();
        var drheader = drl.RecordLength.ToString() + drl.FieldControlString
            + drl.FieldAreaBaseAddress.ToString() + drl.ExCharacterSetIndicator
            + drl.FieldLengthSize.ToString() + drl.FieldPositionSize.ToString()
            + drl.Reserved.ToString() + drl.FieldTagSize.ToString();
        Console.WriteLine(header);

        //DR 目录区
        Console.WriteLine();
        Console.WriteLine("DR 目录区");
        var drdir = new S57Directory(drl);
        foreach (var di in drdir.Items)
        {
            Console.WriteLine($"{di.Tag}\t{di.FieldLength}\t{di.FieldPosition}");
        }
image.png

上图显示,第一个DR的目录包含了0001、DSID和DSSI。可由DDR中的数据描述可知,0001包含1个属性,DSID包含16个属性,而DSSI包含11个属性。因此该DR字段区S57FieldArea分为三段,对应类S57Field

DSID字段区编码示例
  • 对应有固定长度的格式,直接读取二进制文件按其格式解析;
  • 而不定长度的格式,其以单元终止符UT分隔;
  • 对于重复字段,会循环出现标签中的属性值,直到遇到字段终止符FT;
  • 各子字段间以字段终止符FT分隔;
  • 不同词汇级别,终止终长度不一样。
属性 格式 长度 二进制数据 解析后结果
RCNM b1 1 0A 10
RCID b1 4 01 0000 00 1
EXPP b1 1 01 1
INTU b1 1 04 4
DSNM A -1 55 5334 414B 3749 4D2E 3030 301F US4AK7IM.000
EDTN A -1 311F 1
UPDN A -1 301F 0
UADT A 8 3230 3230 3034 3037 20200407
ISDT A 8 3230 3230 3034 3037 20200407
STED R 4 3033 2E31 3.1
PRSP b1 1 01 1
PSDN A -1 1F
PRED A -1 322E 301F 2.0
PROF b1 1 01 1
AGEN b1 2 26 02 500
COMT A -1 50 726F 6475 6365 6420
6279 204E 4F41 411F
Produced by NOAA

二进制可变长度格式中,1F代表UT,1E代表FT。

分析完其具体格式后,着手解析工作:

    public class S57FieldArea
    {
        public List<S57Field> Fields;

        public S57FieldArea(S57Directory dir, S57DataDescriptiveFields ddfs)
        {
            Fields = new List<S57Field>();

            foreach (var item in dir.Items)
            {
                var ddf = ddfs.Fields.First(x => x.Tag == item.Tag);
                Fields.Add(new S57Field(ddf, item.FieldLength));
            }
        }
    }

    public class S57Field
    {
        //标签名
        public string Tag;

        //属性
        public string[] Attrs;
        //属性对应的值
        public dynamic[] Values;

        public S57Field(S57DataDescriptiveField ddf, int fieldLength)
        {
            Tag = ddf.Tag;
            var attrsLen = ddf.FieldAttrFormats.Count;
            Attrs = new string[attrsLen];
            Values = new dynamic[attrsLen];

            //词汇级别2 意味着终止符长度为2,否则为1
            var teminatorLen = ddf.FieldControl.LexicalLevel == 2 ? 2 : 1;

            // 是否存在重复属性
            var isRepeated = ddf.FieldAttrFormats[0].IsRepeated;
            var repeatedNo = 1; //确定重复的次数
            if (isRepeated)
            {
                var oldPos = BytesHelper.Position;
                var lens = 0;
                repeatedNo = 0;
                while (lens < fieldLength-teminatorLen)
                {
                    repeatedNo++;
                    for (int i = 0; i < attrsLen; i++)
                    {
                        var af = ddf.FieldAttrFormats[i];
                        var flen = af.FormatLength;
                        
                        if (af.FormatLength == -1) //格式的长度不定,则终止符为单元终止符
                        {
                            flen = 0;
                            //根据终止符计算字段的长度
                            if (teminatorLen == 2)
                            {
                                while (!(BytesHelper.ENCBytes[oldPos + flen] == Helper.UT &&
                                         BytesHelper.ENCBytes[oldPos + flen + 1] == 0))
                                {
                                    flen++;
                                }
                            }
                            else
                            {
                                while (BytesHelper.ENCBytes[oldPos + flen] != Helper.UT)
                                {
                                    flen++;
                                }
                            }

                            flen += teminatorLen; //加上终止符长度
                        }

                        oldPos += flen;
                        lens += flen;
                    }
                }
            }
            
            for (int i = 0; i < attrsLen; i++)
            {
                Attrs[i] = ddf.FieldAttrFormats[i].AttrName;
                if (isRepeated)
                {
                    var type = ddf.FieldAttrFormats[i].Format;
                    var length = ddf.FieldAttrFormats[i].FormatLength;

                    switch (type)
                    {
                        case "b1":
                            switch (length)
                            {
                                case 1: Values[i] = new Byte[repeatedNo]; break;
                                case 2: Values[i] = new UInt16[repeatedNo]; break;
                                case 4: Values[i] = new UInt32[repeatedNo]; break;
                            }
                            break;
                        case "b2":
                            switch (length)
                            {
                                case 1: Values[i] = new SByte[repeatedNo]; break;
                                case 2: Values[i] = new Int16[repeatedNo]; break;
                                case 4: Values[i] = new Int32[repeatedNo]; break;
                            }
                            break;
                        case "B": Values[i] = new ulong[repeatedNo]; break;
                        case "I": Values[i] = new int[repeatedNo]; break;
                        case "R": Values[i] = new double[repeatedNo]; break;
                        case "@":
                        case "A": Values[i] = new string[repeatedNo]; break;
                    }
                    
                }
            }

            for (int j = 0; j < repeatedNo; j++)
            {
                for (int i = 0; i < attrsLen; i++)
                {
                    var af = ddf.FieldAttrFormats[i];
                    var flen = af.FormatLength;

                    if (af.FormatLength == -1) //格式的长度不定,则终止符为单元终止符
                    {
                        flen = 0;
                        //根据终止符计算字段的长度
                        if (teminatorLen == 2)
                        {
                            while (!(BytesHelper.ENCBytes[BytesHelper.Position + flen] == Helper.UT &&
                                     BytesHelper.ENCBytes[BytesHelper.Position + flen + 1] == 0))
                            {
                                flen++;
                            }
                        }
                        else
                        {
                            while (BytesHelper.ENCBytes[BytesHelper.Position + flen] != Helper.UT)
                            {
                                flen++;
                            }
                        }
                    }

                    if (isRepeated)
                    {
                        Values[i][j] = getSubFieldValue(af.Format, flen, ddf.FieldControl.LexicalLevel);
                    }
                    else
                    {
                        Values[i] = getSubFieldValue(af.Format, flen, ddf.FieldControl.LexicalLevel);
                    }

                    //跳过单元终止符
                    if (af.FormatLength == -1) BytesHelper.Position += teminatorLen;
                }
            }

            //最后跳过字段终止符
            BytesHelper.Position += teminatorLen;
        }

        private static dynamic getSubFieldValue(string type, int length, int lexicalLevel)
        {
            switch (type)
            {
                case "b1":
                    switch (length)
                    {
                        case 1:
                            return BytesHelper.GetByte();
                        case 2:
                            return BytesHelper.GetUInt16();
                        case 4:
                            return BytesHelper.GetUInt32();
                        default:
                            throw new Exception($"getSubFieldValue 解析出错[{type}][{length}]");
                    }
                case "b2":
                    switch (length)
                    {
                        case 1:
                            return BytesHelper.GetSByte();
                        case 2:
                            return BytesHelper.GetInt16();
                        case 4:
                            return BytesHelper.GetInt32();
                        default:
                            throw new Exception($"getSubFieldValue 解析出错[{type}][{length}]");
                    }
                case "A":
                    return BytesHelper.GetString(length, lexicalLevel);
                case "B":
                    return BytesHelper.GetBitStr(length);
                case "I":
                    return BytesHelper.GetInteger(length);
                case "R":
                    return BytesHelper.GetDouble(length);
                case "@":
                    return BytesHelper.GetString(length);
                default:
                    throw new Exception($"getSubFieldValue 解析出错[{type}][{length}]");
            }
        }
    }

为提高数据存储、读取效率,涉及到重复字段信息时都采用数组形式,而非列表形式存储。数组形式优点是效率高,缺点是事先需要知道数组长度,动态新增或删除元素时处理较为复杂(需添加额外代码)。此时,采用数组形式是为了程序处理效率,而牺牲了代码简洁度。

利用循环重复解析DR,添加验证代码如下:

        //DR 字段区
        Console.WriteLine();
        Console.WriteLine("DR 字段区");
        var fa = new S57FieldArea(drdir, ddfs);
        foreach (var f in fa.Fields)
        {
            Console.WriteLine($"{f.Tag} ");
            for (int i = 0; i < f.Attrs.Length; i++)
            {
                Console.Write($"  |- {f.Attrs[i]} ");
                if(f.Values[i] is IList)
                {
                    Console.WriteLine();
                    foreach (var val in f.Values[i])
                    {
                        Console.WriteLine($"      {val}");
                    }
                }
                else
                {
                    Console.WriteLine($"{f.Values[i]}");
                }
            }
        }

即可得解析结果如下:

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

推荐阅读更多精彩内容