Spark之RDD的Action算子和最受学生喜爱/欢迎的科目案例(5)

一   RDD 的 Action 算子

Action : 行动算子 ,调用行动算子会触发job执行 ,本质上是调用了 sc.runJob 方法 ,该方法从最后一个RDD,根据其依赖关系 ,从后往前 ,划分 Stage ,生成 TaskSet .

二   对RDD的操作(创建,查看)

1  创建RDD的方法

1.1  通过并行化方法 ,将 Driver 端的集合转成 RDD

将Driver端的scala集合并行化成RDD,RDD中并没有真正要计算的数据,只是记录以后从集合中的哪些位置获取数据

val  rdd1:RDD[Int] = sc.parallelize(Array(1,2,3,4,5,6,7),2)     --- 这里的分区数是 2

1.2  从 HDFS 指定的目录创建RDD

指定以后从哪里读取创建RDD,可以是多种文件系统,需要指定文件系统的协议,如hdfs://,flile://,s3://等

val  lines:RDD[String] = sc.textFile("hdfs://linux04:8020/log" ,2)       --- 2 可以省略不写 ,默认就是 2

log :  是linux04节点上 hdfs 中跟目录下的一个文件夹

8020 : 是linux04 节点hdfs 的端口号

注意 : 使用上面两种方式创建 RDD 都可以指定分区的数量 .

1.3  使用 makeRDD 方法创建RDD

将Driver端的scala集合并行化成RDD,RDD中并没有真正要计算的数据,只是记录以后从集合中的哪些位置获取数据

    val conf = new SparkConf().setAppName(this.getClass.getSimpleName).setMaster("local[*]")
    val sc = new SparkContext(conf)
 
    val arr = List(1,2,3,4)
    val listRDD: RDD[Int] = sc.makeRDD(arr, 2)   --2个分区
    --index 表示分区的索引 ;it 表示对应分区的迭代器,一个分区的迭代器抓取该分区的一条条数据
    val result: Array[String] = listRDD.mapPartitionsWithIndex((index, it) => {
      it.map(li => s"分区号为 :$index ,对应的迭代器为 :$li")
    }).collect()
    result.foreach(println)
    
     结果为 : 分区号为 :0 ,对应的迭代器为 :1   分区号为 :0 ,对应的迭代器为 :2
             分区号为 :1 ,对应的迭代器为 :3    分区号为 :1 ,对应的迭代器为 :4

2   查看RDD 的数量

val  rdd1:RDD[Int] = sc.parallelize(Array(1,2,3,4,5,6,7),2)

rdd1.partitions.length

三   RDD 常用的 Action 算子 

1  collect

1) 将数据以数组形式收集回 Driver 端 ,数据按照分区编号有序返回 ;

2) 即将数据从不同的 Eexcutor 中的不同 Task 中收集起来到 Driver 端 ,通过网络将数据按分区编号依次进行收集 ,如果数据很大 ,就只能收集一部分,另外一部分的数据如果没有另外存储到磁盘或者内存中的话可能会丢失 .

    val conf = new SparkConf().setAppName(this.getClass.getSimpleName).setMaster("local[*]")
    val sc = new SparkContext(conf)
    val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5), 2)
    val array: Array[Int] = rdd1.collect()
    println(array.toBuffer)   ---ArrayBuffer(1, 2, 3, 4, 5)

2  count 

1) 计算RDD中元素的数量 ,调用了 sc.runJob ,每个分区的条数再相加 ,底层调用了迭代器 ,来一条加一条

2) 先在每一个分区count,然后返回数组,再在driver端sum

    val conf = new SparkConf().setAppName(this.getClass.getSimpleName).setMaster("local[*]")
    val sc = new SparkContext(conf)
    val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5), 2)
    val count1: Long = rdd1.count()
    println(count1)    --- 5

3  sum 

1)  sum : 底层调用 fold(有初始值 ,是0.0 ,也可以是其他数值) ,先在每个区内局部聚合 ,然后再全局聚合 ,最后返回的是Double

sum : 先局部聚合 ,之后再全局聚合 -----该方法仅限于"数值" 类型的聚合 ,"字符"的不可以使用这个方法
val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5), 2)
val sum1: Double = rdd1.sum()
println(sum1)     -- 15.0 = (1+2+3)+(4+5)

