moviepy音视频剪辑:视频剪辑基类VideoClip的属性及方法详解

☞ ░ 前往老猿Python博文目录

一、概述

在《moviepy音视频剪辑:moviepy中的剪辑基类Clip详解》和《moviepy音视频剪辑:moviepy中的剪辑基类Clip的属性和方法详解》介绍了剪辑相关类及类关系。可以看到视频剪辑类VideoClip是其中非常重要的一个剪辑类,它主要有六个直接子类(VideofileClip、 ImageSequenceClip、CompositeVideoClip、ImageClip、DataVideoClip、UpdatedVideoClip)和两个间接子类(ColorClip, TextClip)。

二、VideoClip的构造方法和属性

2.1、构造方法

2.1.1、构造方法语法:

__init__(self, make_frame=None, ismask=False, duration=None, has_constant_size=True)

2.1.2、参数释义

  • make_frame:帧的构建方法,帧的构建方法用于根据时间构建帧,该方法是get_frame获取帧时调用的方法。帧的构建可以从已有剪辑中获取或变换,也可以代码自己填充;
  • ismask:是否作为遮罩使用
  • duration:构建剪辑的时长
  • has_constant_size:表示是否所有帧大小都是相同,如果是动态图像,该值必须为False,该值用于增加遮罩时使用,如果固定大小,遮罩大小就是剪辑的大小,如果不是,则遮罩大小需要加遮罩的视频帧取对应帧的大小。

2.2、size属性

剪辑的大小和分辨率,是一个二元组,内容为:(宽度,高度),单位是像素,直接通过属性名访问。

2.3、w,h属性

w,h属性即剪辑的宽和高,单位是像素,实际这两个属性就是从size属性获取的。属性访问直接通过属性名访问。

2.4、ismask属性

布尔类型表示剪辑是否为遮罩,该属性在构造方法中传入。

2.5、make_frame属性

帧的构建方法,通过构造方法传入或set_make_frame方法设置,帧的构建方法用于根据时间构建帧,该方法是get_frame获取帧时调用的方法。帧的构建可以从已有剪辑中获取或变换,也可以代码自己填充。

2.6、mask属性

当一个视频帧有遮罩时,使用该属性记录遮罩的剪辑。如果为None,则视频剪辑完全不透明。可以通过add_mask、set_opacity等方法来构建剪辑的遮罩,也可以通过set_mask来将已有的剪辑设置为视频剪辑的遮罩。

2.7、aspect_ratio属性

aspect_ratio属性为剪辑的纵横比,实际上就是剪辑的宽/高。注意该属性为浮点数,只读,通过属性名访问。

三、VideoClip的访问方法

3.1、save_frame方法

save_frame方法调用语法如下:
save_frame(self, filename, t=0, withmask=True)

该方法用于将t指定时刻位置的帧保存到指定图像文件,t 表示方法可以是如下四种之一:

  1. 秒,为一个浮点数数字,如75.35
  2. 分钟和秒组成的元组,如(1,15.35)
  3. 时、分、秒组成的元组,如(0,1,15.35)
  4. 用冒号分隔的时间字符串,如‘0:1:15.35’

如果withmask为True,对应帧的遮罩会被写入图片的alpha通道层,仅对PNG图像有效。

注:

图像的alpha通道一般用作不透明度参数。如果一个像素的alpha通道数值为0%,那它就是完全透明的(也就是看不见的),而数值为100%则意味着一个完全不透明的像素(传统的数字图像)。在0%和100%之间的值则使得像素可以透过背景显示出来,就像透过玻璃(半透明性),它使数码合成变得容易。alpha通道值可以用百分比、整数或者像RGB参数那样用0到1的实数表示。

3.2、write_videofile方法

write_videofile方法用于将视频剪辑输出到文件,调用语法如下:

write_videofile(self, filename, fps=None, codec=None,
                        bitrate=None, audio=True, audio_fps=44100,
                        preset="medium",
                        audio_nbytes=4, audio_codec=None,
                        audio_bitrate=None, audio_bufsize=2000,
                        temp_audiofile=None,
                        rewrite_audio=True, remove_temp=True,
                        write_logfile=False, verbose=True,
                        threads=None, ffmpeg_params=None,
                        logger='bar')

