三、RTMP推流的原理及实现_rcl 推流-程序员宅基地

技术标签: 流媒体开发  音视频  ffmpeg  

1、启动 SRS 服务器

sudo ./objs/srs -c conf/rtmp.conf

2、启动 RTMP 客户端(rtmpdump)

./RTMP_DEMO

注意:设置客户端请求的地址要和 SRS 服务启动的地址一样。
eg:客户端中设置的是:

#define RTMP_URL "rtmp://192.168.1.3/live/livestream"

查看客户端的 log,发现连接成功:

在这里插入图片描述

打印 SRS 服务端的 log:发现 RTMP Client 连接成功,因为这个客户端也是在本机上启动的,所以地址也是 198.168.1.3。

在这里插入图片描述

3、 RTMP 客户端开始推流

  • 客户端会将设置好的 pcm 音频数据+ yuv 视频数据推到 SRS 服务端。
  • 拉流端会从SRS 服务端拉流播放。
    在这里插入图片描述

4、RTMP客户端实现原理

RTMP 客户端进行推流,下面简单说下实现原理。

  • 初始化 RTMP推流对象:RTMPPusher

  • 解析 RTMP URL,eg :“rtmp://192.168.1.3/live/livestream”

    (1) 对url做合法性的校验,不合法直接返回;
    (2) 解析url,解析出:protocol、host、path、port。
    protocol:16
    host:192.168.1.3/live/livestream
    path:live/livestream
    port:1935

  • RTMPPusher发起请求Connect

    (1) 构建 Socket

    	struct sockaddr_in service;
    	memset(&service, 0, sizeof(struct sockaddr_in));
    	service.sin_family = AF_INET; // 指定(TCP/IP – IPv4)
    	service->sin_addr.s_addr = inet_addr(hostname) //指定目的ip地址:192.168.1.3
    	service->sin_port = htons(port); //指定目的端口号port:1935
    

    (2) 建立TCP连接

    	/*1、创建 Socket */
    	r->m_sb.sb_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    	/*2、开始3次握手,尝试建立 TCP 连接*/
    	connect(r->m_sb.sb_socket, service, sizeof(struct sockaddr));
    	/*3、成功建立连接*/
    	LogInfo("TCP Connect Success!!!");
    

    (3) 建立 RTMP Connection

    	int ret;
    	/* 1、RTMP  Hand Shake*/
    	ret = HandShake(r, TRUE)
    	if(ret == 0){
          
            RTMP_LogInfo(RTMP_LOGERROR, "%s, RTMP handshake failed.", __FUNCTION__);
            RTMP_Close(r);
            return FALSE;
    	}
    	RTMP_LogInfo(RTMP_LOGINFO, "%s, RTMP handshaked success", __FUNCTION__);
    
    	/*2、发送 connect 命令消息(Command Message)
    	*  如果熟悉RTMP协议的话,就知道在RTMP握手完成之后,
    	*  就要发送⼀个connect命令消息,⽤于客户端向服务器发送连接请求,
    	*  如果服务端同意连接,会返回连接成功的消息。
    	*/
    	ret = SendConnectPacket(r,cp);
    	if(ret==0){
          
            RTMP_LogInfo(RTMP_LOGERROR, "%s,Setp 3  RTMP connect failed.", __FUNCTION__);
            RTMP_Close(r);
            return FALSE;
    	}
    	/*3、到这里 RTMP Connection 成功建立*/
    	RTMP_LogInfo(RTMP_LOGINFO, "Setp 3 connect. msg cmd (typeID=20) (Connect) ok");
    	return TRUE;
    

    HandShake、SendConnectPacket都是 librtmp 里面函数,需要对 RTMP 协议熟悉才能看懂。

    • RTMP连接
      这里只是简要介绍一下RTMP 的连接过程,具体 RTMP 协议可以看Aodbe官方RTMP协议书。
      RTMP协议是应⽤层协议,是要靠底层可靠的传输层协议(通常是TCP)来保证信息传输的可靠性的。
      
      在传输层TCP协议的连接建⽴完成后,RTMP协议也要客户端和服务器通过“握⼿”来建⽴基于传输层
      链接之上的RTMP Connection链接。
      
      在握手完成后,先通过发送 connect 命令消息,来建立 NetConnection,才能进行会话。
      
    • RTMP Connection 握手(hand shake)
      要建⽴⼀个有效的RTMP Connection链接,⾸先要“握⼿”:客户端要向服务器发送C0,C1,C2(按序)三个 chunk,
      服务器向客户端发送S0,S1,S2(按序)三个chunk,然后才能进⾏有效的信息传输。
      本身并没有规定这6个Message的具体传输顺序,但RTMP协议的实现者需要保证这⼏点:
      1、客户端要等收到S1之后才能发送C2;
      2、客户端要等收到S2之后才能发送其他信息(控制信息和真实⾳视频等数据);
      3、服务端要等到收到C0之后发送S1;
      4、服务端必须等到收到C1之后才能发送S2;
      5、服务端必须等到收到C2之后才能发送其他信息(控制信息和真实⾳视频等数据)
      

    (4) 建立 RTMP Stream
    通过发送 Create Stream 命令消息,来建立 NetStream 信息通道 。

    Netstream建⽴在NetConnection(第 3 步)之上,通过NetConnection的createStream命令创建,⽤于传输具体的⾳频、视频等信息。在传输层协议之上只能连接⼀个NetConnection,但⼀个NetConnection可以建⽴ 多个NetStream来建⽴不同的流通道传输数据。

    当发送完CreateStream消息后,解析服务器返回的消息会得到⼀个stream ID, 这个ID也就是以后和服 务器通信的 message stream ID, ⼀般返回的是1,不固定。

        if (!RTMP_ConnectStream(rtmp_, 0))
        {
          
            LogInfo("RTMP_ConnectStream failed");
            return FALSE;
        }
    

    (5)音频编码、音频重采样、视频编码器初始化的过程

    	// 初始化publish  (推流起始时间 )
        AVPublishTime::GetInstance()->Rest();
    
    	// 设置音频编码器,并初始化
    	audio_encoder_ = new AACEncoder();
        Properties  aud_codec_properties;
        aud_codec_properties.SetProperty("sample_rate", audio_sample_rate_);
        aud_codec_properties.SetProperty("channels", audio_channels_);
        aud_codec_properties.SetProperty("bitrate", audio_bitrate_);
        if(audio_encoder_->Init(aud_codec_properties) != RET_OK)
        {
          
            LogError("AACEncoder Init failed");
            return RET_FAIL;
        }
        aac_fp_ = fopen("push_dump.aac", "wb");
        if(!aac_fp_)
        {
          
            LogError("fopen push_dump.aac failed");
            return RET_FAIL;
        }
    	
    	//设置音频重采样:将 s16 交错模式的 PCM 数据 ----> AV_SAMPLE_FMT_FLT 棋盘格式的数据
        audio_resampler_ = new AudioResampler();
        AudioResampleParams aud_params;
        aud_params.logtag = "[audio-resample]";
        aud_params.src_sample_fmt = (AVSampleFormat)mic_sample_fmt_;
        aud_params.dst_sample_fmt = (AVSampleFormat)audio_encoder_->get_sample_format();
        aud_params.src_sample_rate = mic_sample_rate_;
        aud_params.dst_sample_rate = audio_encoder_->get_sample_rate();
        aud_params.src_channel_layout = av_get_default_channel_layout(mic_channels_);
        aud_params.dst_channel_layout = audio_encoder_->get_channel_layout();
        aud_params.logtag = "audio-resample-encode";
        audio_resampler_->InitResampler(aud_params);
    	
    	//设置视频编码器
        video_encoder_ = new H264Encoder();
        Properties  vid_codec_properties;
        vid_codec_properties.SetProperty("width", video_width_);
        vid_codec_properties.SetProperty("height", video_height_);
        vid_codec_properties.SetProperty("fps", video_fps_);
        vid_codec_properties.SetProperty("b_frames", video_b_frames_);
        vid_codec_properties.SetProperty("bitrate", video_bitrate_);
        vid_codec_properties.SetProperty("gop", video_gop_);
        if(video_encoder_->Init(vid_codec_properties) != RET_OK)
        {
          
            LogError("H264Encoder Init failed");
            return RET_FAIL;
        }
        h264_fp_ = fopen("push_dump.h264", "wb");
        if(!h264_fp_)
        {
          
            LogError("fopen push_dump.h264 failed");
            return RET_FAIL;
        }
    
    

    (6)构造 FLV 格式,因为RTMP推流是以FLV的格式去发送
    FLV 格式中一个重要的字段:metaData
    在这里插入图片描述
    可以从上图看出 FLV 的 metaData 字段,保存着FLV 视频和音频的元信息。

        FLVMetadataMsg *metadata = new FLVMetadataMsg();
        // 设置视频相关
        metadata->has_video = true;
        metadata->width = video_encoder_->get_width();
        metadata->height = video_encoder_->get_height();
        metadata->framerate = video_encoder_->get_framerate();
        metadata->videodatarate = video_encoder_->get_bit_rate();
        // 设置音频相关
        metadata->has_audio = true;
        metadata->channles = audio_encoder_->get_channels();
        metadata->audiosamplerate = audio_encoder_->get_sample_rate();
        metadata->audiosamplesize = 16;
        metadata->audiodatarate = 64;
        metadata->pts = 0;
        
        /* metadata push到消息队列 */
        rtmp_pusher->Post(RTMP_BODY_METADATA, metadata, false);
    

    (7) 设置音频捕获器
    音频捕获器的作用:
    1、打开要发送的pcm文件
    2、启动一个线程,循环发送pcm数据到pcm_buf
    3、执行回调消费pcm_buf
    4、将pcm_buf重采样后,进行编码。
    代码只看关键地方:

    	//音频捕获器初始化,在这个函数里面会打开pcm文件,得到fd:pcm_fp_
        audio_capturer_->Init(aud_cap_properties)
    
    	//Start里面会启动线程执行Loop操作
      	audio_capturer_->Start()
    
    	//Loop操作:会从打开的pcm文件中持续读取数据到pcm_buf中,然后传入回调callback_get_pcm_消费。
    	void AudioCapturer::Loop()
    	{
          		
    	    int nb_samples = 1024;
    	    pcm_total_duration_ = 0;
    	    pcm_start_time_ = TimesUtil::GetTimeMillisecond();
    	    while(true)
    	    {
          
    	        if(request_exit_)
    	            break;
    	         /* 每次读取1024个sample到pcm_buf中*/   
    	        if(readPcmFile(pcm_buf_, nb_samples) == 0)
    	        {
          
    	            if(!is_first_frame_) {
          
    	                is_first_frame_ = true;
    	                LogInfo("%s:t%u", AVPublishTime::GetInstance()->getAInTag(),
    	                        AVPublishTime::GetInstance()->getCurrenTime());
    	            }
    	            if(callback_get_pcm_)
    	            {
              
    	                //执行回调,callback_get_pcm,将pcm_buf传入然后消费。
    	                callback_get_pcm_(pcm_buf_, nb_samples *4); // 2通道 s16格式
    	            }
    	        }
    	        std::this_thread::sleep_for(std::chrono::milliseconds(2));
    	    }
    	    closePcmFile();
    	}
    
    	
    	//执行callback_get_pcm_回调
    	/*
    	* pcm:就是pc_buf
    	* size:4096 ==> 1024个样本 * 2通道 * 16位采样格式
    	*/
    	void PushWork::PcmCallback(uint8_t *pcm, int32_t size){
          
    			
    		    if(need_send_audio_spec_config)
    		    {
          
    		        need_send_audio_spec_config = false;
    		        /* 生成 AudioSpecMsg 消息,推给服务端。
    		        */
    		        AudioSpecMsg *aud_spc_msg = new AudioSpecMsg(audio_encoder_->get_profile(),
    		                                                     audio_encoder_->get_channels(),
    		                                                     audio_encoder_->get_sample_rate());
    		        aud_spc_msg->pts_ = 0;
    		        /*
    		        * 对此大家可能很困惑?为什么要先把 Audio的配置信息(采样率、通道数...)先单独推送到服务端,
    		        * 为什么不和下面的数据一起推过去?其实在视频数据包推送哪里也会有类似的处理,它需要先把关键帧
    		        * 的 sps 和 pps 先单独推送到服务端。
    		        * 
    		        * 因为:假设sps和pps作为配置信息存放在关键帧的报文中推送到服务端,如果出现网络错误导致
    				* 这个关键帧没有到达服务端,缺少了 sps 和 pps,那后面推送的 B 帧、P 帧也无法播放了。
    		        * 
    		        * 针对上面问题:在推流的时候,sps 和 pps 不和关键帧放在一起往服务端推送,而是先把 sps+pps 数
    				* 据成功推到服务端后,才能推后面的帧。如果 sps、pps 推失败了,后面推多少都是白推。
    		        */
    		        rtmp_pusher->Post(RTMP_BODY_AUD_SPEC, aud_spc_msg);
    		    }
    		   
    		    // 1、创建一个AVFrame,参数(sample_rate、channel_layout、nb_samples、nb_channels)
    		    //   根据重采样后的pcm数据格式进行设置
    		    // 2、将pcm buf中的数据填充到AVFrame的data中
    		    // 3、取出AVFrame的data中的pcm数据,开始进行重采样
    		    // 4、将重采样后的数据写入AVAudioFifo
    		    auto ret = audio_resampler_->SendResampleFrame(pcm, size);
    			
    			// 1、构造一个AVFrame,参数根据重采样pcm后的格式进行设置
    			// 2、从AVAudioFifo中取出pcm数据,填充到这个AVFrame的data中(有点困惑,上面的重采样的AVFrame其实可以共用到这里,为什么要经过一手AVAudioFifo?)
    			// 3、resampled_frames.push_back(frame)
    			vector<shared_ptr<AVFrame>> resampled_frames;
    			ret = audio_resampler_->ReceiveResampledFrame(
                    resampled_frames,
                    audio_encoder_->GetFrameSampleSize());
    			
    			// 音频数据编码
    			// 1、将重采样后的pcm进行AAC编码
    	        int aac_size = audio_encoder_->Encode(resampled_frames[i].get(),
                                            aac_buf_, AAC_BUF_MAX_LENGTH);
    
    	}
    

    AudioSpec是FLV Audio Tag区域的字段,当AACPacketType ==0,就代表是这个Audio Tag里面装的是音频的配置信息。
    当AACPacketType ==1 就代表Audio Tag区域里面是音频数据。
    在这里插入图片描述

    (8)构造ADTS流发送到服务端

    ADTS是AAC音频的传输流格式
    在这里插入图片描述
    在这里插入图片描述
    1、很显然,下面先构造ADTS的数据,然后写到aac_buf_
    2、然后构造AudioRawMsg,将aac_buf_的数据拷贝给AudioRawMsg中的data
    3、发送AudioRawMsg消息。

        int aac_size = audio_encoder_->Encode(resampled_frames[i].get(),
                                              aac_buf_, AAC_BUF_MAX_LENGTH);
        if(aac_size > 0)
        {
          
            if(aac_fp_)
            {
          
                uint8_t adts_header[7]; //ADTS Header占7个字节
                /* 构造ADTS Header*/
                audio_encoder_->GetAdtsHeader(adts_header, aac_size);
                fwrite(adts_header, 1, 7, aac_fp_);
                fwrite(aac_buf_, 1, aac_size, aac_fp_);
                fflush(aac_fp_);
            }
            AudioRawMsg *aud_raw_msg = new AudioRawMsg(aac_size + 2);
            // 打上时间戳
            aud_raw_msg->pts = AVPublishTime::GetInstance()->get_audio_pts();
            aud_raw_msg->data[0] = 0xaf;
            aud_raw_msg->data[1] = 0x01;    // 1 =  raw data数据
            memcpy(&aud_raw_msg->data[2], aac_buf_, aac_size);
            rtmp_pusher->Post(RTMP_BODY_AUD_RAW, aud_raw_msg);
            LogDebug("PcmCallback Post");
        }
    
    ADTS流 = ADTS Header(7字节) + Audio AAC Data(aac_size)
    ADTS Header:
    	1、sampling_frequency_index: 比如我设置pcm采样率是48khz,那么index就是3。
        2、syncword:同步头占12位,总是0xFFF
        3、ID: MPEG标示符,0表示MPEG-4 , 1表示MPEG-2,我这里是1
        4、Layer:总是0x00
        5、profile:表示使用那个级别的AAC
        6、protection_absent: 表示是否误码校验
        ...
    

    (8) 设置视频捕获器

    初始化

        double video_frame_duration = 1000.0 / video_encoder_->get_framerate();
        LogInfo("video_frame_duration:%lf", video_frame_duration);
        AVPublishTime::GetInstance()->set_video_pts_strategy(AVPublishTime::PTS_RECTIFY);//帧间隔矫正
        video_capturer = new VideoCapturer();
        Properties  vid_cap_properties;
        vid_cap_properties.SetProperty("video_test", 1);
        vid_cap_properties.SetProperty("input_yuv_name", input_yuv_name_);
        vid_cap_properties.SetProperty("width", desktop_width_);
        vid_cap_properties.SetProperty("height", desktop_height_);
        if(video_capturer->Init(vid_cap_properties) != RET_OK)
        {
          
            LogError("VideoCapturer Init failed");
            return RET_FAIL;
        }
    

    启动Loop线程,循环读取yuv文件

        yuv_buf_size = width_ * height_ * 1.5;
    	yuv_buf_ = new uint8_t[yuv_buf_size];
        while(true)
        {
          
            if(request_exit_)
                break;
            if(readYuvFile(yuv_buf_, yuv_buf_size) == 0)
            {
          
                if(!is_first_frame_) {
          
                    is_first_frame_ = true;
                    LogInfo("%s:t%u", AVPublishTime::GetInstance()->getVInTag(),
                            AVPublishTime::GetInstance()->getCurrenTime());
                }
                if(callable_object_)
                {
          
                    callable_object_(yuv_buf_, yuv_buf_size);
                }
            }
    
            std::this_thread::sleep_for(std::chrono::milliseconds(2));
        }
    

    执行回调callable_object_:YuvCallback消费数据yuv_buf

        if(need_send_video_config)
        {
          	
        	/* 发送videoConfig Message */
            need_send_video_config = false;
            VideoSequenceHeaderMsg * vid_config_msg = new VideoSequenceHeaderMsg(
                        video_encoder_->get_sps_data(),
                        video_encoder_->get_sps_size(),
                        video_encoder_->get_pps_data(),
                        video_encoder_->get_pps_size()
                        );
            vid_config_msg->nWidth = video_width_;
            vid_config_msg->nHeight = video_height_;
            vid_config_msg->nFrameRate = video_fps_;
            vid_config_msg->nVideoDataRate = video_bitrate_;
            vid_config_msg->pts_ = 0;
            rtmp_pusher->Post(RTMP_BODY_VID_CONFIG, vid_config_msg);
        }	
    
    

    FLV的Video Tag Data部分,如下:
    AVCPacketType ==0 时,data部分就是Specific,我打开这段flv文件,是h264编码(codecId = 7)
    可以在Specific里面看到,H264编码中关键的参数:sps、pps
    所以:Specific配置部分就是NALU的头
    在这里插入图片描述

    开始对视频yuv数据进行编码,编码好的packet存放到video_nalu_buf
    然后将video_nalu_buf中的packet封装成NALU,通过Video Message发送出去。

    if(video_encoder_->Encode(yuv, 0, video_nalu_buf, video_nalu_size_) == 0)
    {
          
        // 获取到编码数据
        NaluStruct *nalu = new NaluStruct(video_nalu_buf, video_nalu_size_);
        nalu->type = video_nalu_buf[0] & 0x1f;
        nalu->pts = AVPublishTime::GetInstance()->get_video_pts();
        rtmp_pusher->Post(RTMP_BODY_VID_RAW, nalu);
        LogDebug("YuvCallback Post");
    }
    

