Spark常用算子

Spark的算子分类:

从大方向说,Spark算子大致可以分为以下两类:

(1)Transformation变换/转换算子:这种变换并不触发提交作业,这种算子是延迟执行的,也就是说从一个RDD转换生成另一个RDD的转换操作不是马上执行,需要等到有Action操作的时候才会真正触发。

(2)Action行动算子:这类算子会触发SparkContext提交job作业,并将数据输出到Spark系统。

从小方向说,Spark算子大致可以分为以下三类:

(1)Value数据类型的Transformation算子,这种变换并不触发提交作业,针对处理的数据项是Value型的数据

(2)Key-Value 数据类型的Transformation算子,这种变换并不触发提交作业,针对处理的数据项是Key-Value型的数据对

(3)Action算子:这类算子会触发SparkContext提交Job作业

(一)Value数据类型的Transformation算子:

(1)输入分区与输出分区一对一型:

1.map算子

2.flatMap算子

3.mapPartitions算子

4.mapPartitionsWithIndex算子

(2)输入分区与输出分区多对一型

5.union算子

6.cartesian算子

(3)输入分区与输出分区多对多型

7.groupBy算子、groupByKey算子

(4)输出分区是输入分区子集类型

8.filter算子

9.distinct算子

10.subtract算子

11.sample算子

(5)Cache算子

13.cache算子

14.persist算子

(二)Key-Value数据类型的Transformation算子

(1)输入分区与输出分区一对一

15.mapValues算子

(2)对单个RDD或者两个RDD聚集

单个RDD聚集

16.combineByKey算子

17.reduceByKey算子

18.repartition算子

两个RDD聚集

19.cogroup算子

(3)连接

20.join算子

21.leftOutJoin和rightOutJoin算子、fullOuterJoin算子

(三)Action算子

(1)无输出

22.foreach算子

(2)HDFS

23.saveAsTextFile算子

24.saveAsObjectFile算子

(3)Scala集合和数据类型

25.collect算子

26.collectAsMap算子

27.count,countByKey,CountByValue算子

28.take、takeSample算子

29.reduce算子

30.aggregate算子

31.zip、zipWithIndex算子
 

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)})

执行结果:

183c8a62121b77e8ed83ac92006b9057.png


2.flatMap算子

 

flatMap是一对多的关系,处理一条数据得到多条结果

将原来 RDD 中的每个元素通过函数 f 转换为新的元素,并将生成的 RDD 的每个集合中的元素合并为一个集合。

adb01f5777805d4ba71b2cd04bfcd90c.png

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)


3.mapPartitions算子

 

mapPartitions遍历的是每一个分区中的数据,一个个分区的遍历。获 取 到 每 个 分 区 的 迭 代器,在 函 数 中 通 过 这 个 分 区 整 体 的 迭 代 器 对整 个 分 区 的 元 素 进 行 操 作,相对于map一条条处理数据,性能比较高,可获取返回值。

可以通过函数f(iter) =>iter.filter(_>=3)对分区中所有的数据进行过滤,大于和等于3的数据保留,一个方块代表一个RDD分区,含有1,2,3的分区过滤,只剩下元素3。

787e4189c5d6c4b57a4e8a5d633dba5b.png


4.mapPartitionsWithIndex(function)算子

拿到每个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)

 

f9fd83dbacf0f9f2f4e4efcaa70cb681.png

5.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)


6.cartesian算子

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

        设A,B为集合,用A中元素为第一元素,B中元素为第二元素构成有序对,所有这样的有序对组成的集合叫做A与B的笛卡尔积,记作AxB.

        笛卡尔积的符号化为:

                A×B={(x,y)|x∈A∧y∈B}

                例如,A={a,b}, B={0,1,2},则

                A×B={(a, 0), (a, 1), (a, 2), (b, 0), (b, 1), (b, 2)}

                B×A={(0, a), (0, b), (1, a), (1, b), (2, a), (2, b)}

7.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)

2905f03beeb6a84d2a9de5638fece01a.png

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)
})

8.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)

 

d178d3ae86ee2e9402813ac0d044237e.png

 

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)

4be672ed064d7a1843925f65500e3dc0.png

10.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)

 