参数说明如下:
  • filename:视频文件名,只要是ffmpeg支持的视频文件如 .ogv, .mp4, .mpeg, .avi, .mov等都可以
  • fps:帧率,每秒编码的帧数
  • codec:用于图像编码的编解码器,可以是ffmpeg支持的任何编解码器。如果文件名的扩展名为“.mp4”、“.ogv”、“.webm”,则会相应地设置编解码器,但如果不喜欢默认值,则仍可以进行设置。对于其他扩展名,必须相应地设置输出文件名。一些常用的编解码器如下:
  • ‘libx264’:视频压缩效果好的一款编解码器,MP4的缺省编解码器,视频质量通过bitrate参数调节
  • ‘mpeg4’:一种可选的MP4编解码器,可以替代’libx264’,可以获得更好的视频质量
  • ‘rawvideo’:完美的视频质量,但文件会巨大,对应视频文件为’.avi’
  • ‘png’:完美的视频质量,对应视频文件为’.avi’,但文件大小比’rawvideo’小
  • ‘libvorbis’:是一种完全开放、免费的编解码器,有不错的视频格式,但是要不广,对应视频文件为’.ogv’
  • ‘libvpx’:一种很适合在HTML5中使用的网络视频轻量级编开源解码器,对应视频文件为’.webm’
  • bitrate:输出视频的比特率,也即码率BPS(Bits Per Second),指每秒传送的数据位数
  • audio:可以为True、False或文件名,如果True且剪辑附加了音频,则音频将作为视频的音频保存,如果为False则不保存音频,如果为音频文件名则将此音频文件将作为视频的音频
  • audio_fps:声音的采样频率
  • preset:设置FFMPEG用于优化压缩的时间。字符串类型,可选值有:ultrafast、superfast、veryfast、faster、fast、medium、slow、slower、veryslow、 placebo。请注意,这不会影响视频的质量,只影响视频文件的大小。所以如果赶时间而文件大小不是很重要可以设置为ultrafast
  • audio_nbytes:音频的采用的位数,对应基于字节为单位就是声道数;
  • audio_codec:音频解码器,例如’.mp3’的’libmp3lame’、‘ogg’的’libvorbis’、 ‘m4a’的’libfdk_aac’、 ‘pcm_s16le’ 16位声音和’pcm_s32le’的32位声音。默认值为“libmp3lame”,除非视频扩展名为“ogv”或“webm”,在这2种情况下,默认值为“libvorbis”。
  • audio_bitrate:音频比特率,字符串形式,如“50k”、“500k”、“3000k”,用于将确定输出文件中音频的大小/质量。请注意,这主要是一个指示性目标,输出文件的比特率不一定会按此设置。
  • audio_bufsize:音频缓冲区大小
  • temp_audiofile:如果输出由音频,则该参数用于指定要生成并合并到电影中的临时音频文件的名称,如果没有指定则用缺省模式的临时文件名
  • rewrite_audio:这个参数目前没有作用,估计是为了兼容以前的版本
  • remove_temp:是否删除临时文件
  • write_logfile:如果为True,将为音频和视频输出记录日志文件。日志文件将以“.log”结尾,包含输出文件的名称
  • verbose:已经废弃使用,留下来是为了兼容性,以前用于打开/关闭消息。现在使用logger=None。
  • threads:用于ffmpeg的线程数,可以加快多核计算机上视频输出的速度
  • ffmpeg_params:需额外传递的其他ffmpeg参数,用列表传递,形如:[’-option1’,‘value1’,’-option2’,‘value2’]
  • logger:字符串类型,"bar"表示进度条、None 表示不设置、或任何程序日志记录器的名字

3.3、write_images_sequence方法

write_images_sequence方法用于将剪辑输出到一系列文件中,调用语法如下:

write_images_sequence(self, nameformat, fps=None, verbose=True,withmask=True, logger='bar')
参数说明如下:
  • nameformat:输出格式和文件名规则,指定了文件系列序号的数字格式和图片类型扩展名的文件名,例如文件名的“frame%03d.png”表示文件名开头为“frame”,后面“%03d”表示文件顺序号以3位数字来顺序编号,文件名后缀“png”表示编码格式为PNG。文件名格式还可以带路径方式,如“目录名\f%04d.jpeg”等。但要求目录名必须已存在,另外如果输出的文件数比设置的序列号范围要多,则会自动往上加1扩展,直到所有内容都输出完成
  • fps:每秒输出帧数,如果没指定则按剪辑的fps进行输出
  • withmask:是否将遮罩作为图像的alpha通道输出,仅对png图像格式有效
  • verbose:是否输出处理信息
  • logger:字符串类型,"bar"表示进度条、None 表示不设置、或任何程序日志记录器的名字