5、WireShark分析

在这里插入图片描述
在这里插入图片描述

6、总结

本文简要介绍了RTMP推流的原理以及过程,只希望对RTMP的推流过程以及原理建立一个简单的认识而已,建议配合RTMP的协议规范和rtmpdump代码进行阅读。本文讲的比较简单,对FLV格式、ADTS、NALU、RTMP的Message机制都忽略不讲,这每一个都是长篇大论,不可能在一文中全部讲出,这也不是本文主要表达的东西。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_38800014/article/details/135257192

智能推荐

从零开始搭建Hadoop_创建一个hadoop项目-程序员宅基地

文章浏览阅读331次。第一部分:准备工作1 安装虚拟机2 安装centos73 安装JDK以上三步是准备工作,至此已经完成一台已安装JDK的主机第二部分:准备3台虚拟机以下所有工作最好都在root权限下操作1 克隆上面已经有一台虚拟机了,现在对master进行克隆,克隆出另外2台子机;1.1 进行克隆21.2 下一步1.3 下一步1.4 下一步1.5 根据子机需要,命名和安装路径1.6 ..._创建一个hadoop项目

心脏滴血漏洞HeartBleed CVE-2014-0160深入代码层面的分析_heartbleed代码分析-程序员宅基地

文章浏览阅读1.7k次。心脏滴血漏洞HeartBleed CVE-2014-0160 是由heartbeat功能引入的,本文从深入码层面的分析该漏洞产生的原因_heartbleed代码分析

