Scala Spark的使用

SparkContext的创建

#创建一个SparkConf
val work: SparkConf = new >SparkConf().setMaster(“local[*]”).setAppName(“work”)
#创建一个SparkContext
val sc = new SparkContext(work)

Spark RDD创建

  1. 创建一个RDD(有如下几种方式)

val rdd = sc.parallelize(Arral(1,2,3,4,5,6,7,8))
val rdd = sc.makeRDD(Array(1,2,3,4,5,6,7,8))
val rdd = sc…textFile(“scr/hotel.txt”)

转换算子

  1. map算子
  2. flatMap算子
  3. mapPartitions算子
  4. mapPartitionsWithlndex算子
  5. union算子
  6. cartesian算子
  7. groupBy算子、groupByKey算子
  8. filter算子
  9. distinct算子
  10. subtract算子
  11. sample算子
  12. cache算子
  13. persist算子
  14. mapValues算子、flatMapValues算子
  15. combineByKey算子
  16. reduceByKey算子
  17. repartition算子
  18. cogroup算子
  19. join算子
  20. leftOutJoin和rightOutJoin算子、fullOuterJoin算子
  21. intersection算子
  22. foreach算子
  23. saveAsTextFile算子
  24. saveAsObjectFile算子
  25. collect算子
  26. collectAsMap算子
  27. count,countByKey,CountByValue算子
  28. take、takeSample算子
  29. reduce算子
  30. aggregate算子
  31. zip、zipWithlndex算子(转换算子)
Value数据类型的Transformation算子
  • 输入分区与输出分区一对一型
  1. map算子

处理数据是一对一的关系,进入一条数据,出去的还是一条数据。map的输入变换函数应用于RDD中所有的元素,而mapPartitions应用于所有分区。区别于mapPartitions主要在于调度粒度不同。如parallelize(1 to 10 ,3),map函数执行了10次,而mapPartitions函数执行了3次。

val infos: RDD[String] = sc.parallelize(Array[String]("hello spark","hello hdfs","hello HBase"))
val result: RDD[Array[String]] = infos.map(one => {
  one.split(" ")
})
result.foreach(arr =>{arr.foreach(println)})
  1. flatMap算子

flatMap是一对多的关系,处理一条数据得到多条结果,将原来 RDD 中的每个元素通过函数 f 转换为新的元素,并将生成的 RDD 的每个集合中的元素合并为一个集合。

val infos: RDD[String] = sc.makeRDD(Array[String]("hello spark","hello hdfs","hello MapReduce"))
val rdd1: RDD[String] = infos.flatMap(one => {
  one.split(" ")
})
rdd1.foreach(println)
  1. mapPartitions算子

mapPartitions遍历的是每一个分区中的数据,一个个分区的遍历。获取到每个分区的迭代器,在函数中通过这个分区整体的迭代 器对整 个分区的元素进行操作,相对于map一条条处理数据,性能比较高,可获取返回值。
可以通过函数f(iter) =>iter.filter(_>=3)对分区中所有的数据进行过滤,大于和等于3的数据保留,一个方块代表一个RDD分区,含有1,2,3的分区过滤,只剩下元素3。

val listRDD: RDD[Int] = sc.makeRDD(List(1,2,3,4,5,6,5,4,3,2,1), 2)
    val mappartitionsRDD: RDD[Int] = listRDD.mapPartitions(x => {
      x.filter(_ > 3)
    })
    mappartitionsRDD.foreach(println(_))
  1. mapPartitionsWithlndex算子

拿到每个RDD中的分区,以及分区中的数据

val lines: RDD[String] = sc.textFile("./data/words",5)
    val result: RDD[String] = lines.mapPartitionsWithIndex((index, iter) => {
      val arr: ArrayBuffer[String] = ArrayBuffer[String]()
      iter.foreach(one => {
//        one.split(" ")
        arr.append(s"partition = [$index] ,value = $one")
      })
      arr.iterator
    }, true)
    result.foreach(println)

打印结果

  • 输入分区与输出分区多对一型
  1. union算子

union合并两个RDD,两个RDD必须是同种类型,不一定是K,V格式的RDD