3.4、write_gif方法

write_gif将剪辑转换成gif动画输出到文件中,调用语法:

def write_gif(self, filename, fps=None, program='imageio',
                  opt='nq', fuzz=1, verbose=True,
                  loop=0, dispose=False, colors=None, tempfiles=False,
                  logger='bar'):
参数说明如下:
  • program:用于转换的软件,可以是“imageio”(这将通过imageio使用FreeImage库),或者是“ImageMagick”,或者是“ffmpeg”
  • opt:应用优化的选项,如果program参数是’imageio’,opt必须是’wu’(Wu)或“nq”(Neuquant),。如果program=‘ImageMagick’,opt可以是“optimizeplus”或“OptimizeTransparency”
  • fuzz:仅当program='ImageMagick’时需要,通过考虑小于fuzz%的颜色差异实际上是相同的来压缩GIF文件大小
  • loop:表示GIF文件播放时循环播放多少次,如果为0就一直不停地播放,否则播放设定次数后就停止,该参数由GIF文件头控制
  • dispose:表示播放动画时渲染当前帧时,如何处理前一帧,该参数由GIF文件头控制,moviepy没有说明该参数怎么使用,缺省值为False,老猿查阅了相关资料,才基本确认该参数的作用,但GIF中该控制参数有四个取值,不知道是否都支持,取值及含义如下:
  • 为0表示绘制一个完整大小的、不透明的GIF帧来替换上一帧,就算连续的两帧只在局部上有细微的差异,每一帧依然是完整独立的绘制
  • 为1表示未被当前帧覆盖的前一帧像素将继续显示,这种方式常用于对GIF动画进行优化,当前帧只需在上一帧的基础上做局部刷新,上一帧中没有被当前帧覆盖的像素区域将继续展示。这种方式既能节省内存,也能提高解码速度
  • 为2 表示绘制当前帧之前,会先把前一帧的绘制区域恢复成背景色,这种方式常用于优化很多帧背景相同的情况,上一帧的背景色能通过当前帧的透明区域显示
  • 为3表示绘制当前帧时,会先恢复到最近一个设置为False或1的帧,然后再将当前帧叠加到上面,这种方式性能比较差,已经被慢慢废弃
  • colors:关于这个参数moviepy没有说明,老猿将该值设置为一个比较大的值,结果报错“ValueError: GIF quantize param must be 2…256”,最后查阅资料确认该参数表示色彩量化使用的调色板索引,取值为2到256。GIF最高支持8位256色,那么如果原图是真彩色的,则在生成最终效果图时,就涉及到真彩色到256的降色。真彩色是24位的,有2的24种颜色,每个像素用3个字节标识一个颜色,R、G、B各占一个字节,而256色每个像素只用一个字节从调色板中索引一种颜色,调色板最多有256种颜色。将2^24种颜色降为256种颜色,降色的过程被成为色彩量化。色彩量化过程分两步:1、根据图片定制调色板;2、遍历像素,对于每一个像素,从调色板中找最接近的颜色,记录该颜色索引。关于调色板请参考《调色板详解
  • tempfiles:将每个帧写入一个文件,而不是将它们传递到RAM中。在内存很少的计算机上很有用,只能与ImageMagick或ffmpeg一起使用。

3.5、subfx方法

subfx方法用于对剪辑指定时间段进行变换,返回该段剪辑变换后的剪辑和原剪辑其他段拼接后的新剪辑,剪辑的时长会自动调整。语法如下:
subfx(self, fx, ta=0, tb=None, **kwargs)

参数说明:
  • fx:用于对剪辑进行变换处理的函数名,这些函数可以是自定义函数,也可以是moviepy.video.fx包下的模块内定义好的可以直接使用的函数,以及其他可能的函数
  • ta:剪辑段开始位置
  • tb:剪辑段结束位置,如果tb为None,则tb被设置为原剪辑的duration,如果tb为负数,则tb被设置为剪辑的duration + tb
  • kwargs,调用fx函数时需要传入的关键字参数