java读取ofd文档内容_ofd电子文档内容分析工具(分析文档、签章和证书)-程序员宅基地

文章浏览阅读1.4k次。前言ofd是国家文档标准,其对标的文档格式是pdf。ofd文档是容器格式文件,ofd其实就是压缩包。将ofd文件后缀改为.zip,解压后可看到文件包含的内容。ofd文件分析工具下载:点我下载。ofd文件解压后,可以看到如下内容: 对于xml文件,可以用文本工具查看。但是对于印章文件(Seal.esl)、签名文件(SignedValue.dat)就无法查看其内容了。本人开发一款ofd内容查看器,..._signedvalue.dat

基于FPGA的数据采集系统(一)_基于fpga的信息采集-程序员宅基地

文章浏览阅读1.8w次,点赞29次,收藏313次。整体系统设计本设计主要是对ADC和DAC的使用,主要实现功能流程为:首先通过串口向FPGA发送控制信号,控制DAC芯片tlv5618进行DA装换,转换的数据存在ROM中,转换开始时读取ROM中数据进行读取转换。其次用按键控制adc128s052进行模数转换100次,模数转换数据存储到FIFO中,再从FIFO中读取数据通过串口输出显示在pc上。其整体系统框图如下:图1:FPGA数据采集系统框图从图中可以看出,该系统主要包括9个模块:串口接收模块、按键消抖模块、按键控制模块、ROM模块、D.._基于fpga的信息采集