4  fold 

1) 有初始值 ,初始值在局部聚合的时候使用一次 ,在全局聚合的时候也使用一次 ;

2) 计算的数据先局部聚合 ,再全局聚合

val conf = new SparkConf().setAppName(this.getClass.getSimpleName).setMaster("local[*]")
val sc = new SparkContext(conf)
val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5), 2)

val folded: Int = rdd1.fold(0)((x, y) => x + y)
println(folded)  -- 15
val folded1: Int = rdd1.fold(0)(_ + _)
println(folded1)  -- 15

val folded2: Int = rdd1.fold(10)(_ + _)
println(folded2)   -- 45 = [(10+(1+2+3))+(10+(4+5))]+10
val folded3: Int = rdd1.fold(15)(_ + _)
println(folded3)   -- [(15+(1+2+3))+(15+(4+5))]+15 = 60

val rdd2: RDD[String] = sc.parallelize(List("A","B","C","D"),2)
val foldStr: String = rdd2.fold("10")(_ + _)
println(foldStr)   -- 1010CD10AB

5  reduce 

先局部聚合 ,再全局聚合 ,都是在Executor 中计算好的 ,然后返回给Driver

    val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5), 2)
    val reduced1: Int = rdd1.reduce(_ + _)
    println(reduced1)    -- 15

6  aggregate

1) 每一个分区会应用一次初始值 ,全局聚合时还会应用一次初始值

2) 可以指定局部聚合的函数和全局聚合的函数 ,可以一样也可以不一样

3) 多个分区的Task是并行执行的 ,哪一个先计算出结果 ,不一定

    val conf = new SparkConf().setAppName(this.getClass.getSimpleName).setMaster("local[*]")
    val sc = new SparkContext(conf)
    val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5), 2)
    --指定一个初始值 ,先局部聚合,使用一次初始值 ;再全局聚合,再使用一次初始值
    val aggregated: Int = rdd1.aggregate(10)(_ + _, _ + _)
    println(aggregated)  -- 45 = [(10+(1+2))+(10+(4+5+3))]+10

    --指定一个初始值 ,先在局部区得到一个最大值 ,然后最大值再进行全局聚合
    val aggregated2: Int = rdd1.aggregate(0)(Math.max(_, _), _ + _)
    println(aggregated2)    -- 8 = 2+5  

7  take  和 first

1) take : take(n) 从0号分区开始取数据 ,如果数据的条数不够 ,再触发一次 action 到下一个分区取 ,会触发一次到多次 Action , 这样取值不会占用资源 .

2) first : 相当于 take(1) ,但是返回的不是数组

    -------take----------
    val rdd1: RDD[Int] = sc.parallelize(List(3, 5, 2, 6, 1),2)
    -- 先从0号分区开始取值,如果值的数量不够 ,就再action ,然后从下一个区开始取值,直到取够为止
    val taked: Array[Int] = rdd1.take(3)
    -- 3 ,5 是0号分区的 ,2是1号分区的
    taked.foreach(println)   -- 3 5 2

    -------first---------
    -- 取第一个分区的第一个元素
    val firsted: Int = rdd1.first()
    println(firsted)     -- 3

8  takeOrdered 和 top

1) takeOrdered : 先调用mapPartitions(对每个分区进行窄依赖操作 ,不会产生shuffle),将最小的N个元素放入到有界优先队列,然后在调用 reduce( ++= ),将每个分区对应的有界优先队列进行合并 . (默认升序) ,调用Ordering的reverse方法就是top方法

2) top : 底层调用的是takeOrderd,只不过是顺序翻转 ; 将每个分区最大的 N 个元素放入到有界优先队列 ,然后调用reduce(++=) 将下一个分区的有界优先队列合并 ,最后留下最大的N个元素 . (默认降序)

有界优先队列 : 相当于可以排序的不会去重的集合