subfx实际上是调用基类Clip的fx方法来实现的,关于Clip的fx方法请参考《moviepy音视频剪辑:moviepy中的剪辑基类Clip的属性和方法详解》。

3.6、fl_image方法

fl_image方法是对get_frame方法获取的帧进行变换的方法,本质上是《moviepy音视频剪辑:moviepy中的剪辑基类Clip详解》介绍的fl方法在内容变换方面的一种变种。

调用语法:fl_image(self, image_func, apply_to=None)
参数说明:
  • image_func:参数image_func是对剪辑帧进行图像变换的函数,带一个参数,参数就是要处理的帧,这个帧直接通过get_frame去获取,image_func函数的返回值为经过变换后的帧
  • apply_to:apply_to表示变换是否需要同时作用于剪辑的音频和遮罩,其值可以为’mask’、‘audio’、[‘mask’,‘audio’]

对比fl方法的调用方法fl(self, fun, apply_to=None, keep_duration=True):

  • fl_image由于只变换内容,因此不涉及时间的变换,keep_duration就是默认为True
  • image_func不带时间参数,这是因为系统默认调用get_frame(t)来获取帧,无需image_func带时间参数
  • fl_image本质上是执行如下语句来完成帧内容的变换:fl(lambda gf, t: image_func(gf(t)), apply_to)
注意:

image_func参数对应的帧数组是只读的,不能修改,实际上get_frame(t)返回的所有帧数组都是只读的。帧的类型为numpy.ndarray,而numpy.ndarray直接定义的数据是可修改的,为什么帧数据不能修改笔者暂时还没弄明白(报错ValueError: assignment destination is read-only),为了规避该问题,将参数img数据采用如下形式的赋值语句:frame= np.array(img)就可以对新的变量frame进行修改,所有变换可以针对新变量frame进行,返回也必须是新变量frame。

3.7、fill_array方法

fill_array方法是用来进行多个视频合成时处理帧的,更多是一个moviepy内部使用的方法,但当应用需要对帧进行变换时也可以调用。fill_array将pre_array的宽和高设置为参数shape对应的数据。

调用语法:fill_array(self, pre_array, shape=(0, 0))
参数说明:
  • pre_array:要处理的数据,结构必须为类似帧的三维数组,其对应的宽和高在pre_array.shape[0::1]内
  • shape:需要将 pre_array变换到的数组维度,shape实际上对应帧的新的宽和高
说明:

fill_array处理数据时,如果发现shape对应的宽或高大于pre_array本身的宽或高,则扩展pre_array的宽或高,扩展的位置使用[1,1,1]填充。如果shape对应的宽或高小于pre_array本身的宽或高,则对pre_array的宽或高超出的数据进行丢弃处理。

3.8、add_mask方法

add_mask方法就是给剪辑增加遮罩,遮罩的duration是调用者的duration,遮罩是由完全不透明(全1组成的YUV值)的像素构成。调用语法非常简单:add_mask(self)

add_mask方法就是将给定剪辑完全遮挡,并返回被遮挡后的剪辑。

3.9、on_color方法

on_color方法用于将当前剪辑放置到一个指定颜色背景的可能更大的剪辑上,用于在原始剪辑扩展大小时将空白处设置为指定颜色。返回值为处理后的新剪辑。

调用语法如下:
on_color(self, size=None, color=(0, 0, 0), pos=None,   col_opacity=None)
参数说明:
  • size:是(宽度,高度)的二元组,如果没有设置缺省为调用剪辑的大小
  • color:设定的背景色RGB颜色组
  • pos:原剪辑对应的剪辑在新剪辑的框架上的位置,请见下面函数set_position的说明
  • col_opacity:以背景色构造的底部剪辑的不透明度,取值为0到1的浮点数,如果为0表示完全透明,为1表示完全不透明,其他表示不透明度的比率。这个参数对新剪辑中原剪辑对应内容没有作用,仅用于底部背景色构造的剪辑,下图蓝色边框是透明度为0.6时背景色案例

在这里插入图片描述

3.10、set_make_frame方法

set_make_frame方法用于设置剪辑帧构建的make_frame方法,make_frame方法在get_frame中被调用来返回指定位置的帧内容。set_make_frame方法非常简单,调用语法如下:set_make_frame(self, mf),参数mf为剪辑帧的构建方法名,除self外,带一个t参数。