val rdd1: RDD[String] = sc.parallelize(List[String]("zhangsan","lisi","wangwu","maliu"),3)
val rdd2: RDD[String] = sc.parallelize(List[String]("a","b","c","d"),4)
val unionRDD: RDD[String] = rdd1.union(rdd2)
unionRDD.foreach(println)
  1. cartesian算子

求笛卡尔积,该操作不会执行shuffle操作,但最好别用,容易触发OOM

val conf=new SparkConf().setAppName("cratesian").setMaster("local")
    val sc=new SparkContext(conf)

    val arr1=sc.parallelize(Array(1,3,5))
    val arr2=sc.parallelize(Array(2,4,6))
    arr1.cartesian(arr2).collect.foreach(println)
  • 输出分区与输出分区多对多型
  1. groupBy算子、groupByKey算子

groupBy算子按照指定的规则,将数据分组

val rdd: RDD[(String, Double)] = sc.parallelize(List[(String,Double)](("zhangsan",66.5),("lisi",33.2),("zhangsan",66.7),("lisi",33.4),("zhangsan",66.8),("wangwu",29.8)))
val result: RDD[(Boolean, Iterable[(String, Double)])] = rdd.groupBy(one => {
  one._2 > 34
})
result.foreach(println)

打印结果

groupByKey算子根据key去将相同的key对应的value合并在一起(K,V)=>(K,[V])

val rdd: RDD[(String, Double)] = sc.parallelize(List[(String,Double)](("zhangsan",66.5),("lisi",33.2),("zhangsan",66.7),("lisi",33.4),("zhangsan",66.8),("wangwu",29.8)))
val rdd1: RDD[(String, Iterable[Double])] = rdd.groupByKey()
rdd1.foreach(info=>{
  val name: String = info._1
  val value: Iterable[Double] = info._2
  val list: List[Double] = info._2.toList
  println("name = "+name+",value ="+list)
})

打印结果

  • 输出分区是输入分区子集类型
  1. filter算子

过滤数据,返回true的数据会被留下

val infos: RDD[String] = sc.makeRDD(List[String]("hehe","hahha","zhangsan","lisi","wangwu"))
val result: RDD[String] = infos.filter(one => {
  !one.equals("zhangsan")
})
result.foreach(println)

打印结果
9. distinct算子

distinct去重,有shuffle产生,内部实际是map+reduceByKey+map实现

val infos: RDD[String] = sc.parallelize(List[String]("a","a","b","a","b","c","c","d"),4)
val result: RDD[String] = infos.distinct()
result.foreach(println)

打印结果

  1. subtract算子

取RDD的差集,subtract两个RDD的类型要一致,结果RDD的分区数与subtract算子前面的RDD分区数多的一致。

val rdd1 = sc.parallelize(List[String]("zhangsan","lisi","wangwu"),5)
val rdd2 = sc.parallelize(List[String]("zhangsan","lisi","maliu"),4)
val subtractRDD: RDD[String] = rdd1.subtract(rdd2)
subtractRDD.foreach(println)
println("subtractRDD partition length = "+subtractRDD.getNumPartitions)

打印结果
11. sample算子

sample随机抽样,参数sample(withReplacement:有无放回抽样,fraction:抽样的比例,seed:用于指定的随机数生成器的种子)
有种子和无种子的区别:
有种子是只要针对数据源一样,都是指定相同的参数,那么每次抽样到的数据都是一样的
没有种子是针对同一个数据源,每次抽样都是随机抽样

val lines: RDD[String] = sc.textFile("./data/sampleData.txt")
val result: RDD[String] = lines.sample(true,0.01,100)
result.foreach(println)
sc.stop

打印结果

  • Cache算子
  1. cache算子
  2. persist算子

cache()和persist()注意问题
1.cache()和persist()持久化单位是partition,cache()和persist()是懒执行算子,需要action算子触发执行
2.对一个RDD使用cache或者persist之后可以赋值给一个变量,下次直接使用这个变量就是使用持久化的数据。也可以直接对RDD进行cache或者persist,不赋值给一个变量
3.如果采用第二种方法赋值给变量的话,后面不能紧跟action算子
4.cache()和persist()的数据在当前application执行完成之后会自动清除