object TakeandTakeOrderTop {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("Take01").setMaster("local[*]")
    val sc = new SparkContext(conf)
    --创建一个原始的RDD
    val rdd1: RDD[Int] = sc.parallelize(List(3, 5, 2, 6, 1))
    --对集合里面的元素进行降序排序
    val sorted: RDD[Int] = rdd1.sortBy(x => x, false)
    --得到排序后的数据
    sorted.collect().foreach(println)     -- 6 5 3 2 1
   
    --先调用mapPartition ,将最小的n个元素放入到有界优先队列(可以将其看做是可以排序但不会去重的集合),
    --然后调用reduce(++=),将每个分区对应的有界优先队列进行合并,然后保留最小的n个元素
    println(rdd1.takeOrdered(3).toBuffer)    -- ArrayBuffer(1, 2, 3)

    -------调用Ordering 方法的reverse方法 ,将takeOrdered结果进行反转,就是 top 方法-------
    -----------既可以得到最大的n 个元素-------------------
    val result: Array[Int] = rdd1.takeOrdered(3)(Ordering.Int.reverse)
    println(result.toBuffer)        -- ArrayBuffer(6, 5, 3)

    --先调用了mapPartition,将最大的n个元素放到有界优先队列 ,然后调用reduce(++=)方法,将每个分区
    --对应的有界优先队列进行合并
    println(rdd1.top(3).toBuffer)    ---ArrayBuffer(6, 5, 3)

    ---------将top 结果进行反转后得到的结果与takeOrdered 结果一致---------------
    val topReverseed: Array[Int] = rdd1.top(3)(Ordering.Int.reverse)
    println(topReverseed.toBuffer)    --- ArrayBuffer(1, 2, 3)
  }
}

9  min 和 max 

1) min  : 根据 ASCLL 码表取最小值 .

2) max : 根据 ASCLL 码表取最大值 .

    val conf = new SparkConf().setAppName("MinMax").setMaster("local[*]")
    val sc = new SparkContext(conf)
    val rdd1: RDD[Int] = sc.parallelize(List(1,5,4,3,2))

    ---min ,取最小值
    val i = rdd1.min()
    println(i)   --- 1
    ---max ,取最大值
    val i1 = rdd1.max()
    println(i1)   --- 5     

    val rdd2: RDD[String] = sc.parallelize(Array("HIVE", "SPARK", "AKKA", "APCHE", "HADOOP"))
    val str = rdd2.min()
    println(str)   --- AKKA
    val str1 = rdd2.max()
    println(str1)   -- SPARK

10   saveAsTextFile 和 foreach 和 foreachPartition

1)  saveAsTextFile : 先调用 mapPartitions ,将数据转成 Hadoop 的TextOutputFormat 要求的 K,V 格式 ,然后再调用 sc.runJob

2)  foreach : 将数据一条一条的取出来 ,传入一个函数 ,这个函数返回 Unit ,比如传入一个打印的逻辑 ,打印的结果在 Executor 端的日志中

3) foreachPartition : 以分区为单位 ,每一个分区就是 Task ,以后可以将数据写入到数据库中 ,一个分区一个链接,效率更高

四  action 算子案例实操

计算出每个科目最受欢迎的/点击率最高的前两位老师的视频

1  数据准备 

http://javaee.bilibili.com/laoyang
http://javaee.bilibili.com/laoyang
http://php.bilibili.com/laoli
http://php.bilibili.com/laoliu
http://php.bilibili.com/laoli
http://php.bilibili.com/laoli
http://bigdata.bilibili.com/laozhang
http://bigdata.bilibili.com/laozhang
http://bigdata.bilibili.com/laozhao
http://bigdata.bilibili.com/laozhao
.........

2  代码实现

2.1  方案一

1)  创建 SparkContext ,获得 sc ,创建原始的 RDD ,获得数据 ,对数据进行切割处理 ,得到想要的字段 ,然后进行组装 ((科目,老师) , 1) ,科目和老师关联在一起 ,作为key为一组 ,1 作为value ,然后调用reduceByKey方法 ,根据key进行分组 ,value进行聚合,得到一个新的RDD(科目和老师 ,点击总次数);

2) 然后根据新RDD的(科目和老师)进行分组和排序即得到结果(groupBy和sortBy和take(2)--这些方法会产生大量的shuffle ,并不是最好的方法)