6fdcbe3c2ca391148c3b6b15ccbf4bcc.png

 

11.sample算子

sample随机抽样,参数sample(withReplacement:有无放回抽样,fraction:抽样的比例,seed:用于指定的随机数生成器的种子)

有种子和无种子的区别:

有种子是只要针对数据源一样,都是指定相同的参数,那么每次抽样到的数据都是一样的

没有种子是针对同一个数据源,每次抽样都是随机抽样

b7f891db5bd6a79cdc4b237685777c50.png

 

62de5ab04ac74f71a5358c1ec2416168.png

 

 (12.13)cache算子、persist算子

49920cd39f56d2d0f5e6f82bf789ab96.png

package core.persist
 
import org.apache.spark.rdd.RDD
import org.apache.spark.storage.StorageLevel
import org.apache.spark.{SparkConf, SparkContext}
 
/**
  * cache()和persist()注意问题
  * 1.cache()和persist()持久化单位是partition,cache()和persist()是懒执行算子,需要action算子触发执行
  * 2.对一个RDD使用cache或者persist之后可以赋值给一个变量,下次直接使用这个变量就是使用持久化的数据。
  * 也可以直接对RDD进行cache或者persist,不赋值给一个变量
  * 3.如果采用第二种方法赋值给变量的话,后面不能紧跟action算子
  * 4.cache()和persist()的数据在当前application执行完成之后会自动清除
  */
object CacheAndPersist {
  def main(args: Array[String]): Unit = {
    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()
  }
}


14.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()

5fe4a7ea877b216c95e62405c4b6d4c5.png

 15.flatMapValues算子

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

val infos: RDD[(String, String)] = sc.makeRDD(
    List[(String,String)](
    ("zhangsan","18"),("lisi","20"),("wangwu","30")
    ))
    val transInfo: RDD[(String, String)] = infos.mapValues(s => {
      s + " " + "zhangsan18"
    })
//    transInfo.foreach(println)
    val result: RDD[(String, String)] = transInfo.flatMapValues(s => {
      //按空格切分
      s.split(" ")
    })
    result.foreach(println)
    sc.stop()

1699ea031fc1e5639ed35f48ce0b8d94.png

 16.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)

ced48c5b10064ae957cc5d0ca231d9eb.png

 17.reduceByKey算子

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

首先会根据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()

8d53ed2c3ea23ab6d935c7b6175e59f7.png

 18.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()

520e3d07c5b92eec2b24b0e9e7799000.png

 

19.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()

158b10269f558d3016ef1d020017e84f.png

 

20.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)

 

b09b79781c33f5fc3a491146734b6436.png

 

21.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()

7a4e6441dc6045cf219f437cc6829c88.png

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()

 

d0610973cfe32043af44fb7795b02472.png

 

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()

0aef396f9ebd4836f5f894c4e29d45eb.png

 

22.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()

9f773ff5cf9d4f6f9ebb70d3395b4fce.png

 

Action:

23.foreach算子

foreach遍历RDD中的每一个元素

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

24.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")

c9bc55285e995819d1460d2b28393b8f.png

632e0937dcd95fd6d656105aa3a2b322.png

 25.saveAsObjectFile算子

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

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

98c1a3bf871023243b531bff89d61dd0.png


26.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)

3c45988610d69d65c2146bba65e24fd5.png

 

27.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()

ebd7304c97191edceb580d2f79bd24b4.png

 

28.count算子

count统计RDD共有多少行数据

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

val result: Long = lines.count()

println(result)

sc.stop()

直接给出结果行数

29.countByKey算子、countByValue算子

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)

cadf8f6d3b1dc326cbd92ff72e8e7a06.png

 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)

c7a095e9bc8408a66384739f1e383273.png

 30、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)

39ceda6b3a29e926e6f5902637b6c119.png


31、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) }

32.Aggregate算子----transformation类算子

首先是给定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)

6e5b45d4589628f2602a5b6d15a298de.png

 mapPartitionsWithIndex注释掉执行结果:

c50567e6ef112bb89ad95f85a769a3cd.png


33.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)

4d39396e32b1262c2f1f7fa47419b641.png

 

34、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)

 

e1baf479d6ef95c16597e7059eb85098.png

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值