注意:
  • set_make_frame方法在父类Clip中也有,在此重写除了实现父类相同的功能外,同时在设置make_frame方法,马上会调用get_frame更新剪辑的size属性
  • 修改make_frame改动的不是原剪辑,而是返回一个设置了make_frame方法的原剪辑的拷贝对应的新剪辑,这是moviepy几乎所有设置方法的特点,一定要用返回的新剪辑覆盖到操作的剪辑变量!不过开发团队也准备在后续版本中进行改变,但至少到现在最新的1.03版本这点是没有变化的。

3.11、set_audio方法

set_audio方法将原剪辑的拷贝剪辑的音频设置为参数指定音频后返回新剪辑。调用语法:set_audio(self, audioclip)

3.12、set_mask方法

set_mask方法将原剪辑的拷贝剪辑的遮罩设置为参数指定剪辑后返回新剪辑。调用语法:set_mask(self, mask),参数mask是调用对象用于遮罩的剪辑。

3.13、set_opacity方法

set_opacity方法将原剪辑拷贝剪辑遮罩的每个元素的值与参数值相乘后返回,实际上就是调整遮罩剪辑帧的YUV值。调用语法:set_opacity(self, op),其中参数op表示透明度或不透明度,为任何浮点数,一般设置为【0,1】区间的一个值。

3.14、set_position方法

set_position方法用于多个剪辑合成一个剪辑时设置调用剪辑实例的拷贝在合成剪辑的位置。

调用语法:

set_position(self, pos, relative=False)

参数说明:
  • pos:剪辑需要放置的位置,可以是如下方式取值:

  • (x,y):x,y用于指定剪辑左上角在合成剪辑的坐标位置

  • (“center”,“top”):设定水平居中,垂直位置到顶部,类似的设置还有’bottom’、‘right’、‘left’

  • (factorX,factorY):基于剪辑的大小设置相对位置, factorX和factorY为(0,1)之间的浮点数,计算位置时是以factorX乘以剪辑的宽,factorY乘以剪辑的高来计算位置,这里剪辑的宽和高是老猿认为应该是最终生成剪辑的宽和高

  • x和y的=的值可以是前三种的组合,x和y可以用不同的方式来设置

  • f(t)->(x,y):为一个通过时间计算该时刻指定剪辑左上角在合成剪辑的坐标位置

  • relative:是否相对位置,如果pos使用factorX或factorY时,relative需要设置为True

3.15、to_ImageClip方法

to_ImageClip方法将剪辑对应时刻t的帧转换成ImageClip图像剪辑,图像剪辑是所有帧都是固定图像数据的剪辑,所有帧都对应为图像数据。

调用语法:

to_ImageClip(self, t=0, with_mask=True, duration=None)

说明:

参数非常简单,不单独解释,但注意图像剪辑在输出到文件时需要设置duration和fps值(为1即可),同时可能在输出文时要指定codec类型,否则可能播放失败。

3.16、to_mask方法

to_mask方法返回一个由调用者剪辑实例构建的遮罩剪辑。

调用语法:

to_mask(self, canal=0)

说明:

to_mask方法用于将当前剪辑生成一个遮罩剪辑,处理时如果调用对象本身有遮罩,则直接返回调用剪辑的遮罩,否则根据调用剪辑的数据生成遮罩数据,生成时是将调用剪辑的每一帧数据的具体像素的YUV值中的某个除以255来实现遮罩的效果,具体对YUV哪个数据进行处理由参数canal指定,0代码Y值、1代表U值、2代表V值。

3.17、to_RGB方法

返回一个由遮罩剪辑生成的非遮罩剪辑。

调用语法:

to_RGB(self)

说明:

该方法的处理过程是,如果调用剪辑不是遮罩,则直接返回自身,否则将剪辑的帧像素YUV各乘以255,再将每个像素的YUV三元组变成一个九元组,其元素是YUV值重复3遍。

这样变换的原因老猿并不十分清楚,查阅了一些资料,估计是因为YUV到RGB并不是简单YUV到RGB的一个转换,而是到24位真彩色的转换,因为一般来说直接采集到的视频数据是RGB24的格式,其位数是YUV的三倍,当RGB24变为YUV时其实是进行了降色处理,现在要恢复到RGB24因此需要进行乘以3。