object FavTeacher01 {
  def main(args: Array[String]): Unit = {
    --让运行变得更灵活 ,根据main方法中插入的参数true 或者false 来决定是否在本地运行
    val isLocal: Boolean = args(0).toBoolean
    val conf = new SparkConf().setAppName(this.getClass.getSimpleName)
    --如果main 方法index为0 的位置传入的参数是true ,就在本地运行
    if(isLocal){
      conf.setMaster("local[*]")
    }
    val sc = new SparkContext(conf)
    --创建原始的RDD
    val lines: RDD[String] = sc.textFile(args(1))
    --对一行行的数据进行整理 ,数据为 :  http://bigdata.bilibili.com/laozhang
    --切割,取出需要的元素然后组装 ,再然后根据key ,对 value进行聚合
    val reduced: RDD[((String, String), Int)] = lines.map(m => {
    --切割数据时因为有的是 // ,有的是 /  ,所以处理的时候用 /+ ,表示可以对单 / ,也可以对双 / 进行切割
      val strings = m.split("/+")
      val url = strings(1)
      val teacher = strings(2)
      val subject = url.split("[.]")(0)
      ((subject, teacher), 1)
    }).reduceByKey(_ + _)

    --对聚合的元素进行排序 ,因为需求是计算出每一个科目点击率最高 ,最受学生欢迎的前两个老师
    --如果直接调用 sortBy 方法是对全局进行排序 ,而非单个科目排序 ,所以不能使用sortBy
    --所以首先先按照学科进行分组
    val grouped: RDD[(String, Iterable[((String, String), Int)])] = reduced.groupBy(_._1._1)
    --然后根据value进行数据的处理  ,这种方法会产生大量的shuffle
    val subAndTeacherCoun: RDD[(String, List[((String, String), Int)])] = grouped.mapValues(it => it.toList.sortBy(-_._2).take(2))
    --将结果打印出来
    println(subAndTeacherCoun.collect().toBuffer)
    
     --结果为 :ArrayBuffer((javaee,List(((javaee,laoyang),210000), ((javaee,xiaoxu),140000))),
     -- (php,List(((php,laoli),60000), ((php,laoliu),20000))),
     -- (bigdata,List(((bigdata,laozhao),370000), ((bigdata,laoduan),140000))))
     
    sc.stop()
  }
}

2.2  方案二

1)  创建 SparkContext ,获得 sc ,创建原始的 RDD ,获得数据 ,对数据进行切割处理 ,得到想要的字段 ,然后进行组装 ((科目,老师) , 1) ,科目和老师关联在一起 ,作为key为一组 ,1 作为value ,然后调用reduceByKey方法 ,根据key进行分组 ,value进行聚合,得到一个新的RDD(科目和老师 ,点击总次数);

2) 定义一个数组 ,里面的元素是所有的科目 ,for循环遍历数组 ,从新的RDD(科目和老师 ,点击总次数),然后根据(科目和老师)这个key ,将科目都过滤出来 ,每一个科目都是分开的(分组的功能) ; (这里没有使用groupBy进行分组)

3) 自定义排序规则或者是导入隐式转换的排序规则将以上结果进行排序 ,然后调用takeOrdered(2)方法就可以得到最终结果.(也没有调用sortBy方法进行排序)