val conf: SparkConf = new SparkConf().setMaster("local").setAppName("cache")
    val sc: SparkContext = new SparkContext(conf)
    sc.setLogLevel("Error")
    val lines: RDD[String] = sc.textFile("./data/persistData.txt")

    //    val linescache: RDD[String] = lines.persist(StorageLevel.MEMORY_ONLY)
    val linescache: RDD[String] = lines.cache()

    val startTime1: Long = System.currentTimeMillis()
    val count1: Long = linescache.count()
    val endTime1: Long = System.currentTimeMillis()
    println("count1 = "+count1+". time = "+(endTime1-startTime1) + "mm")

    val starttime2: Long = System.currentTimeMillis()
    val count2: Long = linescache.count()
    val endTime2: Long = System.currentTimeMillis()
    println("count2 = "+count2+", time = "+(endTime2-starttime2) + "ms")
    sc.stop()
Key-Value数据类型的Transformation算子
  • 输入分区与输出分区一对一
  1. mapValues算子、flatMapValues算子

mapValues算子针对K,V格式的数据,只对Value做操作,Key保持不变

val infos: RDD[(String, String)] = sc.makeRDD(
    List[(String,String)](
    ("zhangsan","18"),("lisi","20"),("wangwu","30")
    ))
val result: RDD[(String, String)] = infos.mapValues(s => {
  s + " " + "zhangsan18"
})
result.foreach(println)
sc.stop()

打印结果

flatMapValues算子(K,V)->(K,V),作用在K,V格式的RDD上,对一个Key的一个Value返回多个Value

  • 对单个RDD或者两个RDD聚集
单个RDD聚集
  1. combineByKey算子

首先给RDD中每个分区中的每一个key一个初始值
其次在RDD每个分区内部相同的key聚合一次
再次在RDD不同的分区之间将相同的key结果聚合一次

val rdd1: RDD[(String, Int)] = sc.makeRDD(List[(String, Int)](
  ("zhangsan", 10), ("zhangsan", 20), ("wangwu", 30),
  ("lisi", 40), ("zhangsan", 50), ("lisi", 60),
  ("wangwu", 70), ("wangwu", 80), ("lisi", 90)
),3)
rdd1.mapPartitionsWithIndex((index,iter)=>{
  val arr: ArrayBuffer[(String, Int)] = ArrayBuffer[(String,Int)]()
  iter.foreach(tp=>{
    arr.append(tp)
    println("rdd1 partition index ="+index+".value ="+tp)
  })
  arr.iterator
}).count()
println("++++++++++++++++++++++++++++++++++++")
val result: RDD[(String, String)] = rdd1.combineByKey(v=>{v+"hello"}, (s:String, v)=>{s+"@"+v}, (s1:String, s2:String)=>{s1+"#"+s2})
result.foreach(println)

打印结果

  1. reduceByKey算子

首先会根据key去分组,然后在每一组中将value聚合,作用在KV格式的RDD上

val infos: RDD[(String, Int)] = sc.parallelize(
    List[(String,Int)](("zhangsan",1),("zhangsan",2),
    ("zhangsan",3),("lisi",100),("lisi",200)),5
    )
val result: RDD[(String, Int)] = infos.reduceByKey((v1, v2)=>{v1+v2})
result.foreach(println)
sc.stop()

打印结果
17. repartition算子

重新分区,可以将RDD的分区增多或者减少,会产生shuffle,coalesc(num,true) = repartition(num)

val rdd1: RDD[String] = sc.parallelize(List[String](
  "love1", "love2", "love3", "love4",
  "love5", "love6", "love7", "love8",
  "love9", "love10", "love11", "love12"
), 3)
val rdd2: RDD[String] = rdd1.mapPartitionsWithIndex((index, iter) => {
  val list: ListBuffer[String] = ListBuffer[String]()
  iter.foreach(one => {
    list.append(s"rdd1 partition = [$index] ,value = [$one]")
  })
  list.iterator
}, true)
val rdd3: RDD[String] = rdd2.repartition(3)
val rdd4: RDD[String] = rdd3.mapPartitionsWithIndex((index, iter) => {
  val arr: ArrayBuffer[String] = ArrayBuffer[String]()
  iter.foreach(one => {
    arr.append(s"rdd3 partition = [$index] ,value = [$one]")
  })
  arr.iterator
})
val results: Array[String] = rdd4.collect()
results.foreach(println)
sc.stop()