但这里有些原理没说清楚,比如RGB24是用三个字节来表示颜色,YUV在这里也用了三个字节,RGB为什么变成了9个字节?这个问题一时无解,先留着。

另外这个方法在moviepy中只是对遮罩进行处理,当带遮罩的剪辑输出到文件或将剪辑的帧保存到图像或gif文件时会调用该方法对剪辑的遮罩进行处理。

3.18、without_audio方法

without_audio方法就是将剪辑拷贝后的声音去除,除了self不带其他参数,返回一个去除了声音的新剪辑。

3.19、afx方法

afx方法对原剪辑浅拷贝后的拷贝剪辑的声音进行变换,返回新剪辑。

调用语法:

afx(self, fun, *a, **k)

说明:
  • 声音变换由函数fun进行
  • a和k是fun变换函数需要带的可变参数和关键字参数
  • afx实际上是调用父类Clip的fx方法去执行fun函数的

四、小结

本文详细介绍了视频剪辑基类VideoClip的构造方法、属性和相关处理方法,相关内容参考了moviepy.video.VideoClip.py的文档字符串以及源代码,并针对部分疑难点进行了资料查询和测试,相关内容的探索和写作断断续续持续了一个多星期(_,为了弥补损失,同时为了将相关探索内容更快传播,将一些重要知识点单独成文发布了),作为一个类对象的介绍来说已经比较全面了。

VideoClip很多方法如save_frame、write_videofile、write_images_sequence、write_gif可以用于输出视频中的对应数据,而subfx、fl_image、add_mask、on_color、set_opacity等是进行视频变换的重要方法。

在文中对部分方法举例进行了运用,但在哪些场景怎么去使用这些方法没有系统介绍,将在后面关于运用场景的文章中进行部分方法使用的介绍。

更多moviepy的介绍请参考《PyQt+moviepy音视频剪辑实战文章目录》或《专栏:使用PyQt开发图形界面Python应用》。

广告

老猿关于PyQt的付费专栏《使用PyQt开发图形界面Python应用》只需要9.9元,本专栏《PyQt+moviepy音视频剪辑实战》文档的同样内容在付费专栏上也有相应内容,总体来说付费专栏介绍更详细或案例更多。

本节内容对应付费专栏的《moviepy音视频剪辑:视频剪辑基类VideoClip详解》。如果有兴趣也愿意支持老猿的读者,欢迎购买付费专栏。

跟老猿学Python、学5G!

☞ ░ 前往老猿Python博文目录

Python数据分析与挖掘

01-08
92讲视频课+16大项目实战+源码+¥800元课程礼包+讲师社群1V1答疑+社群闭门分享会=99元   为什么学习数据分析?       人工智能、大数据时代有什么技能是可以运用在各种行业的?数据分析就是。       从海量数据中获得别人看不见的信息,创业者可以通过数据分析来优化产品,营销人员可以通过数据分析改进营销策略,产品经理可以通过数据分析洞察用户习惯,金融从业者可以通过数据分析规避投资风险,程序员可以通过数据分析进一步挖掘出数据价值,它和编程一样,本质上也是一个工具,通过数据来对现实事物进行分析和识别的能力。不管你从事什么行业,掌握了数据分析能力,往往在其岗位上更有竞争力。    本课程共包含五大模块: 一、先导篇: 通过分析数据分析师的一天,让学员了解全面了解成为一个数据分析师的所有必修功法,对数据分析师不在迷惑。   二、基础篇: 围绕Python基础语法介绍、数据预处理、数据可视化以及数据分析与挖掘......这些核心技能模块展开,帮助你快速而全面的掌握和了解成为一个数据分析师的所有必修功法。   三、数据采集篇: 通过网络爬虫实战解决数据分析的必经之路:数据从何来的问题,讲解常见的爬虫套路并利用三大实战帮助学员扎实数据采集能力,避免没有数据可分析的尴尬。   四、分析工具篇: 讲解数据分析避不开的科学计算库Numpy、数据分析工具Pandas及常见可视化工具Matplotlib。   五、算法篇: 算法是数据分析的精华,课程精选10大算法,包括分类、聚类、预测3大类型,每个算法都从原理和案例两个角度学习,让你不仅能用起来,了解原理,还能知道为什么这么做。
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值