object FavTeacher02 {
  def main(args: Array[String]): Unit = {
    val isLocal = args(0).toBoolean
    val subjects = Array("bigdata","javaee","php")
    val conf = new SparkConf().setAppName(this.getClass.getSimpleName)
    if(isLocal){
      conf.setMaster("local[*]")   -- * 代表多个进程
    }

    --创建 sparkcontext
    val sc = new SparkContext(conf)
    val lines: RDD[String] = sc.textFile(args(1))
    --对读取的一行行数据进行切割处理 ,根据需求拿到想要的元素并组装 ,然后进行全局聚合
    val reduced: RDD[((String, String), Int)] = lines.map(m => {
      val strings = m.split("/+")
      val url = strings(1)
      val subject = url.split("[.]")(0)
      val teachers = strings(2)
      ((subject, teachers), 1)
    }).reduceByKey(_ + _)

    --需求是计算出每科最受学生欢迎的老师 ,所以需要对每一个科目进行排序处理
    --先将一个学科的数据进行过滤
    for(sb <- subjects){
      --第一轮先将其中一个科目过滤出来,然后对这个科目进行排序取值
      val subjRDD: RDD[((String, String), Int)] = reduced.filter(_._1._1.equals(sb))

      -----------取出一个组里面最大的两个元素--takeOrdered --降序取值--------------
      ---第一种方法:直接在后面new 一个Ordering,重写排序规则(匿名内部类 ,参考java 的形式)----
      val res: Array[((String, String), Int)] = subjRDD.takeOrdered(2)(new Ordering[((String, String), Int)] {
        override def compare(x: ((String, String), Int), y: ((String, String), Int)): Int = {
          -(x._2 - y._2)
        }
      })  

      ---第二种方法 : 自定义一个隐式参数 ,重写里面的排序规则,降序排序------------
      implicit val ord:Ordering[((String, String), Int)] = Ordering[Int].on[((String, String), Int)](t => -t._2)
     --然后去降序排序后的前2个元素  隐式参数可以导入,也可以不导入,如果不导入,代码会自己去上下文中查找
      val res = subjRDD.takeOrdered(2)(ord)
      println(res.toBuffer)
        --结果为:ArrayBuffer(((bigdata,laozhang),60000), ((bigdata,laoduan),140000))
        --ArrayBuffer(((javaee,xiaoxu),140000), ((javaee,laoyang),210000))
        --ArrayBuffer(((php,laoliu),20000), ((php,laoli),60000))
    }
    sc.stop()
  }
}

2.3  方案三

1)  创建 SparkContext ,获得 sc ,创建原始的 RDD ,获得数据 ,对数据进行切割处理 ,得到想要的字段 ,然后进行组装 ((科目,老师) , 1) ,科目和老师关联在一起 ,作为key为一组 ,1 作为value ,然后调用reduceByKey方法 ,根据key进行分组 ,value进行聚合,得到一个新的RDD(科目和老师 ,点击总次数);

2) 定义一个分区器自定义分区规则 ,需要知道有多少科目 ,然后将科目放到自定义的分区器中得到每个科目对应的唯一的分区号(一个科目一个分区号) ,然后新的RDD(科目和老师 ,点击总次数)就根据得到的分区号将对应的科目放到对应的分区里面;

3) 从上得到很多个分区, 一个区里面装的都是一个类型的科目的数据 ,有多少个科目就有多少个分区 ,一个分区一个分区的取值 ,导入隐式转换排序规则(降序排序) ,定义一个TreeSet集合 ,将排序的结果放到TreeSet集合里面 ;

4) 取出一个区的数据 ,一个区一个迭代器,然后一条一条的取出来放到 TreeSet集合里面 ,根据隐式转换的降序规则 ,对 TreeSet集合里面的数据进行降序排序 ,只要集合里面的元素的个数大于2 ,就将集合里面三个元素中最小的那个移除掉,直到一个区里面的元素都取出比对完,最后留在 treeSet 集合中的两个元素就是同一个科目点击率最高的两位老师 .