打印结果

两个RDD聚集
  1. cogroup算子

合并两个RDD,生成一个新的RDD。分区数与分区数多个那个RDD保持一致

val rdd1 = sc.parallelize(List[(String,String)](("zhangsan","female"),("zhangsan","female1"),("lisi","male"),("wangwu","female"),("maliu","male")),3)
val rdd2 = sc.parallelize(List[(String,Int)](("zhangsan",18),("lisi",19),("lisi",190),("wangwu",20),("tianqi",21)),4)
val resultRDD: RDD[(String, (Iterable[String], Iterable[Int]))] = rdd1.cogroup(rdd2)

resultRDD.foreach(info=>{
  val key = info._1
  val value1: List[String] = info._2._1.toList
  val value2: List[Int] = info._2._2.toList
  println("key ="+key+",value"+value1+", value2 = "+value2)
})
println("resultRDD partition length ="+resultRDD.getNumPartitions)
sc.stop()

打印结果

  • 连接
  1. join算子

会产生shuffle,(K,V)格式的RDD和(K,V)格式的RDD按照相同的K,join得到(K,(V,W))格式的数据,分区数按照大的来。

val nameRDD: RDD[(String, String)] = sc.parallelize(List[(String,String)](("zhangsan","female"),("lisi","male"),("wangwu","female")),3)
val scoreRDD: RDD[(String, Int)] = sc.parallelize(List[(String,Int)](("zhangsan",18),("lisi",19),("wangwu",20)),2)
val joinRDD: RDD[(String, (String, Int))] = nameRDD.join(scoreRDD)
println(joinRDD.getNumPartitions)
joinRDD.foreach(println)

打印结果
20. leftOutJoin和rightOutJoin算子、fullOuterJoin算子

leftOuterJoin(K,V)格式的RDD和(K,V)格式的RDD,使用leftOuterJoin结合,以左边的RDD出现的key为主 ,得到(K,(V,Option(W)))

val nameRDD: RDD[(String, String)] = sc.parallelize(
    List[(String,String)](("zhangsan","female"),
    ("lisi","male"),("wangwu","female"),("maliu","male")
    ))
val scoreRDD: RDD[(String, Int)] = sc.parallelize(
    List[(String,Int)](("zhangsan",22),("lisi",19),
    ("wangwu",20),("tianqi",21)
    ))
val leftOutJoin: RDD[(String, (String, Option[Int]))] = nameRDD.leftOuterJoin(scoreRDD)
leftOutJoin.foreach(println)
sc.stop()

打印结果

rightOuterJoin(K,V)格式的RDD和(K,W)格式的RDD使用rightOuterJoin结合以右边的RDD出现的key为主,得到(K,(Option(V),W))

val nameRDD: RDD[(String, String)] = sc.parallelize(
    List[(String,String)](("zhangsan","female"),("lisi","male")
    ,("wangwu","female"),("maliu","male")),3
    )
  val scoreRDD: RDD[(String, Int)] = sc.parallelize(
      List[(String,Int)](("zhangsan",18),("lisi",19),
      ("wangwu",20),("tianqi",21)),4
      )
  val rightOuterJoin: RDD[(String, (Option[String], Int))] = nameRDD.rightOuterJoin(scoreRDD)
  rightOuterJoin.foreach(println)
  println("rightOuterJoin RDD partiotion length = "+rightOuterJoin.getNumPartitions)
  sc.stop()

打印结果

fullOuterJoin算子(K,V)格式的RDD和(K,V)格式的RDD,使用fullOuterJoin结合是以两边的RDD出现的key为主,得到(K(Option(V),Option(W)))

val nameRDD: RDD[(String, String)] = sc.parallelize(List[(String,String)](("zhangsan","female"),("lisi","male"),("wangwu","female"),("maliu","male")),3)
val ageRDD: RDD[(String, Int)] = sc.parallelize(List[(String,Int)](("zhangsan",18),("lisi",16),("wangwu",20),("tianqi",21)),4)
val fullOuterJoin: RDD[(String, (Option[String], Option[Int]))] = nameRDD.fullOuterJoin(ageRDD)
fullOuterJoin.foreach(println)
println("fullOuterJoin RDD partition length = "+fullOuterJoin.getNumPartitions)
sc.stop()