微服务 spring cloud zuul com.netflix.zuul.exception.ZuulException GENERAL-程序员宅基地

文章浏览阅读2.5w次。1.背景错误信息:-- [http-nio-9904-exec-5] o.s.c.n.z.filters.post.SendErrorFilter : Error during filteringcom.netflix.zuul.exception.ZuulException: Forwarding error at org.springframework.cloud..._com.netflix.zuul.exception.zuulexception

邻接矩阵-建立图-程序员宅基地

文章浏览阅读358次。1.介绍图的相关概念  图是由顶点的有穷非空集和一个描述顶点之间关系-边(或者弧)的集合组成。通常,图中的数据元素被称为顶点,顶点间的关系用边表示,图通常用字母G表示,图的顶点通常用字母V表示,所以图可以定义为:  G=(V,E)其中,V(G)是图中顶点的有穷非空集合,E(G)是V(G)中顶点的边的有穷集合1.1 无向图:图中任意两个顶点构成的边是没有方向的1.2 有向图:图中..._给定一个邻接矩阵未必能够造出一个图

随便推点

MDT2012部署系列之11 WDS安装与配置-程序员宅基地

文章浏览阅读321次。(十二)、WDS服务器安装通过前面的测试我们会发现,每次安装的时候需要加域光盘映像,这是一个比较麻烦的事情,试想一个上万个的公司,你天天带着一个光盘与光驱去给别人装系统,这将是一个多么痛苦的事情啊,有什么方法可以解决这个问题了?答案是肯定的,下面我们就来简单说一下。WDS服务器,它是Windows自带的一个免费的基于系统本身角色的一个功能,它主要提供一种简单、安全的通过网络快速、远程将Window..._doc server2012上通过wds+mdt无人值守部署win11系统.doc