object FavTeacher03 {
  def main(args: Array[String]): Unit = {
    val isLocal = args(0).toBoolean     ----main方法参数一 : true
    --创建SPARKCONF
    val conf = new SparkConf().setAppName(this.getClass.getSimpleName)
    --判断传入的参数是true还是false,是否需要在本地运行
    if(isLocal){
      conf.setMaster("local[*]")
    }
    --创建 sparkcontext
    val sc = new SparkContext(conf)
    --创建原始RDD
    val lines: RDD[String] = sc.textFile(args(1))   ----main方法参数二 : 待处理文件输入路径
    --对读取的行数据进行处理,先按一定规则进行切割,得到想要的数据,然后将其进行组装,然后按照key进行分组聚合
    val reduced: RDD[((String, String), Int)] = lines.map(line => {
      val strings = line.split("/+")
      val url = strings(1)
      val subject = url.split("\\.")(0)
      val teacher = strings(2)
      ((subject, teacher), 1)
    }).reduceByKey(_ + _)

    val topN = args(2).toInt       ----main方法参数三 : 2

    --先计算数据有多少个学科,触发一次Action ,将数据计算好收集到Driver端
    --((String, String), Int) 算出来有多少个科目
    val subjects = reduced.map(_._1._1).distinct().collect()
    --针对科目创建一个分区器, 确保每个科目划分到自己的分区去
    val subjectPartitioner = new SubjectPatitionner(subjects)

    --按照指定的分区器进行分区 ((String, String), Int)
    --得出的结果数据,就是按照科目划分好了的数据.也就是php,java等的数据都在各自的一个区里面
    val partitionedRDD: RDD[((String, String), Int)] = reduced.partitionBy(subjectPartitioner)

    --排序,每个分区内的数据就是对应科目的数据,没有其他科目的数据 ((subject, teacher), 1)
    val sorted: RDD[((String, String), Int)] = partitionedRDD.mapPartitions(it => {
      --定义一个可排序的集合
      implicit val ord = Ordering[Int].on[((String, String), Int)](t => -t._2)
      val sorter: mutable.Set[((String, String), Int)] = new mutable.TreeSet[((String, String), Int)](ord)

      --将迭代器中的每一条数据一次遍历处理
      it.foreach(t => {
        sorter += t
        if (sorter.size > topN) {
          --移除小的
          sorter -= sorter.last
        }
      })
      sorter.iterator
    })
    println(sorted.collect().toBuffer)
    /**
     * 结果为 : ArrayBuffer(((javaee,laoyang),210000), ((javaee,xiaoxu),140000),
     * ((php,laoli),60000), ((php,laoliu),20000),
     * ((bigdata,laozhao),370000), ((bigdata,laoduan),140000))
     */
    sc.stop()
  }
}
--自定义分区规则
class SubjectPatitionner(val subjects: Array[String]) extends Partitioner{
  --定义的规则,一个科目对应唯一的编号
  --定义一个map集合 ,装科目和其对应的唯一的编号
  val subjectToIndex = new mutable.HashMap[String, Int]()
  --初始编号为0
  var index = 0
  for(sb <- subjects){
    --循环一次,将科目和对应的编号装到map集合里面
    subjectToIndex(sb) = index
    --然后编号加1,然后再循环
    index += 1
  }
  --根据科目的数量,决定分区的数量
  override def numPartitions: Int = subjects.size
  --根据科目,到map集合里面得到对应的唯一的分区编号
  override def getPartition(key: Any): Int = {
    --参数key 是 reduced: RDD[((String, String), Int)]中的key ,key的1号元素就是科目
    val sb = key.asInstanceOf[(String, String)]._1
    --根据科目,取出其对应的编号
    subjectToIndex(sb)
  }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spark RDD(弹性分布式数据集)是Spark中最基本的数据抽象,它代表了一个不可变、可分区、可并行计算的数据集合。转换算子是用于对RDD进行转换操作的方法,可以通过转换算子RDD进行各种操作和变换,生成新的RDD。 以下是一些常见的Spark RDD转换算子: 1. map(func):对RDD中的每个元素应用给定的函数,返回一个新的RDD,新RDD中的每个元素都是原RDD中元素经过函数处理后的结果。 2. filter(func):对RDD中的每个元素应用给定的函数,返回一个新的RDD,新RDD中只包含满足条件的元素。 3. flatMap(func):对RDD中的每个元素应用给定的函数,返回一个新的RDD,新RDD中的每个元素都是原RDD中元素经过函数处理后生成的多个结果。 4. union(other):返回一个包含原RDD和另一个RDD中所有元素的新RDD。 5. distinct():返回一个去重后的新RDD,其中不包含重复的元素。 6. groupByKey():对键值对RDD进行分组,返回一个新的键值对RDD,其中每个键关联一个由具有相同键的所有值组成的迭代器。 7. reduceByKey(func):对键值对RDD中具有相同键的值进行聚合操作,返回一个新的键值对RDD,其中每个键关联一个经过聚合函数处理后的值。 8. sortByKey():对键值对RDD中的键进行排序,返回一个新的键值对RDD,按照键的升序排列。 9. join(other):对两个键值对RDD进行连接操作,返回一个新的键值对RDD,其中包含两个RDD中具有相同键的所有元素。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值