打印结果

  1. intersection算子

intersection取两个RDD的交集,两个RDD的类型要一致,结果RDD的分区数要与两个父RDD多的那个一致

val rdd1: RDD[String] = sc.parallelize(List[String]("zhangsan","lisi","wangwu"),5)
val rdd2: RDD[String] = sc.parallelize(List[String]("zhangsan","lisi","maliu"),4)
val result: RDD[String] = rdd1.intersection(rdd2)
result.foreach(println)
println("intersection partition length = "+ result.getNumPartitions)
sc.stop()

打印结果

行动算子

Action算子
  • 无输出
  1. foreach算子

foreach遍历RDD中的每一个元素

val lines: RDD[String] = sc.textFile("./data/words") lines.foreach(println)
  • HDFS
  1. saveAsTextFile算子

将DataSet中的元素以文本的形式写入本地文件系统或者HDFS中,Spark将会对每个元素调用toString方法,将数据元素转换成文本文件中的一行数据,若将文件保存在本地文件系统,那么只会保存在executor所在机器的本地目录

val infos: RDD[String] = sc.parallelize(List[String]("a","b","c","e","f","g"),4)

infos.saveAsTextFile("./data/infos")

目录结构

  1. saveAsObjectFile算子

将数据集中元素以ObjectFile形式写入本地文件系统或者HDFS中

infos.saveAsObjectFile("./data/infosObject")

目录结构

  • Scala集合和数据类型
  1. collect算子

collect回收算子,会将结果回收到Driver端,如果结果比较大,就不要回收,这样的话会造成Driver端的OOM

val lines: RDD[String] = sc.textFile("./data/words")
sc.setLogLevel("Error")
val result: Array[String] = lines.collect()
result.foreach(println)

打印结果

  1. collectAsMap算子

将K、V格式的RDD回收到Driver端作为Map使用

val weightInfos: RDD[(String, Double)] = sc.parallelize(
    List[(String,Double)](new Tuple2("zhangsan",99),
        new Tuple2("lisi",78.6),
        new Tuple2("wangwu",122.2323)
        )
        )
val stringToDouble: collection.Map[String, Double] = weightInfos.collectAsMap()
stringToDouble.foreach(tp=>{
  println(tp._1+"**************"+tp._2)
})
sc.stop()

打印结果

  1. count,countByKey,CountByValue算子

count统计RDD共有多少行数据

val lines: RDD[String] = sc.textFile("./data/sampleData.txt")

val result: Long = lines.count()

println(result)

sc.stop()
直接给出结果行数

countByKey统计相同的key出现的个数

val rdd: RDD[(String, Integer)] = sc.makeRDD(List[(String,Integer)](
    ("a",1),("a",100),("a",1000),("b",2),("b",200),("c",3),("c",4),("d",122)
    ))
val result: collection.Map[String, Long] = rdd.countByKey()
result.foreach(println)

countByValue统计RDD中相同的Value出现的次数,不要求数据必须为RDD格式

val rdd = sc.makeRDD(List[(String,Integer)](
    ("a",1),("a",1),("a",1000),("b",2),("b",200),("c",3),("c",3)
    ))
val result: collection.Map[(String, Integer), Long] = rdd.countByValue()
result.foreach(println)

打印结果

  1. take、takeSample算子

take取出RDD中的前N个元素

val lines: RDD[String] = sc.textFile("./data/words")

val array: Array[String] = lines.take(3)

array.foreach(println)

takeSapmle(withReplacement,num,seed),随机抽样将数据结果拿回Driver端使用,返回Array,
withReplacement:有无放回抽样,num:抽样的条数,seed:种子

val lines: RDD[String] = sc.textFile("./data/words")

val result: Array[String] = lines.takeSample(false,3,10)

result.foreach(println)

打印结果

  1. reduce算子
val rdd: RDD[Int] = sc.makeRDD(Array[Int](1,2,3,4,5))

val result: Int = rdd.reduce((v1, v2) => { v1 + v2 })

//直接得到结果

println(result) }
  1. aggregate算子

首先是给定RDD的每一个分区一个初始值,然后RDD中每一个分区中按照相同的key,结合初始值去合并,最后RDD之间相同的key聚合