python--xlrd/xlwt/xlutils_xlutils模块可以读xlsx吗-程序员宅基地

文章浏览阅读219次。python–xlrd/xlwt/xlutilsxlrd只能读取,不能改,支持 xlsx和xls 格式xlwt只能改,不能读xlwt只能保存为.xls格式xlutils能将xlrd.Book转为xlwt.Workbook,从而得以在现有xls的基础上修改数据,并创建一个新的xls,实现修改xlrd打开文件import xlrdexcel=xlrd.open_workbook('E:/test.xlsx') 返回值为xlrd.book.Book对象,不能修改获取sheett_xlutils模块可以读xlsx吗

关于新版本selenium定位元素报错:‘WebDriver‘ object has no attribute ‘find_element_by_id‘等问题_unresolved attribute reference 'find_element_by_id-程序员宅基地

文章浏览阅读8.2w次,点赞267次,收藏656次。运行Selenium出现'WebDriver' object has no attribute 'find_element_by_id'或AttributeError: 'WebDriver' object has no attribute 'find_element_by_xpath'等定位元素代码错误,是因为selenium更新到了新的版本,以前的一些语法经过改动。..............._unresolved attribute reference 'find_element_by_id' for class 'webdriver

DOM对象转换成jQuery对象转换与子页面获取父页面DOM对象-程序员宅基地

