精华内容
下载资源
问答
  • 2018-05-04 11:00:08

    cifar-10 数据集是机器学习入门第二个使用到的数据集合(第一个当然是MNIST),下面介绍一下如何解析。

    1. cifar-10 简介

    该数据集共有60000张彩色图像,图像大小是3通道的32*32,分为10个类,每类6000张图。这里面有50000张用于训练,构成了5个训练批,每一批10000张图;另外10000用于测试,单独构成一批。测试批的数据里,取自10类中的每一类,每一类随机取1000张。抽剩下的就随机排列组成了训练批。注意一个训练批中的各类图像并不一定数量相同,总的来看训练批,每一类都有5000张图。
    数据的下载:共有三个版本,python,matlab,binary version 适用于C语言
    python: http://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz

    MATLAB: http://www.cs.toronto.edu/~kriz/cifar-10-matlab.tar.gz

    bin: http://www.cs.toronto.edu/~kriz/cifar-10-binary.tar.gz

    2. 数据解析,Python为例

    cifar-10 数据以字典的形式存储,key为:dict_keys([b’batch_label’, b’labels’, b’data’, b’filenames’]),也就是说包含四个内容: batch_label,标签,图像数据,图片对应的文件名。通过一下函数加载:

    def unPickle(file):
        import pickle as pk
        with open(file, 'rb') as f:
        d = pk.load(f, encoding='bytes')
        return d

    得到数据:

    data = unPickle('data_batch_1')
    img = data[b'data']
    img.shape  # 显示为(100003072

    由于是3通道32*32的彩色图像,故每个图像大小为32*32*3=3072

    3. 重建图像

    将数组重建为彩色图像:

    img_0 = img[0] #得到第一张图像
    img_reshape = img_0.reshape(3,32,32)
    import PIL.Image as image
    import matplotlib.pyplot as plt
    r = image.fromarray(img_reshape[0]).convert('L')
    g = image.fromarray(img_reshape[1]).convert('L')
    b = image.fromarray(img_reshape[2]).convert('L')
    img_m = image.merge('RGB',(r,g,b))
    plt.imshow(img_m)
    plt.show()

    重要说明

    原数据是以数组存储,在应用中需要数据为什么形式(数据还是rgb图像)根据需求决定。

    更多相关内容
  • TS数据流的解析

    千次阅读 2022-03-07 13:38:10
    TS数据流的解析 文档修改信息 1 TS 数据流简介 1.1 TS流的构成 1.2 TS流的产生 1.3 TS流的格式 1.4 参考文档 1.5 小结 2 TS数据流包头的解析 2.1 解析步骤 2.2 包长解析 2.3 PID解析 2.4 负载单元开始标志...

    TS数据流的解析

    文档修改信息

    版本作者时间备注
    1.0李海杰2022-03-07搬运到CSDN并修改相关数据

    1 TS 数据流简介

    ​ 简单来说,数字机顶盒是能够接收到一段段的码流,这样的码流我们可以称之为TS(Transport Stream,传输流),每个TS流里都携带的有一些信息,例如Video、Audio、PAT、PMT等。

    1.1 TS 流的构成

    ​ TS 流是一种用于数据传输的单一传输流,它是由一些具有共同时间基准或独立时间基准的一个或多个PES复合而成的。

    在这里我们需要知道一些其他数据流:

    • ES流(Elementary Stream) 基本码流,不分段的音频、视频或其他信息的连续码流。
    • PES流 把基本流ES分割成段,并加上相应头文件打包成形的打包基本码流。
    • PS流(Program Stream) 节目流,将具有共同时间基准的一个或多个PES组合(复合)而成的单一数据流(用于播放或编辑系统,如m2p)。

    1.2 TS 流的产生

    TS流的产生

    • 从上图可以看出,视频ES和音频ES通过打包器和共同或独立的系统时间基准形成一个个PES,通过TS复用器复用形成的传输流。注意这里的TS流是位流格式,也就是说TS流是可以按位读取的。
      PES和TS关系.png

    • 在实际传输中,PES包会被切割后装载到TS包中,上图就是PES和TS之间的关系图。

    注意

    1. 一个PES包可以装载到不同的TS包。
    2. 每一个TS包必须只含有从一个PES来的数据。
    3. PES包头必须跟在TS包的链接头后面。
    4. 对于一个特定的PES,最后一个TS包可以含有填充比特。

    1.3 TS 流的格式

    ​ TS流是基于Packet的位流格式,每个包的长度是固定的188个字节(或者204个字节,原因是在188个字节后加上了16个字节的CRC校验数据,其他格式一样)。整个TS流组成形式如下:

    TS包格式.png

    1. 包头信息
    Packet Header(包头)信息说明标识位数说明
    1sync_byte8bits同步字节
    2transport_error_indicator1bit错误指示信息(1:该包至少有1bits传输错误)
    3payload_unit_start_indicator1bit负载单元开始标志(packet不满188字节时需填充)
    4transport_priority1bit传输优先级标志(1:优先级高)
    5PID13bitPacket ID号码,唯一的号码对应不同的包
    6transport_scrambling_control2bits加密标志(00:未加密;其他表示已加密)
    7adaptation_field_control2bits附加区域控制
    8continuity_counter4bits包递增计数器
    • 上面表格是一个包(Package)的头(Header)的说明,其中需要注意的是:PID是TS流中唯一识别标志,Packet Data是什么内容就是由PID决定的。 如果一个TS流中的一个Packet的Packet Header中的PID是0x0000,那么这个Packet的Packet Data就是DVB的PAT表而非其他类型数据(如Video、Audio或其他业务信息)。
    TS流中PID的分配
    PID值说明
    PAT0x0000-
    CAT0x0001-
    TSDT0x0002-
    预留0x0003 至0x000F
    NIT, ST0x0010-
    SDT , BAT, ST0x0011-
    EIT, ST0x0012-
    RST, ST0x0013-
    TDT, TOT, ST0x0014-
    网络同步0x0015
    预留使用0x0016 至 0x001B
    带内信令0x001C
    DIT0x001E
    SIT0x001F
    • 上面是一些表的PID值,这些值是固定的,不允许更改
    1. 包数据结构
    • TS流的包数据是承载实际数据的区域,根据不同的PID,有不同的解析方式。

    1.4 参考文档

    1.5 小结

    • TS流是一种位流(当然就是数字的), 它是由ES流分割成PES后复用而成的;它经过网络传输被机顶盒接收到; 数字电视机顶盒接收到TS流后将解析TS流。

    • TS流是由一个个Packet(包)构成的, 每个包都是由Packet Header(包头)和Packet Data(包数据)组成的。 其中Packet Header指示了该Packet是什么属性的,并给出了该Packet Data的数据的唯一网络标识符PID。

    • TS流和PS流的区别:TS流的包结构是长度是固定的;PS流的包结构是可变长度的。 这导致了 TS流的抵抗传输误码的能力强于PS流 (TS码流由于采用了固定长度的包结构, 当传输误码破坏了某一TS包的同步信息时,接收机可在固定的位置检测它后面包中的同步信息,从而恢复同步,避免了信息丢失。 而PS包由于长度是变化的,一旦某一 PS包的同步信息丢失, 接收机无法确定下一包的同步位置,就会造成失步,导致严重的信息丢失。 因此,在信道环境较为恶劣,传输误码较高时,一般采用TS码流;而在信道环境较好,传输误码较低时,一般采用PS码流)。由于TS码流具有较强的抵抗传输误码的能力,因此目前在传输媒体中进行传输的MPEG-2码流基本上都采用了TS码流的包格。

    2 TS数据流包头的解析

    ​ 上面我们对TS数据流进行了简单介绍,了解到了TS数据流的基本结构和组成部分,那么我们就可以着手对TS数据流的解析工作。

    2.1 解析步骤

    ​ 在对TS数据流包头进行分析的时候我们主要获取的信息有三点:数据包长度PID值负载单元开始标志。所以我们可以按下面步骤来进行初步解析:

    1. 获取TS数据流包长度信息。
    2. 得到TS数据流包头PID信息。
    3. 确定每个数据包的负载单元开始标志。

    2.2 包长解析

    ​ 我们清楚,TS数据包长度是固定的,188字节或者204字节。那么我们在对数据包长度进行解析时,就只需获取一段完整的TS数据包,然后来判断它是188字节还是204字节。在这里我是采用if语句来进行判断。

    ​ 首先第一步获取一段完整的TS数据包。因为每一段TS数据包的包头第一个字节都是0x47,那么我们只用找到这个标志位就能开始下一步判断。

    int ParseLen(FILE *pTsFile, int TsPosition)
    {
        int PackageLength = 0;
    
        while(fgetc(pTsFile) != EOF)
        {
            if(fgetc(pTsFile) == 0x47)
            {
                if(JudgmentPackageLength(pTsFile, TsPosition) == 204)
                {
                    PackageLength = 204;
                    break;
                }
    
                if(JudgmentPackageLength(pTsFile, TsPosition) == 188)
                {
                    PackageLength = 188;
                    break;
                }
            }
        }
    
        return PackageLength;
    }
    
    • 我们先通过fopen()函数来打开TS文件,并保存文件流指针。然后用fgetc()函数来读取文件中的每个字节数据,再加上while()循环语句来实现遍历读取。当我们遇到为0x47的数据时,就调用一个判断函数来进行下一步的判断。
     for(Time = 0; Time < 10; Time++)
        {
            if(fseek(pTsFile, TsPosition - 1, SEEK_CUR) == -1)
            {   
                printf("fseek error!");
    
                return value;
            }
    
            if(feof(pTsFile))
            {
                value = -1;
    
                return value;
            }
    
            fseek(pTsFile,188,SEEK_CUR);
            PackageByte = fgetc(pTsFile);
    
            if(PackageByte == 0x47)
            {
                value = 188;
            }
            else
            {
                fseek(pTsFile,203,SEEK_CUR);
                PackageByte = fgetc(pTsFile);
                
                if(PackageByte == 0x47)
                {
                    value = 204;
                }
                else
                {
                    value = -1;
                }
            }
        }
    
    • 在判断函数中我是采用从当前0x47位置向后偏移188字节或者204字节的方式来判断下一段数据是否为0x47。如果是,说明是一段完整数据包;如果不是,说明我们并未找到包头,那么继续遍历。
    • 在这里我们为了排除偶然情况,我们可以选择利用for()函数进行一定次数的循环,往后多移动几次来确定所获取的的确是一段完整的TS数据包。
    • 通过这几步骤的循环判断,我们就可解析出我们的数据包长度是188字节还是204字节。

    2.3 PID解析

    ​ 当我们解析出一段完整的数据包后就可以利用一个数组来对这一段完整的数据进行保存,方便我们后面的解析。对包头解析时我们需要知道我们的包头结构,每个字节代表什么含义,每个数据有占有多少字节位数。下面就是我们的包头结构表:

    Packet Header(包头)信息说明标识位数说明
    1sync_byte8bits同步字节
    2transport_error_indicator1bit错误指示信息(1:该包至少有1bits传输错误)
    3payload_unit_start_indicator1bit负载单元开始标志(packet不满188字节时需填充)
    4transport_priority1bit传输优先级标志(1:优先级高)
    5PID13bitPacket ID号码,唯一的号码对应不同的包
    6transport_scrambling_control2bits加密标志(00:未加密;其他表示已加密)
    7adaptation_field_control2bits附加区域控制
    8continuity_counter4bits包递增计数器

    ​ 我们可以看出我们所想获取到的PID是包头的第2个字节的5bits加上第3个字节的8bits,那么我们可以先创建一个结构体,来对包头数据进行存储。

    typedef struct TS_Package_Head
    {
        unsigned sync_byte                          :8;
        unsigned transport_error_indicator          :1;
        unsigned payload_unit_start_indicator       :1;
        unsigned transport_priority                 :1;
        unsigned PID                                :13;
        unsigned transport_scrambling_control       :2;
        unsigned adaptation_field_control           :2;
        unsigned continuity_counter                 :4;
    }TS_Package_Head;
    
    • 因为我们知道,TS数据包的包头是固定4个字节。那么我们就将之前所保存的数据包数据取出前四个字节来放入结构体中。这样一来我们就能够通过结构体来获取到我们该段数据包的PID数据。
    TS_Package_Head ParseTS_PackageHead(TS_Package_Head TS_PackageHead, unsigned char PackageHead_data[4])
    {
    	TS_PackageHead.sync_byte = PackageHead_data[0];
    	TS_PackageHead.transport_error_indicator = PackageHead_data[1] >> 7;
    	TS_PackageHead.payload_unit_start_indicator = (PackageHead_data[1] >> 6) & 0x01;
    	TS_PackageHead.transport_priority = (PackageHead_data[1] >> 5) & 0x01;
    	TS_PackageHead.PID = ((PackageHead_data[1] & 0x1F) << 8) | PackageHead_data[2];
    	TS_PackageHead.transport_scrambling_control = PackageHead_data[3] >> 6;
    	TS_PackageHead.adaptation_field_control = (PackageHead_data[3] >> 4) & 0x03;
    	TS_PackageHead.continuity_counter = PackageHead_data[3] & 0x0f;
    	
        return TS_PackageHead;
    }
    
    • PID是不同的表之间的重要标识,所以我们可以通过PID来获取到我们所需要解析的表的数据包。

    2.4 负载单元开始标志解析

    ​ 同PID获取方式一样,我们所保存的结构体里payload_unit_start_indicator就是负载单元开始的标志。通俗来说:

    • 当该位置为’1’时,说明这段数据包含有该pid表的首张数据表,并且有效数据从该数据包的第五个字节开始。
    • 当该位置为’0’时,说明这段数据包不包含该pid表的首张数据表,而是该pid表的后续数据。有效数据从该数据包的第四个字节开始。
    • 这个标志位是我们获取pid表的第一张表的有效标志。

    2.5 小结

    ​ 对于包头的解析是我们分析一段数据包的开始,我们可以从解析出来的数据中可以得知该段数据包是不是我们所需要的数据包。在这里我们需要注意的问题就是:

    1. 我们对数据进行遍历的时候,可以将所获的一段完整数据包给单独提取出来进行保存。这样我们在进行解析的时候就可以对这段保存的数据来进行操作,避免了反复移动文件指针来进行数据的操作,极大的方便了我们代码的可阅读性。
    2. 在进行下一步解析之前我们就应该构思好我们代码的初步框架,避免后来出现代码逻辑混乱和反复循环读取数据的情况。
    3. 包头的PID,payload_unit_start_indicator, continuity_counter,这三个重要数据是我们判断一个完整数据包的依据。

    3 PAT表的解析

    ​ PAT表是我们解析TS流的起点,也是机顶盒接收的入口点,使它获取数据的开始。PAT表定义了我们当前TS流中所有的节目,其PID固定为0x0000。它是PSI的根节点,要查询节目必须从PAT表开始。

    3.1 解析步骤

    1. 寻找到PID为0x0000的TS数据包
    2. 根据解析出来的负载单元开始标志,确定数据包除包头外的有效数据起始位置,将有效数据提取保存。
    3. 将获取的有效数据存入PAT表的对应结构体中,方便保存。

    3.2 PAT表的描述

    • PAT表主要携带有以下信息:
    TS流IDtransport_stream_id该ID标志唯一的流ID
    节目频道号program_number该号码标志TS流中的一个频道,该频道可以包含很多的节目(即可以包含多个Video PID和Audio PID)
    PMT的PIDprogram_map_PID表示本频道使用哪个PID做为PMT的PID,因为可以有很多的频道,因此DVB规定PMT的PID可以由用户自己定义
    • PAT表的结构:
    #字段名占位次序说明
    0*table_id8 bits第0个字节PAT的table_id只能是0x00
    1*section_syntax_indicator1 bit第1、2个字节段语法标志位,固定为1
    2*zero1 bit第1、2个字节
    3*reserved2 bits第1、2个字节
    4*section_length12 bits第1、2个字节意思是 段长度为29字节
    5*transport_stream_id16 bits第3、4个字节TS的识别号
    6*reserved2 bits第5个字节TS的识别号
    7*version_number5 bits第5个字节一旦PAT有变化,版本号加1
    8*current_next_indicator1 bit第5个字节当前传送的PAT表可以使用,若为0则要等待下一个表
    9*section_number4 bits第6个字节给出section号,在sub_table中, 第一个section其section_number为"0x00", 每增加一个section,section_number加一
    10*last_section_number4 bits第7个字节sub_table中最后一个section的section_number
    循环开始(循环内的数据解析见下一节内容!)
    -program_number16 bits节目号
    -reserved3 bits-
    -network_id 或 program_map_PID13 bitsprogram_number为0x0000时, 这里是network_id(NIT的PID); 其余情况是program_map_PID(PMT的PID)
    循环结束
    -CRC_3232 bits最后4个字节-

    注意

    • 从for()开始,就是描述了当前流中的频道数目(N),每一个频道对应的PMT PID是什么。解复用程序需要接收所有的频道号码和对应的PMT 的PID,并把这些信息在缓冲区中保存起来。在后部的处理中需要使用到PMT的 PID。
    • 由以上几个表可以分析出PAT表和PMT表有着内在的联系。也就是之前提到的。PAT表描述了当前流的NIT(Network Information Table,网络信息表)中的PID、当前流中有多少不同类型的PMT表及每个PMT表对应的频道号。

    3.3 PAT表的获取

    ​ 因为我们了解到了,所有PAT表的PID都固定是0x0000,所以在对PAT表获取时就只用利用if()判断语句来判断哪个数据包的PID是0x0000,即可找到PAT表。

                if(fgetc(pTsFile) == 0x47)
                {   
                    fseek(pTsFile, -TsPackage_len, SEEK_CUR);
                    fseek(pTsFile, TsPosition - 1, SEEK_CUR);
    
                    fread(TSdata, 1, TsPackage_len, pTsFile);
    
                    TS_PackageHead = ParseTS_Type();
                }
    
                if(TS_PackageHead.PID == 0 && value == 0)
                {
                    TS_PackagePAT  = ParseTS_PAT(TsPackage_len, TS_PackageHead);
                    value++;
                }
    

    3.4 PAT表信息解析

    ​ 找到PAT表以后,我们就能够把我们的数据保存在结构体中。

    TS_PAT TSpatTable(TS_PAT TS_PackagePAT, unsigned char PATtable[204])
    {
        int PAT_Length = 0;
    
    	TS_PackagePAT.table_id = PATtable[0];
    	TS_PackagePAT.section_syntax_indicator = PATtable[1] >> 7;
    	TS_PackagePAT.zero = (PATtable[1] >> 6) & 0x1;
    	TS_PackagePAT.reserved_1 = (PATtable[1] >> 4) & 0x3;
    	TS_PackagePAT.section_length = ((PATtable[1] & 0x0F) << 8) | PATtable[2];
    	TS_PackagePAT.transport_stream_id = (PATtable[3] << 8) | PATtable[4];
    	TS_PackagePAT.reserved_2 = PATtable[5] >> 6;
    	TS_PackagePAT.version_number = (PATtable[5] >> 1) & 0x1F;
    	TS_PackagePAT.current_next_indicator = (PATtable[5] << 7) >> 7;
    	TS_PackagePAT.section_number = PATtable[6];
    	TS_PackagePAT.last_section_number = PATtable[7];
    	PAT_Length = 3 + TS_PackagePAT.section_length;
    	TS_PackagePAT.CRC_32 = (PATtable[PAT_Length - 4] << 24) | (PATtable[PAT_Length - 3] << 16)
    	        | (PATtable[PAT_Length - 2] << 8) | PATtable[PAT_Length - 1];
    
        return TS_PackagePAT;
    }
    
    for(PatProgramPosition = 8; PatProgramPosition  < PAT_Length - 4; PatProgramPosition  += 4)
    	{
    		if (0x00 == ((PATtable[PatProgramPosition ] << 8) | PATtable[1 + PatProgramPosition ]))
    		{
    			TS_PackagePAT.network_PID = ((PATtable[2 + PatProgramPosition ] & 0x1F) << 8) | PATtable[3 + PatProgramPosition ];
    			printf("the network_PID %0x\n", TS_PackagePAT.network_PID);
    		}
    		else
    		{
    			TS_PackagePAT.astPAT_Program[PAT_ProgramCount].program_number = (PATtable[PatProgramPosition ] << 8)
    			        | PATtable[1 + PatProgramPosition ];
    			TS_PackagePAT.astPAT_Program[PAT_ProgramCount].reserved_3 = PATtable[2 + PatProgramPosition ] >> 5;
    			TS_PackagePAT.astPAT_Program[PAT_ProgramCount].program_map_PID = ((PATtable[2 + PatProgramPosition ] & 0x1F) << 8)
    			        | PATtable[3 + PatProgramPosition ]; 
                PAT_ProgramCount++;
    		}
    	}
    

    注意:

    • 一定要对负载单元开始标志进行判读,确定好有效数据读取位置。

    • 在PAT表中有个循环,代表了存储的节目号信息。所以要在位运算时格外注意,一旦位置出错,后面的数据存储也会出错。

    • PAT表会在TS流中循环发送,所以我们只用解析一次,无需一直解析。

    • 虽然PAT表只用解析一次,但是表里的节目号等信息是有很多,一定要解析完整。

    3.5 小结

    ​ PAT表是我们解析的第一张表,解析方法各有不同,但是需要想好。因为后面随着表的数量的增加,如果方法过于复杂,会导致后续表格的解析也会同样复杂,这样就导致了整体代码显的冗长。而且在初步解析时我们同样要理清自己的解析思路,避免后续出现逻辑错误。

    4 PMT表的解析

    ​ PMT是连接节目号与节目元素的桥梁,主要提供节目numbers和节目elements之间的映射关系。在一个TS流中包含有多少个频道,就包含多个PID不同的PMT表。

    ​ 我们知道,一个电视节目至少包含了视频和音频数据,而每一个节目的视音频数据都是以包的形式在TS流中传输的; 所以说,一个TS流包含了多个节目的视频和音频数据包。 要想过滤出一个TS流中其中一个节目的视频和音频,则需要知道这个节目中视频和音频的标识号PID。 PMT表的作用就在于,它提供了每个节目视频、音频(或其他)数据包的PID。

    4.1 解析步骤

    1. 结合PAT表的信息,寻找到各个PMT表的PID,并提取对应的TS数据包
    2. 根据解析出来的负载单元开始标志,确定数据包除包头外的有效数据起始位置,将有效数据提取保存。
    3. 将获取的有效数据存入PMT表的对应结构体中,方便保存。

    4.2 PMT表的描述

    ​ PMT表中包含的数据如下:

    • 当前频道中所包含的所有Video数据的PID。

    • 当前频道中所包含的所有Audio数据的PID。

    • 和当前频道关联在一起的其他数据的PID(如数字广播,数据通讯等使用的PID)。

      所以,当我们解析了PMT表之后,我们就能够获取到频道中所有的PID信息,例如当前频道包含多少Video、共多少个Audio和其他数据,还能知道每种数据对应的PID分别是什么。这样如果我们要选择其中一个Video和Audio收看,那么只需要把要收看的节目的Video PID和Audio PID保存起来,在处理Packet的时候进行过滤即可实现。

      PMT表的结构如下:

      Syntax(句法结构)No. of bits(所占位数)Identifier(识别符)Note(注释)
      program_map_section(){
      table_id8uimsbf表标识符
      Section_syntax_indicator1bslbf段语法指示符,通常设置为“1”
      “0”1bslbfzero
      Reserved2bslbf保留
      Section_length12uimsbf段长度,从program_number开始,到CRC_32(含)的字节总数
      program_number16uimsbf频道号码,表示当前的PMT关联到的频道
      Reserved2bslbf保留
      Version_number5bslbf版本号码,如果PMT内容有更新,则它会递增1通知解复用程序需要重新接收节目信息
      Current_next_indicator1bslbf当前未来标志符
      Section_number8uimsbf当前段号码
      last_section_number8uimsbf最后段号码,含义和PAT中的对应字段相同
      reserved3bslbf保留
      PCR_PID13uimsbfPCR(节目参考时钟)所在TS分组的PID
      reserved4bslbf保留
      program_info_length12uimsbf节目信息长度(之后的是N个描述符结构,一般可以忽略掉,这个字段就代表描述符总的长度,单位是Bytes)紧接着就是频道内部包含的节目类型和对应的PID号码了,头两位为“00”
      for(i=0;i<N;i++){
      descriptor()
      }
      for(i=0;i<N1;i++){
      stream_type8uimsbf流类型,标志是Video还是Audio还是其他数据
      reserved3bslbf保留
      elementary_PID13uimsbf该节目中包括的视频流,音频流等对应的TS分组的PID
      reserved4bslbf保留
      ES_info_length12uimsbf头两位为"00"
      for(i=0;i<N2;i++){
      descriptor()
      }
      }
      CRC_3232rpchof32位, 表示CRC的值
      }

    注意

    • 在PMT表结构中我们发现了有两个地方有Descriptor(),这是描述符数据。第一个Descruptor()数据的长度是由program_info_length来决定的,后面一个Descruptor()数据长度是由ES_info_length来决定的。
    • 这里的描述符可能为Mosaic descriptor(马赛克描述符)Service move descriptor(业务转移描述符)Stream identifier descriptor(流标识符描述符)Teletext descriptor(图文电视描述符)

    4.3 PMT表的获取

    ​ 在这里我们就需要同PAT表的获取做对比。因为在一段TS流中PAT表我们只用解析一次即可,但是PMT表不可以只获取一张。因为PMT表有多张,且携带的信息也不同。我采用的方法是在PAT表解析时,利用一个数组来保存所有的PMT的PID信息,然后在通过循环遍历来找到每一张PMT表。

    if((TS_PackageHead.PID == TS_PackagePAT.astPAT_Program[PMTCount].program_map_PID) && 
                                (TS_PackagePAT.astPAT_Program[PMTCount].program_map_PID != 0))
                {
                    TS_Program = ParseTS_PMT(TsPackage_len, TS_PackageHead);
                    
                    PMTCount++;
                }
    
    

    注意

    • 为了避免重复解析,我利用标号来记录解析表的序号。又因为PMT表的PID不可能是0x0000。所以加上一个判断条件来避免空数据的错误解析。

    4.4 PMT表信息解析

    ​ TS流的几张表的解析方式都差不多,同PAT表一样,我们根据负载单元开始标志来确定数据包的有效数据的起始位置。然后将数据存入到对应的结构体当中。

    //判断负载单元开始的标志
        for(int i = 0; i < PmtStreamCount; i++)
        {
            if((TS_PackagePMT.astPMT_Stream[i].stream_type == 0x01)|| (TS_PackagePMT.astPMT_Stream[i].stream_type == 0x02) 
                    || (TS_PackagePMT.astPMT_Stream[i].stream_type ==0x1b))
            {
                TS_Program.VideoPID = TS_PackagePMT.astPMT_Stream[i].elementary_PID;
            }
    
            if((TS_PackagePMT.astPMT_Stream[i].stream_type == 0x03)|| (TS_PackagePMT.astPMT_Stream[i].stream_type == 0x04) 
                    || (TS_PackagePMT.astPMT_Stream[i].stream_type ==0x0f) || (TS_PackagePMT.astPMT_Stream[i].stream_type == 0x11))
            {
                TS_Program.AudioPID[ProgramPoint] = TS_PackagePMT.astPMT_Stream[i].elementary_PID;
                
                ProgramPoint++;
            }
        }
     //将数据存入结构体当中   
        TS_PMT TSPMT_table(TS_PMT TS_PackagePMT, unsigned char PMTtable[204])
    {
    	int PMT_Length = 0;
        unsigned char descriptor[204] = {0};
        
    
    	TS_PackagePMT.table_id = PMTtable[0];
    	TS_PackagePMT.section_syntax_indicator = PMTtable[1] >> 7;
    	TS_PackagePMT.zero = (PMTtable[1] >> 6) & 0x01;
    	TS_PackagePMT.reserved_1 = (PMTtable[1] >> 4) & 0x03;
    	TS_PackagePMT.section_length = ((PMTtable[1] & 0x0F) << 8) | PMTtable[2];
    	TS_PackagePMT.program_number = (PMTtable[3] << 8) | PMTtable[4];
    	TS_PackagePMT.reserved_2 = PMTtable[5] >> 6;
    	TS_PackagePMT.version_number = (PMTtable[5] >> 1) & 0x1F;
    	TS_PackagePMT.current_next_indicator = (PMTtable[5] << 7) >> 7;
    	TS_PackagePMT.section_number = PMTtable[6];
    	TS_PackagePMT.last_section_number = PMTtable[7];
    	TS_PackagePMT.reserved_3 = PMTtable[8] >> 5;
    	TS_PackagePMT.PCR_PID = ((PMTtable[8] << 8) | PMTtable[9]) & 0x1FFF;
    	TS_PackagePMT.reserved_4 = PMTtable[10] >> 4;
    	TS_PackagePMT.program_info_length = ((PMTtable[10] & 0x0F) << 8) | PMTtable[11];
    
    	if (0 != TS_PackagePMT.program_info_length)
    	{
    		memcpy(TS_PackagePMT.program_info_descriptor, PMTtable + 12, TS_PackagePMT.program_info_length);
    
            ParseTS_Descriptor(TS_PackagePMT.program_info_descriptor);
    	}
    
    	PMT_Length = TS_PackagePMT.section_length + 3;
    	TS_PackagePMT.CRC_32 = (PMTtable[PMT_Length - 4] << 24) | (PMTtable[PMT_Length - 3] << 16)
    	        | (PMTtable[PMT_Length - 2] << 8) | PMTtable[PMT_Length - 1];
    
        return TS_PackagePMT;
    }
    
    //循环存放
        for(PmtStreamPosition += TS_PackagePMT.program_info_length; PmtStreamPosition < (PMT_Length - 4); PmtStreamPosition += 5)
        {
            unsigned char descriptor[204] = {0};
    
            TS_PackagePMT.astPMT_Stream[PmtStreamCount].stream_type = PMTtable[PmtStreamPosition];
           
            TS_PackagePMT.astPMT_Stream[PmtStreamCount].reserved_5 = (PMTtable[PmtStreamPosition + 1]) >> 5;
            TS_PackagePMT.astPMT_Stream[PmtStreamCount].elementary_PID = ((PMTtable[PmtStreamPosition + 1] & 0x1f) << 8) 
                    | PMTtable[PmtStreamPosition + 2];
            TS_PackagePMT.astPMT_Stream[PmtStreamCount].reserved_6 = (PMTtable[PmtStreamPosition + 3] >> 4);
            TS_PackagePMT.astPMT_Stream[PmtStreamCount].ES_info_length = ((PMTtable[PmtStreamPosition + 3] & 0xf) << 8)
                    | PMTtable[PmtStreamPosition + 4];
            
    
    
            if(TS_PackagePMT.astPMT_Stream[PmtStreamCount].ES_info_length != 0)
            {
                memcpy(TS_PackagePMT.astPMT_Stream[PmtStreamCount].descriptor, PMTtable + PmtStreamPosition + 5,
    			        TS_PackagePMT.astPMT_Stream[PmtStreamCount].ES_info_length);
    			PmtStreamPosition += TS_PackagePMT.astPMT_Stream[PmtStreamCount].ES_info_length;
            
                ParseTS_Descriptor(TS_PackagePMT.astPMT_Stream[PmtStreamCount].descriptor);
            }
          }
    

    ​ 这样我们就能够获取到PMT表当中的信息。

    注意:

    • 因为PMT表结构中涉及到循环,所以我们在对for()循环里面的数据进行存储时,可以再将循环体里面的信息建立一个单独的结构体。再在主结构体当中建立它的结构体数组,这样我们就能够存储相关信息
    • 在进行数据存储时,一定要记得计算Descriptor()描述符的长度,再做位运算的时候记得加上或删去该段长度。

    4.5 小结

    ​ 相对于解析PAT表,在解析PMT表的过程中增加了对结构体循环数据的存储,和描述符的数据存储。在后面的表格数据存储中,几乎每一张都涉及到了这两个方面。总体来说就是位运算和对每个字节信息的理解。对于关键的信息我们要加强记忆,遇到不懂或者含义不清的地方要及时参考标准文档。

    5 SDT表的解析

    ​ 在解析TS流的时候,首先寻找PAT表,根据PAT获取所有PMT表的PID;再寻找PMT表,获取该频段所有节目数据并保存。这样,只需要知道节目的PID就可以根据PacketHeade给出的PID过滤出不同的Packet,从而观看不同的节目。这些就是PAT表和PMT表之间的关系。而由于PID是一串枯燥的数字,用户不方便记忆、且容易输错,所以需要有一张表将节目名称和该节目的PID对应起来,DVB设计了SDT表来解决这个问题。 该表格标志一个节目的名称,并且能和PMT中的PID联系起来,这样用户就可以通过直接选择节目名称来选择节目了。

    5.1 解析步骤

    1. 获取到PID = 0x0011的数据包,再根据table_id = 0x42(现行TS) ||table_id = 0x46(非现行TS),找到SDT表
    2. 根据解析出来的负载单元开始标志,确定数据包除包头外的有效数据起始位置,将有效数据提取保存。
    3. 将获取的有效数据存入SDT表的对应结构体中,方便保存。

    5.2 SDT表的描述

    “SDT描述了业务内容及信息,连接了NIT、EIT和PMT(PSI)”

    SDT表主要提供关于Service的信息,如Service name,Service provider 等。

    这里所说的业务,即Service;CCTV 1是一个Service(我们说“频道”),湖南卫视也是一个Service

    PID = 0x0011

    table_id = 0x42 (discribe actual TS,现行TS)

    table_id = 0x46 (discribe not actual TS,非现行TS)

    • SDT表被切分成业务描述段(service_description_section),由PID为0x0011的TS包传输(BAT段也由PID为0x0011的TS包传输,但table_id不同)。

    • 描述现行TS(即包含SDT表的TS)的SDT表的任何段的table_id都为0x42,且具有相同的table_id_extension(transport_stream_id)以及相同的original_network_id。 指向非现行TS的SDT表的任何段的table_id都应取0x46。

    • 业务描述表SDT(见下一节的SDT表结构)中的每一个子表,都用来描述包含于一个特定的传输流中的业务。 该业务可能是现行传输流中的一部分,也可能是其他传输流中的一部分,可以根据table_id 来确定区分上述两种情况。

    ​ 下表是SDT的结构:

    Syntax(句法结构)No. of bits(所占位数)Identifier(识别符)Note(注释)
    service_description_section(){
    table_id8uimsbf表标识符
    Section_syntax_indicator1bslbf段语法指示符,通常设置为“1”
    Reserved_future_use1bslbf预留使用
    Reserved2bslbf保留
    Section_length12uimsbf段长度,12位,指出了此Section的长度,头两位为“00”,值不超过1021
    transport_stream_id16uimsbf传输流标识符,给出TS的识别号
    Reserved2bslbf保留
    Version_number5bslbfsub_table的版本号,值为0~31,当sub_table信息改变时,值加1,若值已为31,则变化后值为0(循环),若curren_next_indicator的值为“1”,version_number指当前sub_table,若curren_next_indicator的值为“0”,version_number指下一个sub_table
    Current_next_indicator1bslbf当前后续指示符,1位,“1”指sub_table是current, “0”指sub_table是next
    Section_number8uimsbf段号,8位,给出section号,在sub_table中,第一个section其section_number为"0x00",每增加一个section,section_number加一
    last_section_number8uimsbf最后段号,sub_table中最后一个section的section_number
    original_nerwork_id16uimsbf原始网络标识符,16位,给出originating delivery system中的网络识别号
    reserved_future_use8bslbf预留使用
    for(i=0;i<N;i++){
    service_id16uimsbf业务标识符,16位,用来标识TS中的Service,等同于program_map_section中的program_number
    reserved_future_use8bslbf预留使用
    EIT_schedule_flag1bslbfEIT时间表标志,1位,若为“1”,则EIT schedule信息在当前TS中,为“0”,则TS中不存在EIT schedule信息
    EIT_present_following_flag1bslbfEIT当前后续标志,1位,若为“1”,则EIT_present_following 信息在当前TS中,为“0”,则TS中不存在EIT_present_following信息。
    running_status3uimsbf
    freed_CA_mode1bslbf自由条件接收模式,1位,“0”表示无CA,“1”表示有CA
    descriptors_loop_length12uimsbf描述符循环长度
    for(i=0;i<N;i++){
    descriptor()
    }
    }
    CRC_3232rpchof32位, 表示CRC的值
    }

    注意

    • 在SDT表中也包含Descriptor()描述符。主要有Bouquet name descriptor(业务群名称描述符)CA identifier descriptor(CA系统控制字描述符)Country availability descriptor(有效国家描述符)Data broadcast descriptor(数据广播描述符)Linkage descriptor(链接描述符)Mosaic descriptor(马赛克描述符)Multilingual service descriptor(多语言服务描述符)NVOD reference descriptor(NVOD参考描述符)Service descriptor(业务描述符)Telephone descriptor(电话描述符)Time shifted service descriptor(时移业务描述符)
    • 同样,描述符的数据长度由descriptors_loop_length()决定。

    5.3 SDT表的获取

    ​ 由上面信息可知道,SDT表的PID为0x0011,但是PID为0x0011的表还有BAT, ST。所以我们不能仅仅从PID这一条信息里找到准确的SDT数据包。所以我们在PID = 0x0011的基础上再判断其除包头外的有效字节的第一个字节(table_id)是否为0x0042或者0x0046。结合这两条判断条件,我们就可以准确筛选出来SDT表。

    if(TS_PackageHead.PID == 0x0011)
    {
    	value2 = SDT_BAT(TsPackage_len,TS_PackageHead,value2);
    }
    
    int SDT_BAT(int TsPackage_len,TS_Package_Head TS_PackageHead, int value2)
    {
        if(TS_PackageHead.payload_unit_start_indicator == 0)
        {
            if(TSdata[4] == 0x42 && value2 == 1)
            {
                ParseTS_SDT(TsPackage_len, TS_PackageHead);
                
                value2++;
    
                return value2;
            }else if (TSdata[4] == 0x4a)
            {
                
            }
            
            
        }
    
        if(TS_PackageHead.payload_unit_start_indicator == 1)
        {
            if(TSdata[5] == 0x42 && value2 == 0)
            { 
                ParseTS_SDT(TsPackage_len, TS_PackageHead);
                
                value2++;
                
                return value2;
            }else if (TSdata[5] == 0x4a)
            {
                
            }
            
        }
    
        return value2;
    }          
    

    注意

    • 因为PAT表和PMT表无需判断table_id就能够筛选出来,但是SDT表判断table_id。所以我们在提取数据包时要先判断包头的负载单元开始标志,防止table_id的信息出错。

    5.4 SDT表信息解析

    ​ 一样的顺序,我们需要先建立结构体,再描述符的循环里面可以创建一个unsigned char 类型的数组来保存描述符数据。

    TS_SDT TSSDT_Table(TS_SDT TS_PackageSDT, unsigned char SDTtable[204])
    {
    	int SDT_Length = 0;
    
    	TS_PackageSDT.table_id = SDTtable[0];
    	TS_PackageSDT.section_syntax_indicator = SDTtable[1] >> 7;
    	TS_PackageSDT.reserved_future_use_1 = (SDTtable[1] >> 6) & 0x01;
    	TS_PackageSDT.reserved_1 = (SDTtable[1] >> 4) & 0x03;
    	TS_PackageSDT.section_length = ((SDTtable[1] & 0x0F) << 8) | SDTtable[2];
    	TS_PackageSDT.transport_stream_id = (SDTtable[3] << 8) | SDTtable[4];
    	TS_PackageSDT.reserved_2 = SDTtable[5] >> 6;
    	TS_PackageSDT.version_number = (SDTtable[5] >> 1) & 0x1F;
    	TS_PackageSDT.current_next_indicator = (SDTtable[5] << 7) >> 7;
    	TS_PackageSDT.section_number = SDTtable[6];
    	TS_PackageSDT.last_section_number = SDTtable[7];
    	TS_PackageSDT.original_network_id = (SDTtable[8] << 8) | SDTtable[9];
    	TS_PackageSDT.reserved_future_use_2 = SDTtable[10];
    
    	SDT_Length = TS_PackageSDT.section_length + 3;
    	TS_PackageSDT.CRC_32 = (SDTtable[SDT_Length - 4] << 24) | (SDTtable[SDT_Length - 3] << 16)
    	        | (SDTtable[SDT_Length - 2] << 8) | SDTtable[SDT_Length - 1];
    
        return TS_PackageSDT;
    }
    
    TS_SDT ParseSDT_Section(TS_SDT TS_PackageSDT, unsigned char SDTtable[204])
    {
    	int SDT_Length = 0;
    	int ServiceCount = 0;
    	int ServicePosition = 11;
    	unsigned char descriptor[204];
    
        SDT_Length = TS_PackageSDT.section_length + 3;
    
    	for (ServicePosition = 11; ServicePosition < SDT_Length - 4; ServicePosition += 5)
    	{
    		TS_PackageSDT.SDT_Service[ServiceCount].service_id = (SDTtable[ServicePosition] << 8)
    		        | SDTtable[ServicePosition + 1];
    		TS_PackageSDT.SDT_Service[ServiceCount].reserved_future_use = SDTtable[ServicePosition + 2] >> 2;
    		TS_PackageSDT.SDT_Service[ServiceCount].EIT_schedule_flag = (SDTtable[ServicePosition + 2] & 0x03) >> 1;
    		TS_PackageSDT.SDT_Service[ServiceCount].EIT_present_following_flag = SDTtable[ServicePosition + 2] & 0x01;
    		TS_PackageSDT.SDT_Service[ServiceCount].running_status = SDTtable[ServicePosition + 3] >> 5;
    		TS_PackageSDT.SDT_Service[ServiceCount].free_CA_mode = (SDTtable[ServicePosition + 3] & 0x1F) >> 4;
    		TS_PackageSDT.SDT_Service[ServiceCount].descriptors_loop_length = ((SDTtable[ServicePosition + 3] & 0x0F) << 8)
    		        | SDTtable[ServicePosition + 4];
    
    		if (TS_PackageSDT.SDT_Service[ServiceCount].descriptors_loop_length != 0)
    		{
    			memcpy(TS_PackageSDT.SDT_Service[ServiceCount].descriptor, SDTtable + 5 + ServicePosition,
    			        TS_PackageSDT.SDT_Service[ServiceCount].descriptors_loop_length);
    			ServicePosition += TS_PackageSDT.SDT_Service[ServiceCount].descriptors_loop_length; 
    
    			ParseTS_Descriptor(TS_PackageSDT.SDT_Service[ServiceCount].descriptor);
    		}
    		ServiceCount++;
    	}
    
    	return TS_PackageSDT;
    }
    
    
    
    void ParseTS_SDT(int TsPackage_len,TS_Package_Head TS_PackageSDTHead)
    {
        TS_SDT TS_PackageSDT = {0};
    	unsigned char SDTtable[204] = {0};
    
        if(TS_PackageSDTHead.payload_unit_start_indicator == 0)
        {
            memcpy(SDTtable, TSdata+4, TsPackage_len-4);
        
            TS_PackageSDT = TSSDT_Table(TS_PackageSDT, SDTtable);
        }
    
        if(TS_PackageSDTHead.payload_unit_start_indicator == 1)
        {
            memcpy(SDTtable, TSdata+5, TsPackage_len-5);
            
            TS_PackageSDT = TSSDT_Table(TS_PackageSDT, SDTtable);
        }
        
        TS_PackageSDT = ParseSDT_Section(TS_PackageSDT, SDTtable);
    
    }
    

    这样我们就可保存并获取到SDT表的信息。

    注意

    • 在解析函数外进行负载单元开始标志的判断是为了甄别出SDT表,而解析函数内的负载单元开始标志的判断是为了区别出有效数据的位置。

    5.5 小结

    ​ 在SDT表的获取过程中,有两个难点。第一个就是对SDT表的获取,如何甄别出SDT,BAT和ST。第二个就是对于描述符的处理,所包含的信息很多。总而言之,每张表的在解析方法上都基本大同小异。

    6 NIT表的解析

    ​ NIT描述了数字电视网络中与网络相关的信息。

    6.1 解析步骤

    1. 通过PID = 0x0010 和 table_id = 0x40 || table_id = 0x41 来判断,获取到含有NIT表的数据包。
    2. 根据解析出来的负载单元开始标志,确定数据包除包头外的有效数据起始位置,将有效数据提取保存。
    3. 将获取的有效数据存入NIT表的对应结构体中,方便保存。

    6.2 NIT表的描述

    NIT表主要提供物理网络本身的一些信息。

    PID = 0x0010

    table_id = 0x40 (discribe actual newwork)

    table_id = 0x41 (discribe not actual newwork)

    • NIT描述了数字电视网络中与网络相关的信息,但这个表本身的信息有限,更多的信息是依靠插入表中的描述符来提供的。 NIT常用的描述符有:网络名称描述符(network_name_descriptor)、有线传送系统(cable_delivery_system_descriptor)、业务列表描述符(service_list_descriptor)和链接描述符(linkage_descriptor)。

    • 网络信息表NIT传递了与通过一个给定的网络传输的复用流/TS流的物理结构相关的信息,以及与网络自身特性相关的信息。在本标准应用的范围内,original_network_id 和 transport_stream_id 两个标识符相结合唯一确定了网络中的TS流。各网络被分配独立的 network_id值作为网络的唯一识别码。这些码字的分配见ETR 162。当NIT表在生成TS流的网络上传输时,network_id和original_network_id将取同一值。

    • 当转换频道时,为了使存取时间最小,IRD可以在非易失性存储器上存储NIT表信息。除现行网络外,也可以为其他网络传输NIT表信息。现行网络的NIT表与其他网络的NIT表使用不同的table_id值来区分。

      NIT的结构如下表:

      Syntax(句法结构)No. of bits(所占位数)Identifier(识别符)Note(注释)
      network_information_section(){
      table_id8uimsbf表标识符
      Section_syntax_indicator1bslbf段语法指示符,通常设置为“1”
      Reserved_future_use1bslbf预留使用
      Reserved2bslbf保留
      Section_length12uimsbf段长度,12位,指出了此Section的长度,头两位为“00”,值不超过1021
      Network_id16uimsbf网络标识符,16位字段。NIT表所描述的传输系统的网络标识,用以区别其他的传输系统。
      Reserved2bslbf保留
      Version_number5uimsbfsub_table的版本号,值为0~31,当sub_table信息改变时,值加1,若值已为31,则变化后值为0(循环),若curren_next_indicator的值为“1”,version_number指当前sub_table,若curren_next_indicator的值为“0”,version_number指下一个sub_table
      Current_next_indicator1bslbf当前后续指示符,1位,“1”指sub_table是current, “0”指sub_table是next
      Section_number8uimsbf段号,8位,给出section号,在sub_table中,第一个section其section_number为"0x00",每增加一个section,section_number加一
      last_section_number8uimsbf最后段号,sub_table中最后一个section的section_number
      Reserved_future_use4bslbf预留使用
      Network_descriptors_length12uimsbf网络描述符长度
      for(i=0;i<N;i++){
      descriptor()First descriptor loop
      reserved_future_use4bslbf-
      transport_stream_loop_length12uimsbf传输流循环长度
      for(i=0;i<N;i++){
      transport_stream_id16uimsbf传输流标识符,16位, 给出TS的识别号
      original_network_id16uimsbf原始网络标识符,16位,给出originating delivery system中的网络识别号
      reserved_future_use4bslbf预留使用
      transport_descriptors_length12uimsbf传输流描述符长度
      for(i=0;i<N;i++){
      descriptor()Second descriptor loopSecond descriptor loop
      }
      }
      CRC_3232rpchof32位, 表示CRC的值
      }

    注意:

    • 在这里我们注意到NIT结构里出现了两个循环,分别成为第一层循环和第二层循环; 每层循环都插入了一个描述符,也就是一共插入了两个描述符。 这两个描述符的特点如下:
    第一层描述符作用域是针对整个网络的,如插入网络名称描述符、链接描述符等
    第二层描述符作用域是第一层循环所代表的一个TS流,如插入有线传输系统描述符
    • 第一层包含的描述符:Linkage descriptor(链接描述符)Multiligual network name descriptor(多语种网络名称描述符)Network name descriptor(网络名称描述符)

    • 第二层包含的描述符:Delivery system descriptor(交付系统描述符)Service list descriptor(业务列表描述符)Frequency list descriptor(频率列表描述符)

    • 此外,NIT表还有如下特点:

    • NIT表被切分为网络信息段(network_information_section)

    • 任何NIT的段都必须由PID为0x0010的TS包传输

    • 现行网络的NIT表任何段的table_id值应为0x40,且具有相同的table_id_extension即(network_id);

    • 现行网络以外的其他网络NIT表的段table_id值应为0x41

    6.3 NIT表的获取

    ​ 由上面信息可知道,NIT表的PID为0x0010,但是PID为0x0010的表还有 ST。所以我们不能仅仅从PID这一条信息里找到准确的NIT数据包。所以我们在PID = 0x0010的基础上再判断其除包头外的有效字节的第一个字节(table_id)是否为0x0040或者0x0041。结合这两条判断条件,我们就可以准确筛选出来NIT表。

    int ParseTS_NIT(int TsPackage_len,TS_Package_Head TS_PackageNITHead, int NITCount)
    {
        TS_NIT TS_PackageNIT = {0};
    	unsigned char NITtable[204] = {0};
    
        if(TS_PackageNITHead.payload_unit_start_indicator == 0 && TSdata[4] == 0x40 )
        {
            memcpy(NITtable, TSdata+4, TsPackage_len-4);
        
            TS_PackageNIT = TSNIT_Table(TS_PackageNIT, NITtable);
    
    		ParseNIT_Section(TS_PackageNIT, NITtable);
    
    		return ++NITCount;
        }
    
        if(TS_PackageNITHead.payload_unit_start_indicator == 1 && TSdata[5] == 0x40)
        {
            memcpy(NITtable, TSdata+5, TsPackage_len-5);
            
            TS_PackageNIT = TSNIT_Table(TS_PackageNIT, NITtable);
    
    		ParseNIT_Section(TS_PackageNIT, NITtable);
    
    		return ++NITCount;
        }
    
    	return NITCount;
    }
    

    注意

    • 因为网络情况是不断变化的,相对应NIT表也是会变化的,所以我们在解析时就不可以同PAT表那样只获取一次NIT表。

    6.4 NIT表信息解析

    ​ 我们以文档为标准,建立好NIT的结构体,然后通过位运算,将数据保存在结构体中。

    TS_NIT TSNIT_Table(TS_NIT TS_PackageNIT, unsigned char NITtable[204])
    {
    	int NetworkDescriptorLen = 0;
    	int NIT_Length = 0;
    
    	TS_PackageNIT.table_id = NITtable[0];
    	TS_PackageNIT.section_syntax_indicator = NITtable[1] >> 7;
    	TS_PackageNIT.reserved_future_use_1 = (NITtable[1] >> 6) & 0x01;
    	TS_PackageNIT.reserved_1 = (NITtable[1] >> 4) & 0x03;
    	TS_PackageNIT.section_length = ((NITtable[1] & 0x0F) << 8) | NITtable[2];
    	TS_PackageNIT.network_id = (NITtable[3] << 8) | NITtable[4];
    	TS_PackageNIT.reserved_2 = NITtable[5] >> 6;
    	TS_PackageNIT.version_number = (NITtable[5] >> 1) & 0x1F;
    	TS_PackageNIT.current_next_indicator = (NITtable[5] << 7) >> 7;
    	TS_PackageNIT.section_number = NITtable[6];
    	TS_PackageNIT.last_section_number = NITtable[7];
    	TS_PackageNIT.reserved_future_use_2 = NITtable[8] >> 4;
    	TS_PackageNIT.network_descriptors_length = ((NITtable[8] & 0x0F) << 8) | NITtable[9];
    	NetworkDescriptorLen = TS_PackageNIT.network_descriptors_length;
    
    	memcpy(TS_PackageNIT.network_descriptor, NITtable + 10, NetworkDescriptorLen);
    	ParseTS_Descriptor(TS_PackageNIT.network_descriptor);
    
    	TS_PackageNIT.reserved_future_use_2 = NITtable[10 + NetworkDescriptorLen] >> 4;
    	TS_PackageNIT.transport_stream_loop_length = ((NITtable[10 + NetworkDescriptorLen] & 0x0F) << 8)
    	        | NITtable[10 + NetworkDescriptorLen + 1];
    
    	NIT_Length = TS_PackageNIT.section_length + 3;
    
    	TS_PackageNIT.CRC_32 = (NITtable[NIT_Length - 4] << 24) | (NITtable[NIT_Length - 3] << 16)
    	        | (NITtable[NIT_Length - 2] << 8) | NITtable[NIT_Length - 1];
    
    	return TS_PackageNIT;
    }
    
    TS_NIT ParseNIT_Section(TS_NIT TS_PackageNIT, unsigned char NITtable[204])
    {
    	int NIT_Length = 0;
    	int TransportPosition = 0;
    	int TranportCount = 0;
    	int NetworkDescriptorLen = 0;
    
    	NetworkDescriptorLen = TS_PackageNIT.network_descriptors_length;
    	NIT_Length = TS_PackageNIT.section_length + 3;
    
    	for (TransportPosition = 10 + NetworkDescriptorLen + 2; TransportPosition < NIT_Length - 4; TransportPosition += 6)
    	{
    		TS_PackageNIT.astNIT_Transport[TranportCount].transport_stream_id = (NITtable[TransportPosition] << 8)
    		        | NITtable[TransportPosition + 1];
    		TS_PackageNIT.astNIT_Transport[TranportCount].original_network_id = (NITtable[TransportPosition + 2] << 8)
    		        | NITtable[TransportPosition + 3];
    		TS_PackageNIT.astNIT_Transport[TranportCount].reserved_future_use = NITtable[TransportPosition + 4] >> 4;
    		TS_PackageNIT.astNIT_Transport[TranportCount].transport_descriptors_length = ((NITtable[TransportPosition + 4] & 0x0F) << 8)
    		        | NITtable[TransportPosition + 5];
    
    		if (0 != TS_PackageNIT.astNIT_Transport[TranportCount].transport_descriptors_length)
    		{
    			memcpy(TS_PackageNIT.astNIT_Transport[TranportCount].descriptor, NITtable + 6 + TransportPosition,
    			        TS_PackageNIT.astNIT_Transport[TranportCount].transport_descriptors_length);
    			TransportPosition += TS_PackageNIT.astNIT_Transport[TranportCount].transport_descriptors_length;
    
    			ParseTS_Descriptor(TS_PackageNIT.astNIT_Transport[TranportCount].descriptor);
    		}
    
    		TranportCount++;
    	}
    
    	return TS_PackageNIT;
    }
    

    注意:

    • NIT表中大部分数据存在于Descriptor()中。
    • 有些NIT表不是由一段数据包发送完成,而是通过好几个数据包来分段发送,所以在这里需要注意数据包获取的完整性和数据包合并的正确性。

    6.5 小结

    • 在解析NIT表时会遇到许多描述符,我们可以参考标准文档《En300468.V1.7.1_Specification for SI in DVB Systems》来查找相关描述符的信息。
    • TS流中还有其他的表类型,例如:
      • CAT表: CAT描述了节目的加密方式。
      • EIT表:EIT按时间顺序提供每一个业务所包含的事件信息。
      • BAT表:BAT将网络中的所有业务分成了多个业务群,以此界定用户。
      • 这些表的解析方法步骤与上面列举的表的解析方法步骤类似,我们就按照标准文档为依据,进行逐步解析就可以了。

    7 描述符的解析

    ​ 描述符的类型也有很多,在标准文档中列举了各个描述符的作用含义和每个数据位所携带的信息。下面主要针对于部分描述符来做一个介绍和解析。

    7.1 描述符的介绍

    ​ 下表列出了部分在标准文档中声明或定义的描述符,并提供了描述符标签值和在SI表中的预期位置。但是这并不意味着它们在其他表中的使用受到限制。

    DescriptorTag valueNITBATSDTEITTOTPMTSIT (see note 1)
    network_name_descriptor0x40*
    service_list_descriptor0x41**
    stuffing_descriptor0x42*****
    satellite_delivery_system_descriptor0x43*
    cable_delivery_system_descriptor0x44*
    VBI_data_descriptor0x45*
    VBI_teletext_descriptor0x46*
    bouquet_name_descriptor0x47***
    service_descriptor0x48**
    country_availability_descriptor0x49***
    linkage_descriptor0x4A*****
    NVOD_reference_descriptor0x4B**
    time_shifted_service_descriptor0x4C**
    short_event_descriptor0x4D**
    extended_event_descriptor0x4E**
    time_shifted_event_descriptor0x4F**
    component_descriptor0x50***
    mosaic_descriptor0x51***
    stream_identifier_descriptor0x52*
    CA_identifier_descriptor0x53****
    content_descriptor0x54**
    parental_rating_descriptor0x55**
    teletext_descriptor0x56*
    telephone_descriptor0x57***

    注意:

    • 该表在《ETSI EN 300 468 V1.7.1 (2006-05)》文档的Page28。

    7.2 描述符的获取

    ​ 从表中我们可以得知不同的描述符的table_id,这个table_id就是我们在解析不同的表中的descriptor()部分的第一个字节。并且我们可以从其他信息中得知descriptor()部分的数据长度,所以我们很容易提取出来描述符的数据。

            if(TS_PackagePMT.astPMT_Stream[PmtStreamCount].ES_info_length != 0)
            {
                memcpy(TS_PackagePMT.astPMT_Stream[PmtStreamCount].descriptor, PMTtable + PmtStreamPosition + 5,
    			        TS_PackagePMT.astPMT_Stream[PmtStreamCount].ES_info_length);
    			PmtStreamPosition += TS_PackagePMT.astPMT_Stream[PmtStreamCount].ES_info_length;
            
                ParseTS_Descriptor(TS_PackagePMT.astPMT_Stream[PmtStreamCount].descriptor);
            }
    
    

    注意:

    • 有些结构体中的descriptor()是循环结构,所以一定要写好与之对应的数据存储方式。

    7.3 描述符的解析

    • 从描述符的介绍中我们了解到,区别于不同描述符之间的标识就是table_id。那么我们就可以用switch()选择语句来让不同的描述符数据传到对应的解析函数中。
    void ParseTS_Descriptor(unsigned char Descriptor[204])
    {
        switch (Descriptor[0])
        {
        case 0x41:
            Service_List_Descriptor(Descriptor);
            break;
    
        case 0x42:
            Stuffing_Descriptor(Descriptor);
            break;
    
        case 0x48:
            Service_Descriptor(Descriptor);
            break;
        
        case 0x52:
            Stream_Identifier_Descriptor(Descriptor);
            break;
        default:
            break;
        }
    }
    
    • 而描述符的数据存储依旧需要使用到结构体,我们以table_id = 0x41为例:
    void Service_List_Descriptor(unsigned char Descriptor[204])
    {
        SERVICR_LIST_DESCRIPTOR ServiceListTable = {0};
        int ServiceListPosition = 0;
        int ServiceInfoPosition = 0;
        
        ServiceListTable.descriptor_tag = Descriptor[ServiceListPosition];
        ServiceListTable.descriptor_length = Descriptor[ServiceListPosition+1];
        
        for(int i = ServiceListPosition+2; i < ServiceListTable.descriptor_length; i += 3)
        {
            ServiceListTable.service_info[ServiceInfoPosition].service_id = Descriptor[i] << 8 | Descriptor[i+1];
            ServiceListTable.service_info[ServiceInfoPosition].service_type = Descriptor[i+2];
    
            ServiceInfoPosition++;            
        }
    }
    

    ​ 这样我们就能够得到table_id = 0x41的描述符信息。其他描述符的解析步骤与之类似,需要查看文档来帮助解析。

    7.4 小结

    ​ 描述符的信息有很多,种类也有很多。不过在代码结构上,我们只需写出各自的解析函数,然后通过switch()语句使之自行对号入座。

    8 数据包的处理

    ​ 因为在解析过程中需要获取到我们想要的数据,从而得到有用的信息。那么我们就要对解析出来的数据进行数据处理。

    8.1 数据的存储

    ​ 这次解析过程中,我主要通过链表的形式来对解析到的表格数据进行保存。首先建立每种类型表的首链表,作为我们提取数据的钥匙。

    void List()
    {
        PmtPoint = (TS_PMT *)malloc(sizeof(TS_PMT));
        PmtPoint->next = NULL;
        PmtPoint->Previous = NULL;
        TS_PackagePmtPoint = PmtPoint;
    
        NitPoint = (TS_NIT *)malloc(sizeof(TS_NIT));
        NitPoint->next = NULL;
        NitPoint->Previous = NULL;
        TS_PackageNitPoint = NitPoint;
    
        BatPoint = (TS_BAT *)malloc(sizeof(TS_BAT));
        BatPoint->next = NULL;
        BatPoint->Previous = NULL;
        TS_PackageBatPoint = BatPoint;
    
        SdtPoint = (TS_SDT *)malloc(sizeof(TS_SDT));
        SdtPoint->next = NULL;
        SdtPoint->Previous = NULL;
        TS_PackageSdtPoint = SdtPoint;
    
        EitPoint = (TS_EIT *)malloc(sizeof(TS_EIT));
        EitPoint->next = NULL;
        EitPoint->Previous = NULL;
        TS_PackageEitPoint = EitPoint;
    }
    

    ​ 然后在对每个表进行解析的时候进行链表连接操作,这样我们就能将每种类型的表很好的保存起来。

    8.2 完整数据包的获取

    ​ 因为有些表的数据长度超过了188个字节,故而无法通过一个数据包来传送,会利用多个数据包来进行分段传送。但是TS流数据包的发送又是随机的,所以我们需要通过一定的过滤机制来过滤出类型正确,顺序正确的数据包。

    • 首先是通过payload_unit_start_indicator标志为’1’来确定含有首张表的数据包。
    • 然后用一个变量来记录下这个数据包的continuity_counter。因为它的下一个数据包将会时continuity_counter+1。
    • 再通过PID进行过滤,这样一来利用这三个过滤条件就可以得到我们想要的数据包。

    8.3 数据包的合并

    ​ 得到完整的数据包后,我们就要将它们有序的放在一起,然后通过结构体放进我们的链表里。

    • 这里我定义了一个容量很大的数组,足够放下一段1027个字节。
    • 然后通过过滤出的有序的顺序,利用memcpy函数将它们复制进这个新创立的数组中。
    • 最后将这个数组放入结构体中进行保存。

    8. 小结

    ​ 这里要注意几点:

    • 连接数组不能用strcat函数,因为他们会自动合并’\0’,所以当数组中存在数据0时就会被合并,从而导致数据错位。
    • 因为TS流是随机发送,有些数据包会乱序,因此要做好标记位,并且对数组及时清零,避免因残留数据而导致数据错位。
    • 这里对数据处理时,可以通过CRC校验位来防止对一张同样的表进行重复保存。

    9 Makefile的编写

    ​ 在代码的结构中,我将不同表的解析函数作为各自的.c文件,结构体和宏定义信息写为各自的.h文件。所以这次的Makefile语句就很简单。

    CFLAGS += -g
    src = $(wildcard ./*.c)
    obj = $(patsubst %.c,%.o,$(src))
    target = ts
    
    $(target) : $(obj)
    	gcc  $(obj) -o $(target) 
    
    %.o : %.c
    	gcc $(CFLAGS) -c $< -o $@
    
    .PHONY: clean
    clean:
    	rm -rf $(obj) $(target)
    

    10 总结

    • TS数据流的解析不单单只是拆解各种表,也不仅仅是打印出描述符的字节信息。而是需要知道TS流的产生,作用,含义;代码的构思,框架,优化。我们还要在这个过程中强化我们编程语言的基础,发散我们的编程思路,巩固所学知识,做到灵活运用。
    • 本次对TS流的解析工作还没有完全做完,在整体代码框架上出现了许多问题,例如:无法灵活提取想用的信息;没有很好的解决不同表的解析次数问题;以及内存分配过剩;代码执行整体效率过低。在解析内容上的问题,例如:TOT表、EIT表等还未完成解析函数的编写;描述符解析函数也未编写完整;无法很好的兼容不同的测试文件;整个代码会因为建立过多无效链表而导致浪费多余空间。在自身能力上的问题,例如:基础知识掌握不牢固;数据结构无法灵活运用;英文文档的阅读能力还要加强。
    • 总而言之,还是要不断学习理论知识,增加自己的代码编写量,仔细分析问题所在,找到解决问题的方法,继续完成本次任务。

    问题:

    1. TS现行流和非现行流区别。
    2. 一张表中,提取了204字节数据包,发现表中section_length大于204个字节,后续数据在那里?是否与非现行流相关。

    11 参考文献

    • 百度百科;
    • 《ETSI EN 300 468 V1.7.1 (2006-05)》;
    • 《is138181》;
    • 《DVB和PSI/SI的基础知识》;
    • 《【PSI/SI学习系列】从TS流到PAT和PMT》
    • 《PSI与SI入门详细介绍》
    展开全文
  • 数据仓库、数据湖的优劣势,湖仓一体架构的应用和优势等方面深度解析Lakehouse架构。

    Lakehouse由lake和house两个词组合而成,其中lake代表Delta Lake(数据湖),house代表data warehouse(数据仓库)。因此,Lakehouse架构就是数据湖和数据仓库的结合。数据仓库和数据湖各自都存在着很多不足,而Lakehouse的出现综合了两者的优势,弥补了它们的不足。

    数据仓库从上世纪 80 年代开始发展和兴起,它的初衷是为了支持BI系统和报表系统,而它的优势也就在于此。结构化的数据可以通过ETL来导入数据仓库,用户可以方便地接入报表系统以及BI系统。同时,它的数据管控能力也比较强。

    数据仓库对于数据 schema 的要求非常严格,很多数据仓库甚至也实现了 acid 事务等能力。但是数据仓库对于半结构化数据比如时序数据和日志,以及非结构化数据比如图片、文档等的支持是非常有限的,因此它不适用于类似于机器学习的应用场景。而且一般情况下,数据仓库都是专有系统,使用成本比较高,数据迁移和同步的灵活性比较低。

    因此,为了解决上述问题,数据湖的架构应运而生。

    数据湖架构的基础是将原始数据以文件的形式存储在像阿里云OSS、AWS S3 和 Azure blob storage 等对象存储系统上。相比于数据仓库使用的专有系统,使用这些对象存储的成本比较低。数据湖的另一个优势是能够对半结构化和非结构化的数据提供非常好的支持。因为数据可以以文件的形式直接存储在数据湖之中,所以数据湖在机器学习等场景中的应用就比较广泛。但是它对于 BI 和报表系统的支持比较差,通常情况下需要通过ETL将数据转存到实时数据库或数据仓库中,才能支持 BI 和报表系统,而这对于数据的实时性和可靠性都会产生负面的影响。

    综上,不论是数据仓库还是数据湖,都无法完全满足用户的需求。

    因此,在很多实际使用场景中,用户会将两者组合起来使用,但是这导致需要构建很多不同的技术栈来支持所有场景。

    比如对于数据分析,需要将结构化的数据输入到数据仓库,然后建立数据市场,对数据分析和 BI 提供支持;对于数据科学和机器学习的场景,需要把不同的数据比如结构化、半结构化以及非结构化的数据存储到数据湖中,经过数据清理,用来支持机器学习和数据科学等场景;而对于流式数据源,需要通过流式数据引擎存储到实时数据库中,同时还需要对数据湖里的数据进行 ETL 提取、转换和加载,来保证数据的质量。

    这导致需要很多不同的系统、不同的工具来支持各种架构,同时为了数据的互通(上图红线),还需要处理不同的专有数据格式之间的差异,以上流程都会大大影响整个系统的效率。

    而且,由于所有技术栈都是互相独立的,导致了维护和使用这些系统的团队也是分散的。比如,数据分析师主要使用数据仓库系统,而数据科学家主要使用数据湖系统,同时数据工程师也需要维护整个系统的不同团队,沟通成本比较高。此外,系统中维护了很多不同格式的数据副本,没有统一的管理数据模型,不同团队的数据很有可能会产生差异。

    因此,这种复杂的组合型数据系统不是一个好的解决方案。基于此,databricks提出了Lakehouse。Lakehouse的设计基于一个原则:实现一个适用于所有场景的统一平台。

    解决的办法是综合数据湖与数据仓库的能力——基于数据湖使用的对象存储,构建数据仓库拥有的数据管控能力。而这一切的关键就是其中的结构化事务层。

    此前,数据湖主要存在以下几个痛点:

    1. 读写并行,就算是追加写的模式也会产生很多问题。用户的期望是所有写操作能够事务性地被同时读到或者同时没有读到,而这是难以实现的,因为在分布式的对象存储上写多个文件,设置一个文件,数据的一致性都是不能完全被保证的。
    2. 数据的修改。由于安全合规等原因,用户会有强制性地修改已有数据的需求,特别是有时候需要根据过滤结果细粒度地修改某些数据。由于数据湖在数据管控能力上的不足,在数据湖上实现此需求往往需要使用全部扫描再重写的方式,成本比较高,速度也比较慢。
    3. 如果一个作业中途失败,而它产生的部分数据已经存入到数据库中,这也会导致数据的损坏。
    4. 批流混合输入。由于数据在批和流系统中都存在,可能会造成数据在两套系统中不一致,导致读取结果不一致。
    5. 存数据历史。有些用户需要保证数据查询的可重复性,方案之一是为了这个需求做很多重复的数据快照,但这会导致数据的存储和计算成本都大幅上升。
    6. 处理海量的元数据。大型数据湖元数据的数据量非常大,经常能够达到大数据的级别。很多数据湖采用的数据目录系统无法支持如此大量的元数据,这也限制了数据湖的扩展性。
    7. 大量小文件的问题。在数据不断输入的过程中,数据湖内会产生大量小文件,随着时间的推移,小文件的数量可能会越来越多,这会严重影响数据湖的读取性能。
    8. 性能问题。在数据湖上达到高性能不是一件容易的事。有的时候为了达到一定的性能要求,用户需要手动做一些性能的优化,比如数据分区等,而这些手动的操作又比较容易出错。
    9. 数据的查询管控。由于数据湖的开放性,确保查询权限合规也是需要解决的问题。
    10. 质量问题。前面很多点都会导致数据质量的问题。在大数据场景下,如何确保数据的正确性也是一个普遍的问题。

    而Delta Lake能够为Lakehouse带来数据质量、可靠性以及查询性能的提升。

    上述前五个问题都是关于数据可靠性,它们都可以通过Delta Lake的 acid 事务能力来解决。在Delta Lake上,每一个操作都是事务的,即每一个操作都是一个整体,要么整体成功,要么整体失败。如果 一个操作在中途失败,Delta Lake会负责将其写入的不完整数据清理干净。具体的实现方式是Delta Lake维护了包含所有操作的一个事务日志,能够保证数据与事务日志的一致性。

    如上图,某次写操作在某个表中添加了很多数据,这些数据被转换成了parquet格式的两个文件file1和file2。有了事务日志,读操作的时候就能够保证要么读不到这条日志,要么同时读到这两条记录,这样就保证了读取的一致性,解决了读写并行的问题。

    此外,有了事务日志后也可以对已有数据做细粒度的修改。比如下一次写操作对表中的某些数据进行修改,在事务日志中就会出现删除原有文件file1和添加修改后文件file3这样两条记录。同样,在读取的时候,这两条记录也会被同时读到或者忽略,使读取的一致性得到保证。

    针对第三点中途失败的作业,Delta Lake写入的事务性能够保证不完整的数据不会被成功写入。

    对于批流混合的输入数据,由于Spark天然支持批流一体,在写入时可以将批和流的数据写入到同一张表,避免了数据冗余及不一致性。

    由于事务日志保存了所有操作的历史记录,我们可以对之前某个时间点的历史数据进行查询。具体实现方法是:Delta Lake可以查到历史某个时间点对应的事务日志,并且根据历史的事务日志进行数据重放,得到该时间点的数据状态。这个能力被称为“时间旅行”。

    那么,Delta Lake是怎样处理海量元数据的呢?答案很简单,使用 Spark 来处理。所有Delta Lake的元数据均以开源parquet的格式存储,数据与元数据总是相伴相生,无需进行同步。使用 Spark 处理元数据,使得Delta Lake的元数据可以在理论上进行无限的扩展。

    Delta Lake还采用索引的机制来优化性能,它采用分区和不同过滤器等的机制,可以跳过数据的扫描。还采用了Z-ordering的机制,可以在对某个列进行优化的同时,使其他列性能牺牲最小化。

    为了解决大量小文件的问题,Delta Lake还可以在后台定期对数据布局进行自动优化。如果存储的小文件过多,会自动的将他们合并成大文件,这解决了数据湖中小文件越来越多的问题。

    对于数据查询的管控,Delta Lake实现了表级别的权限控制,也提供了权限设置 API,可以根据用户的权限动态对视图进行脱敏。

    最后,Delta Lake实现了schema的验证功能来保证数据质量。存在Delta Lake表中的所有数据都必须严格符合其对应的schema,它还支持在数据写入时做schema 的合并演化。当输入数据的 schema 发生变化的时候,Delta Lake可以自动对表的schema进行相应的演化。

    总的来说,Delta Lake是在数据湖存储之上,实现了数据仓库拥有的ACID事务特性、高性能数据治理能力以及数据质量保证。同时它是基于开放的存储格式,其本身也是开源的。此外,Delta Lake在架构设计上采用了多层的数据模型来简化设计,一层层逐步提高数据质量。

    刚刚进入Delta Lake的数据表,完全对应着数据的原始输入,数据质量比较低的,被称为Bronze表。Bronze表的数据保留也可以设置得长一些,以便从这些表中回溯历史数据。Bronze表中的数据经过过滤清理,就可以得到下一层的Silver表,可以使其与其他表或者维度表进行创意操作,进行数据的扩展。再往下一层,可以根据业务的需求对已经清理过滤好的数据进行聚合,得到Gold表,可以直接支持业务分析、报表等应用。

    可以看到,在Delta Lake架构中,数据质量是在不断提升的。相比于lambda 架构,它的设计优势在于在每一层都可以使用PDO统一的数据管道,以事务性的操作对表进行更新,还可以减少数据冗余,从而优化存储和计算的开销。

    总体而言,Lakehouse的架构优势有以下几个方面:

    1. Delta Lake的计算和存储天然分离,用户可以进行更灵活的资源调度。
    2. Lakehouse依赖于可以无限扩容的对象存储服务,其元数据的处理也依赖于高扩展性的 Spark 作业,用户无须关心存储容量的问题。
    3. 开放的数据格式可以让数据在不同系统之间的迁移更加顺畅。
    4. 与数据湖相同,Lakehouse同时支持结构化、半结构化与非结构化的数据。
    5. 批流一体。与 lambda 架构不同,Lakehouse能够做到真正的批流一体,从而简化数据的架构。

    Databricks公司与阿里云联手打造了全新的产品 databricks 数据洞察,简称DDI。

    Databricks 独家优化了databricks runtime引擎,也可以理解为Apache Spark的加强版,它与Delta Lake 融合进阿里云的整套生态系统中,与ECS、OSS、JindoFS进行了很好的结合,提供了全托管高性能的企业级 Spark平台,能够同时支持企业的商业洞察分析以及机器学习训练等。

    作者:张泊 Databricks 软件工程师

    原文链接

    本文为阿里云原创内容,未经允许不得转载。

    展开全文
  • MySQL多表查询(解析

    千次阅读 2018-04-19 16:59:52
    实际应用大部分情况下,查询语句都会涉及到多表格 :1.1 多表连接有哪些分类?1.2 针对这些分类有哪些连接方法?1.3 这些连接方法分别作用于哪些应用场景?这篇针对这三个点通过实例来讲述,目的是穷尽所有的场景和...

    一.前言 

    二.示例

    三.注意事项

    一.前言 

    上篇讲到Mysql中关键字执行的顺序,只涉及了一张表;实际应用大部分情况下,查询语句都会涉及到多张表格 :

    1.1 多表连接有哪些分类?

    1.2 针对这些分类有哪些连接方法?

    1.3 这些连接方法分别作用于哪些应用场景?

    这篇针对这三个点通过实例来讲述,目的是穷尽所有的场景和所有的方法,并且对每个方法的使用做实例。

    首先先列举本篇用到的分类(内连接,外连接,交叉连接)和连接方法(如下):

    A)内连接join,inner join

    B)外连接left join,left outer join,right join,right outer join,union

    C)交叉连接cross join


    二.下面以实例进行分析

    两张假设有两张表格A和B,把表格当作一个集合,那么表格中的记录就是集合中的一个元素。

    两张表格如下:

    TableA:TableB:

    2.1 内连接(只有一种场景)

    inner join 或者join(等同于inner join)

    [java]  view plain  copy
    1. select a.*, b.* from tablea a  
    2. inner join tableb b  
    3. on a.id = b.id  

    [java]  view plain  copy
    1. select a.*, b.* from tablea a  
    2. join tableb b  
    3. on a.id = b.id  

    结果如下:

    应用场景:

    这种场景下得到的是满足某一条件的A,B内部的数据;正因为得到的是内部共有数据,所以连接方式称为内连接。

    2.2 外连接(六种场景)

    2.2.1 left join 或者left outer join(等同于left join)

    [java]  view plain  copy
    1. select a.*, b.* from tablea a  
    2. left join tableb b  
    3. on a.id = b.id  

    或者

    [java]  view plain  copy
    1. select a.*, b.* from tablea a  
    2. left outer join tableb b  
    3. on a.id = b.id  

    结果如下,TableB中更不存在的记录填充Null:


    应用场景:


    这种场景下得到的是A的所有数据,和满足某一条件的B的数据;

    2.2.2  [left   join 或者left outer join(等同于left join)]  +  [where B.column is null]

    [java]  view plain  copy
    1. select a.id aid,a.age,b.id bid,b.name from tablea a  
    2. left join tableb b  
    3. on a.id = b.id  
    4. Where b.id is null  

    结果如下:


    应用场景:

    这种场景下得到的是A中的所有数据减去"与B满足同一条件 的数据",然后得到的A剩余数据;

    2.2.3  right join 或者fight outer join(等同于right join)

    [java]  view plain  copy
    1. select a.id aid,a.age,b.id bid,b.name from tablea a  
    2. right join tableb b  
    3. on a.id = b.id  
    结果如下,TableB中更不存在的记录填充Null:

    应用场景:


    这种场景下得到的是B的所有数据,和满足某一条件的A的数据;

    2.2.4 [left   join 或者left outer join(等同于left join)]  +  [where A.column is null]

    [java]  view plain  copy
    1. select a.id aid,a.age,b.id bid,b.name from tablea a  
    2. right join tableb b  
    3. on a.id = b.id  
    4. where a.id is null  
    结果如下:


    应用场景:


    这种场景下得到的是B中的所有数据减去 "与A满足同一条件 的数据“,然后得到的B剩余数据;

    2.2.5 full join (mysql不支持,但是可以用 left join  union right join代替)

    [java]  view plain  copy
    1. select a.id aid,a.age,b.id bid,b.name from tablea a  
    2. left join tableb b  
    3. on a.id = b.id  
    4. union  
    5. select a.id aid,a.age,b.id bid,b.name from tablea a  
    6. right join tableb b  
    7. on a.id = b.id  
    union过后,重复的记录会合并(id为2,3,4的三条记录),所以结果如下:

    应用场景:



    这种场景下得到的是满足某一条件的公共记录,和独有的记录

    2.2.6 full join + is null(mysql不支持,但是可以用 (left join + is null) union (right join+isnull代替)

    [java]  view plain  copy
    1. select a.id aid,a.age,b.id bid,b.name from tablea a  
    2. left join tableb b  
    3. on a.id = b.id  
    4. where b.id is null  
    5. union  
    6. select a.id aid,a.age,b.id bid,b.name from tablea a  
    7. right join tableb b  
    8. on a.id = b.id  
    9. where a.id is null  
    结果如下:


    应用场景:


    这种场景下得到的是A,B中不满足某一条件的记录之和

    注:上面共有其中七(2^3-1)种应用场景,还有一种是全空白,那就是什么都不查,七种情形包含了实际应用所有可能的场景

    2.3 交叉连接 (cross join)

    2.3.1 实际应用中还有这样一种情形,想得到A,B记录的排列组合,即笛卡儿积,这个就不好用集合和元素来表示了。需要用到cross join:

    [java]  view plain  copy
    1. select a.id aid,a.age,b.id bid,b.name from tablea a  
    2. cross join tableb b  


    2.3.2 还可以为cross  join指定条件 (where):

    [java]  view plain  copy
    1. select a.id aid,a.age,b.id bid,b.name from tablea a  
    2. cross join tableb b  
    3. where a.id = b.id  
    结果如下;

    注:这种情况下实际上实现了内连接的效果

    三 注意事项

    上面仍然存在遗漏,那就是mysql对sql语句的容错问题,即在sql语句不完全符合书写建议的情况,mysql会允许这种情况,尽可能地解释它:

    3.1 一般cross join后面加上where条件,但是用cross join+on也是被解释为cross join+where;

    3.2 一般内连接都需要加上on限定条件,如上面场景2.1;如果不加会被解释为交叉连接;

    3.3 如果连接表格使用的是逗号,会被解释为交叉连接;

    注:sql标准中还有union join和natural  inner join,mysql不支持,而且本身也没有多大意义,其结果可以用上面的几种连接方式得到

    总结:总结了mysql所有连接方法,其中有一些是之前没有注意到的问题,平时开发也都不外乎这些。

    展开全文
  • 在这次项目中我们使用Redis存储数据,使用一个key存储了图像(二维数组),然而redis数据量越大,在查询的时候速度越慢,所以我们要将数据尽量压缩,直接方式是存储为二进制,但是二进制的...
  • oracle数据库中批量把一张里面的数据插入到不同的中的方法
  • 某电商销售数据分析 SQL 面试题解析

    万次阅读 多人点赞 2020-04-22 21:56:53
    本文基于某电商销售数据分析 SQL 面试题,解析并给出了 MySQL 中的解决方法。依次使用了 GROUP BY 分组加聚合函数、HAVING 过滤和DISTINCT 去重、ORDER BY 排序加上 LIMIT 子句实现 Top-N 排行榜,以及窗口函数。
  • KITTI 3D目标检测数据解析(完整版)

    千次阅读 多人点赞 2021-07-18 08:45:35
    1. KITTI数据集概述 1.1 传感器配置   由于彩色相机成像过程中的拜耳阵列(Bayer Pattern)插值处理过程,彩色图像分辨率较低,而且对于光照敏感性不高,所以采集车配备了两组双目相机,一组灰度的,一组彩色的。...
  • 语义解析是近几年发展起来的一个NLP的分支,主要目的是将自然语言的文本描述,自动转成机器语言(SQL)语句。也称Text-to-SQL,nl2SQL等。随着知识图谱的发展,也逐渐孵化出很nl2Cypher算法。 NL2SQL的历史悠久,...
  • RAW数据格式解析

    万次阅读 2019-11-07 17:50:12
    RAM数据格式解析 Raw格式是sensor的输出格式,是未经处理过的数据,表示sensor接受 到的各种光的强度。 Raw数据在输出的时候是有一定的顺序的,一般为以下四种: 00: GR/BG 01: RG/GB 10: BG/GR 11: GB/RG 为什么...
  •   最近在用NuScenes 3D目标检测数据集,可能由于官方提供了解析工具包nuscenes-devkit,绝大多数博客只介绍了如何使用工具包进行数据解析和可视化,对于数据解析的内部逻辑就不是很关注了。   我本来是想搜寻...
  • Profinet协议解析-过程数据

    千次阅读 2020-12-27 14:04:17
    新的一篇文章,开始讲下一个... 过程数据,在整个链路上循环传输,每一台从站都与主站之间有着相关的交互连接,当网络中存在个从站,可想而知,数据量将会是庞大的,数据量就像路上的汽车,有限的带宽是马路,过多...
  • 导入excel数据时,日期格式(如2018/7/7)数据默认会被解析成double格式,解决方法如下:写一个公共类方法去校验并且转换正确的数值 package com.test.util; import java.text.DateFormat; import java.text....
  • Python解析多帧dicom数据

    千次阅读 2019-02-20 22:09:05
    但是,没有提供解析多帧图的示例。本文结合相关函数和DICOM知识做一个简单说明。 DICOM多帧数据存储 DICOM标准中关于多帧数据存储的最重要一部分说明是PS3.5 Annex A.4 A.4 Transfer Syntaxes For Encapsulation of ...
  • 【YOLOv5-6.x】数据增强代码解析

    千次阅读 2022-03-29 20:05:19
    文章目录前言像素级数据增强HSV色域变换旋转Rotation缩放Scale翻转Flip平移Translate剪切Shear透视Perspective三种常用的图片级数据增强MixupCutoutCutmixMosaic数据增强完整代码及数据Reference 前言 本文使用的...
  • java连接oracle数据库 json的生成及解析
  • 前言 2010年左右,还是在上学的时候,学过一门...如今,十年风云际会,大数据早已成了行业绕不开的话题,这其中我们或多或少会接触到很新兴的概念,例如数据湖、数据中台等,通过一些碎片化的学习,也是大概知...
  • Oracle 对比两张表数据是否一致

    万次阅读 2019-01-14 17:10:54
    有两张表CHECK_CITY_1和CHECK_CITY_2,现在要互相对比一下,两个表数据的差异,(即A中有哪些B里没有,B中有哪些A中没有) select t1.* from (select * from CHECK_CITY_1 minus select * from CHECK_CITY_2) t1 ...
  • 在机器学习中,mnist数据集是一个使用很广泛的数据集,一般教程中都是python对mnist数据集的解析,在本文中将采用C++语言进行手写数据集的数据提取。 环境准备: vs2015 OpenCV4.5.0 首先在mnist官网上下载mnist数据...
  • 大数据平台-元数据管理系统解析

    万次阅读 多人点赞 2018-03-14 09:25:24
    在前面的集成开发环境建设相关文章中,我们也提到过,元数据MetaData狭义的解释是用来描述数据数据,广义的来看,除了业务逻辑直接读写处理的那些业务数据,所有其它用来维持整个系统运转所需的信息/数据都可以叫...
  • Hive元数据表结构详解

    万次阅读 2018-06-14 21:59:37
    概述 我们知道Apache Hive 是构建在Apache Hadoop之上的...本文我们介绍Hive的原数据表,他默认是存储再derby中的,但是我们一般会修改会mysql。作者使用的也是mysql进行的管理。 Hive元数据表结构 hive-...
  • Logstash 支持不同的数据源头,在数据从源头到目标的过程中,Logstash提供了对数据处理的操作。对数据的操作需要配置filter的内容。 关于安装Logstash的安装可以看之前的文章安装Logstash并...解析键值对的数据 .
  • 数据表设计与mysql入门(一)

    万次阅读 多人点赞 2018-07-18 22:17:33
    这篇文章旨在带领新进的同学对基本的数据库知识、数据表逻辑设计、mysql数据库有一个基本了解,以便快速上手实际业务。
  • GPS数据解析以及模块连接开发板

    万次阅读 2017-05-24 15:52:18
    二、GPS数据解析: 1、数据解析:    NMEA 0183是美国国家海洋电子协会(National Marine Electronics Association )为海用电子设备制定的标准格式。目前业已成了GPS导航设备统一的RTCM(Radio ...
  • 最近在学习Hbase的使用,并仔细阅读了一篇官方推荐的博客,在这里就以一边翻译一边总结的方式和大家一起梳理一下HBase的数据模型和基本的设计思路。 官方推荐的博客原文地址:...
  • Python数据分析与可视化案例解析

    千次阅读 2021-06-03 07:52:38
    已知两个Excel表格:学生基本信息、期末考试成绩分别用于存放学生的基本信息(包括姓名、性别、班级)和学生的期末成绩(包括姓名、语文、数学、英语、总分),部分数据如下图所示(完整数据见学生基本信息....
  • Unity 解析视频流数据

    千次阅读 2019-08-14 18:36:53
    首先在AndroidStudio中将数据转换为yuv图片数据,然后传到Unity,Unity解析Yuv图片数据并显示 MediaCodec mCodec = MediaCodec.createDecoderByType("video/avc"); final MediaFormat mediaformat = MediaFormat....
  • ODB++ 数据格式解析软件

    千次阅读 2020-04-02 07:36:55
    Genesis2000软件是以此数据格式为基础. 与普通Gerber 274X格式的联系和区别 Gerber274X 是一种更加通用的PCB 数据描述格式. 基本支持ODB++格式的软件,都支持将ODB++格式转换为Gerber274X. Gerber格式数据, 是为...
  • 多表连接查询详细解析(详细版)

    千次阅读 2020-08-12 18:41:57
    1.为什么要张表,因为设计成一张表会造成数据冗余,浪费存储空间。 2.连接查询的分类 根据语法出现的年代来划分: SQL92(一些老的DBA可能还在使用这种语法。DBA:DataBase Administrator,数据库管理员) SQL99...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 230,123
精华内容 92,049
关键字:

多张数据表解析