val rdd1: RDD[(String, Int)] = sc.makeRDD(List[(String, Int)](
  ("zhangsan", 10), ("zhangsan", 20), ("wangwu", 30),
  ("lisi", 40), ("zhangsan", 50), ("lisi", 60),
  ("wangwu", 70), ("wangwu", 80), ("lisi", 90)
), 3)
rdd1.mapPartitionsWithIndex((index,iter)=>{
  val arr: ArrayBuffer[(String, Int)] = ArrayBuffer[(String,Int)]()
  iter.foreach(tp=>{
    arr.append(tp)
    println("rdd1 partition index ="+index+", value ="+tp)
  })
  arr.iterator
}).count()
val result: RDD[(String, String)] = 
    rdd1.aggregateByKey("hello")(
        (s, v)=>{s+"~"+v}, (s1, s2)=>{s1+"#"+s2}
        )
result.foreach(println)

打印结果
打印结果
mapPartitionsWithIndex注释掉执行结果:
打印结果

  1. zip、zipWithlndex算子(转换算子)

zip算子 —Transformation类算子,将两个RDD合成一个K,V格式的RDD,分区数要相同,每个分区中的元素必须相同

val rdd1: RDD[String] = sc.parallelize(List[String]("a","b","c","d"),2)
val rdd2: RDD[Int] = sc.parallelize(List[Int](1,2,3,4),2)
val result: RDD[(String, Int)] = rdd1.zip(rdd2)
result.foreach(println)

打印结果
打印结果
zipWithIndex算子—Transformation类算子

val rdd1 = sc.parallelize(List[String]("a","b","c"),2)
val rdd2 = sc.parallelize(List[Int](1,2,3),numSlices = 2)
val result: RDD[(String, Long)] = rdd1.zipWithIndex()
val result2: RDD[(Int, Long)] = rdd2.zipWithIndex()
result.foreach(println)
result2.foreach(println)

打印结果
在这里插入图片描述

Spark内置函数RDD Spark中有收藏–Spark RDD的函数详解

将程序打包成jar包在集群上运行

pass

List匹配

//partition:按照某种条件对数据进行分类
        //def partition(p: A => Boolean): (Repr, Repr)
        println(List(1, 2, 3, 4, 5) partition (_ % 2 == 0))
        //(List(2, 4),List(1, 3, 5))
      
        //find:查找集合第一个符合条件的元素
        //def find(p: A => Boolean): Option[A]
        println(List(1, 2, 3, 4, 5) find (_ % 2 == 0)) //Some(2)
        println(List(1, 2, 3, 4, 5) find (_ <= 0))     //None
        
        //takeWhile:着个匹配符合条件的元素 直到不符合条件 之后的元素不在判断
        //override def takeWhile(p: A => Boolean): List[A]
        println(List(6, 5, 2, 3, 5) takeWhile (_ < 4))
        //List()
        
        //dropWhile:着个匹配去除符合条件的元素 直到不符合条件 之后的元素不在判断
        //override def dropWhile(p: A => Boolean): List[A]
        println(List(1, 2, 6, 4, 5) dropWhile (_ < 4))
        //List(6, 4, 5)
      
        //span 着个匹配按照某种条件对数据进行分类 直到不符合条件 之后的元素不在判断
        //override def span(p: A => Boolean): (List[A], List[A])
        println(List(1, 7, 3, 4, 5) span (_ < 4))
        //(List(1),List(7, 3, 4, 5))
        
        //forall  当所有的元素满足条件时 返回true 否则返回false
        //def forall(p: A => Boolean): Boolean
        println(List(1, 7, 3, 4, 5) forall (_ >= 4))   //false
        println(List(11, 7, 13, 4, 5) forall (_ >= 4)) //true
        println
        
        //exists  当存在(至少有一个满足)元素满足条件时 返回true 否则返回false
        //def exists(p: A => Boolean): Boolean
        println(List(1, 7, 3, 4, 5) exists (_ >= 4))   //true
        println(List(11, 7, 13, 4, 5) exists (_ >= 4)) //true
        
        println
        def hastotallyZeroRow(m: List[List[Int]]) = m exists (row => row forall (_ == 0))
        val m = List(List(1, 0, 0), List(0, 1, 0), List(0, 0, 0))
        println(hastotallyZeroRow(m))   //true

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值