文章浏览阅读198次。一:模态窗口//父页面JSwindow.showModalDialog(ifrmehref, window, 'dialogWidth:550px;dialogHeight:150px;help:no;resizable:no;status:no');//子页面获取父页面DOM对象//window.showModalDialog的DOM对象var v=parentWin..._jquery获取父window下的dom对象

什么是算法?-程序员宅基地

文章浏览阅读1.7w次,点赞15次,收藏129次。算法(algorithm)是解决一系列问题的清晰指令,也就是,能对一定规范的输入,在有限的时间内获得所要求的输出。 简单来说,算法就是解决一个问题的具体方法和步骤。算法是程序的灵 魂。二、算法的特征1.可行性 算法中执行的任何计算步骤都可以分解为基本可执行的操作步,即每个计算步都可以在有限时间里完成(也称之为有效性) 算法的每一步都要有确切的意义,不能有二义性。例如“增加x的值”,并没有说增加多少,计算机就无法执行明确的运算。 _算法

【网络安全】网络安全的标准和规范_网络安全标准规范-程序员宅基地

文章浏览阅读1.5k次,点赞18次,收藏26次。网络安全的标准和规范是网络安全领域的重要组成部分。它们为网络安全提供了技术依据,规定了网络安全的技术要求和操作方式,帮助我们构建安全的网络环境。下面,我们将详细介绍一些主要的网络安全标准和规范,以及它们在实际操作中的应用。_网络安全标准规范