spark的RDD中的action(执行)和transformation(转换)两种操作中常见函数介绍

spark的RDD中的action(执行)和transformation(转换)两种操作中常使用的函数

(1) 弹性分布式数据集(RDD)
        Spark是以RDD概念为中心运行的。RDD是一个容错的、可以被并行操作的元素集合。创建一个RDD有两个方法:在你的驱动程序中并行化一个已经存在的集合;从外部存储系统中引用一个数据集。RDD的一大特性是分布式存储,分布式存储在最大的好处是可以让数据在不同工作节点并行存储,以便在需要数据时并行运算。弹性指其在节点存储时,既可以使用内存,也可已使用外存,为使用者进行大数据处理提供方便。除此之外,RDD的另一大特性是延迟计算,即一个完整的RDD运行任务被分为两部分:Transformation和Action.

在spark新版中,也许会有更多的action和transformation,可以参照spark的主页。
hadoop提供的接口只有map和reduce函数,spark是mapreduce的扩展,提供两类操作,而不是两个,使使用更方便,开发时的代码量会尽量的被spark的这种多样的API减少数十倍。
(2) RDD两种操作的简单介绍
2.1 Transformation
Transformation用于对RDD的创建,RDD只能使用Transformation创建,同时还提供大量操作方法,包括map,filter,groupBy,join等,RDD利用这些操作生成新的RDD,但是需要注意,无论多少次Transformation,在RDD中真正数据计算Action之前都不可能真正运行。
2.2 Action
Action是数据执行部分,其通过执行count,reduce,collect等方法真正执行数据的计算部分。实际上,RDD中所有的操作都是Lazy模式进行,运行在编译中不会立即计算最终结果,而是记住所有操作步骤和方法,只有显示的遇到启动命令才执行。这样做的好处在于大部分前期工作在Transformation时已经完成,当Action工作时,只需要利用全部自由完成业务的核心工作。

2.3函数概览,如下图

(3) 下面是在python中对RDD的生成,以及一些基本的Transformation,Action操作

# -*- coding:utf-8 -*-
from pyspark import SparkContext, SparkConf
from pyspark.streaming import StreamingContext
import math
appName ="test_spark_1" #你的应用程序名称
master= "local"#设置单机
conf = SparkConf().setAppName(appName).setMaster(master)#配置SparkContext
sc = SparkContext(conf=conf)
 
# parallelize:并行化数据,转化为RDD
data = [1, 2, 3, 4, 5]
distData = sc.parallelize(data, numSlices=10)  # numSlices为分块数目,根据集群数进行分块
 
# textFile读取外部数据
rdd = sc.textFile("./c2.txt")  # 以行为单位读取外部文件,并转化为RDD
print rdd.collect()
 
# map:迭代,对数据集中数据进行单独操作
def my_add(l):
    return (l,l)
data = [1, 2, 3, 4, 5]
distData = sc.parallelize(data)  # 并行化数据集
result = distData.map(my_add)
print (result.collect())  # 返回一个分布数据集

3.1 Spark 的12个Actions 操作函数总结及举例

Actions算子是Spark算子的一类,这一类算子会触发SparkContext提交job作业。下面介绍常用的Spark支持的actions。

1. reduce(func) 
使用函数func(两个输入参数,返回一个值)对数据集中的元素做聚集操作。函数func必须是可交换的(我理解的就是两个参数互换位置对结果不影响),并且是相关联的,从而能够正确的进行并行计算。

>>> data = sc.parallelize(range(1,101))
>>> data.reduce(lambda x, y: x+y)
5050

2. collect() 
在driver程序中以数组形式返回数据集中所有的元素。这以action通常在执行过filter或者其他操作后返回一个较小的子数据集时非常有用。

>>> data = sc.parallelize(range(1,101))
>>> data.filter(lambda x: x%10==0).collect()
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]

3. count() 
返回数据集中元素的个数。

>>> data.count()
100

4. first() 
返回数据集中的第一个元素,相当于take(1)。

>>> data.first()
1

5. take() 
以数组形式返回数据集中前n个元素。需要注意的是,这一action并不是在多个node上并行执行,而是在driver程序所在的机器上单机执行,会增大内存的压力,使用需谨慎。

>>> data.take(10)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

6. takeSample(withReplacement, num, [seed]) 
以数组形式返回从数据集中抽取的样本数量为num的随机样本,有替换或者无替换的进行采样。可选参数[seed]可以允许用户自己预定义随机数生成器的种子。

>>> data.takeSample(False, 20)
[60, 97, 91, 62, 48, 7, 49, 89, 40, 44, 15, 2, 33, 8, 30, 82, 87, 96, 32, 31]   
>>> data.takeSample(True, 20)
[96, 71, 20, 71, 80, 42, 70, 93, 77, 26, 14, 82, 50, 30, 30, 56, 93, 46, 70, 70]

7. takeOrdered(n, [ordering]) 
返回RDD的前n个元素,可以利用自然顺序或者由用户执行排序的comparator。

>>> score = [('Amy',98),('Bob',87),('David',95),('Cindy',76),('Alice',84),('Alice',33)]
>>> scoreRDD = sc.parallelize(score)
>>> scoreRDD.takeOrdered(3)
[('Alice', 33), ('Alice', 84), ('Amy', 98)]  #可以根据两个Alice的例子看到,当元祖中第一个元素相同时,会继续比较第二个元素,仍然按升序排列
>>> scoreRDD.takeOrdered(3, key=lambda x: x[1])  #按照分数升序排序
[('Alice', 33), ('Cindy', 76), ('Alice', 84)]
>>> scoreRDD.takeOrdered(3, key=lambda x: -x[1])  #按照分数降序排序
[('Amy', 98), ('David', 95), ('Bob', 87)]

注意,第2个参数这里是一个匿名函数,这个匿名函数并不会改变scoreRDD中的值,也就是第3个例子中,并不是将每个人的分数变为负数,而是提供一个排序的依据,说明此时为降序排序。如果是想要改变RDD中的值,可以进行如下操作:

>>> scoreRDD.map(lambda x: (x[0], -x[1])).takeOrdered(3, lambda x: x[1])
[('Amy', -98), ('David', -95), ('Bob', -87)]

这个例子并没有什么实际意义,只是提醒takeOrdered算子中第二个参数的作用。

8. saveAsTextFile(path) 
将数据集中的元素以文本文件(或者文本文件的一个集合)的形式写入本地文件系统,或者HDFS,或者其他Hadoop支持的文件系统的指定路径path下。Spark会调用每个元素的toString方法,将其转换为文本文件中的一行。

9. saveAsSequenceFile(path) 
将数据集中的元素以Hadoop SequenceFile的形式写入本地文件系统,或者HDFS,或者其他Hadoop支持的文件系统的指定路径path下。RDD的元素必须由实现了Hadoop的Writable接口的key-value键值对组成。在Scala中,也可以是隐式可以转换为Writable的键值对(Spark包括了基本类型的转换,例如Int,Double,String等等)

10. saveAsObjectFile(path) 
利用Java序列化,将数据集中的元素以一种简单的形式进行写操作,并能够利用SparkContext.objectFile()加载数据。(适用于Java和Scala)

11. countByKey() 
只能作用于键值对(K, V)形式的RDDs上。按照Key进行计数,返回键值对(K, int)的哈希表。

>>> score = [('Amy',98),('Bob',87),('David',95),('Cindy',76),('Alice',84),('Alice',33)]  #一组学生对应的成绩
>>> scoreRDD = sc.parallelize(score)
>>> scoreRDD.countByKey()
defaultdict(<class 'int'>, {'Cindy': 1, 'Alice': 2, 'Bob': 1, 'Amy': 1, 'David': 1})  
>>> result = scoreRDD.countByKey()
>>> type(result)  #查看返回值类型
<class 'collections.defaultdict'> 
>>> result['Alice']
2
>>> result['Sunny']
0
>>> testDict = {'Cindy': 1, 'Alice': 2, 'Bob': 1, 'Amy': 1, 'David': 1}
>>> testDict['Sunny']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'Sunny'

!!!特别注意,Pyspark中返回的是一个collections.defaultdict()类,collections是python的一个模块,是一个数据类型容器模块,需要注意defaultdict与dict还是有区别的。defaultdict是Python内建函数dict的一个子类,构建的是一个类似dictionary的对象,其中key的值是自行赋值,但是value的类型,是function_factory(工厂函数)的类实例,即使对于一个key,它的value值有缺失,也会有一个默认值。上述代码中最后的例子可以看书,虽然result和testDict中都没有key为‘Sunny’这个键值对,但是result会返回一个默认值0,而testDict就出现了KeyError的错误。关于defaultdict和dict的区别,这里就不做过多的解释,但是大家需要注意这里返回的类型并不是dict。

12. foreach(func) 
在数据集的每个元素上调用函数func。这一操作通常是为了实现一些副作用,比如更新累加器或者与外部存储系统进行交互。注意:在foreach()之外修改除了累加器以外的变量可能造成一些未定义的行为。更多内容请参阅闭包进行理解。

3.2  Spark 的20个Transformations 操作函数总结及举例

1. map(func) 

将func函数作用到数据集的每个元素,生成一个新的分布式的数据集并返回

>>> a = sc.parallelize(('a', 'b', 'c'))
>>> a.map(lambda x: x+'1').collect()
['a1', 'b1', 'c1']

2. filter(func) 
选出所有func返回值为true的元素,作为一个新的数据集返回

>>> a = sc.parallelize(range(10))
>>> a.filter(lambda x: x%2==0).collect()  # 选出0-9的偶数
[0, 2, 4, 6, 8]

3. flatMap(func) 
与map相似,但是每个输入的item能够被map到0个或者更多的items输出,也就是说func的返回值应当是一个Sequence,而不是一个单独的item

>>> l = ['I am Tom', 'She is Jenny', 'He is Ben']
>>> a = sc.parallelize(l,3)
>>> a.flatMap(lambda line: line.split()).collect()  # 将每个字符串中的单词划分出来
['I', 'am', 'Tom', 'She', 'is', 'Jenny', 'He', 'is', 'Ben']

4. mapPartitions(func) 
与map相似,但是mapPartitions的输入函数单独作用于RDD的每个分区(block)上,因此func的输入和返回值都必须是迭代器iterator。 
例如:假设RDD有十个元素0~9,分成三个区,使用mapPartitions返回每个元素的平方。如果使用map方法,map中的输入函数会被调用10次,而使用mapPartitions方法,输入函数只会被调用3次,每个分区被调用1次。

>>> def squareFunc(a):
. . .     for i in a:
. . .         yield i*i
. . .
>>> a = sc.parallelize(range(10), 3)
PythonRDD[1] at RDD at PythonRDD.scala:48
>>> a.mapPartitions(squareFunc).collect()
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

5. mapPartitionsWithIndex(func) 
与mapPartitions相似,但是输入函数func提供了一个正式的参数,可以用来表示分区的编号。

>>> def func(index, iterator):  # 返回每个分区的编号和数值
. . .     yield (‘index ‘ + str(index) + ’ is: ‘ + str(list(iterator)))
. . .
>>> a = sc.parallelize(range(10),3)
>>> a.mapPartitionsWithIndex(func).collect()
['index 0 is: [0, 1, 2]', 'index 1 is: [3, 4, 5]', 'index 2 is: [6, 7, 8, 9]']
>>> def squareIndex(index, iterator):  # 返回每个数值所属分区的编号和数值的平方
...     for i in iterator:
...         yield ("The index is: " + str(index) + ", and the square is: " + str(i*i))
... 
>>> a.mapPartitionsWithIndex(squareIndex).collect()
['The index is: 0, and the square is: 0', 'The index is: 0, and the square is: 1', 'The index is: 1, and the square is: 4', 'The index is: 1, and the square is: 9', 'The index is: 1, and the square is: 16', 'The index is: 2, and the square is: 25', 'The index is: 2, and the square is: 36', 'The index is: 3, and the square is: 49', 'The index is: 3, and the square is: 64', 'The index is: 3, and the square is: 81']

6. sample(withReplacement, fraction, seed) 
从数据中抽样,withReplacement表示是否有放回,withReplacement=true表示有放回抽样,fraction为抽样的概率(0<=fraction<=1),seed为随机种子。 
例如:从1-100之间抽取样本,被抽取为样本的概率为0.2

>>> data = sc.parallelize(range(1,101),2)
>>> sample = data.sample(True, 0.2)
>>> sampleData.count()
19
>>> sampleData.collect()
[16, 19, 24, 29, 32, 33, 44, 45, 55, 56, 56, 57, 65, 65, 73, 83, 84, 92, 96]

!!!注意,Spark中的sample抽样,当withReplacement=True时,相当于采用的是泊松抽样;当withReplacement=False时,相当于采用伯努利抽样,fraction并不是表示抽样得到的样本占原来数据总量的百分比,而是一个元素被抽取为样本的概率。fraction=0.2并不是说明要抽出100个数字中20%的数据作为样本,而是每个数字被抽取为样本的概率为0.2,这些数字被认为来自同一总体,样本的大小并不是固定的,而是服从二项分布。

7. union(otherDataset) 
并集操作,将源数据集与union中的输入数据集取并集,默认保留重复元素(如果不保留重复元素,可以利用distinct操作去除,下边介绍distinct时会介绍)。

>>> data1 = sc.parallelize(range(10))
>>> data2 = sc.parallelize(range(6,15))
>>> data1.union(data2).collect()
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 7, 8, 9, 10, 11, 12, 13, 14]

8. intersection(otherDataset) 
交集操作,将源数据集与union中的输入数据集取交集,并返回新的数据集。

>>> data1 = sc.parallelize(range(10))
>>> data2 = sc.parallelize(range(6,15))
>>> data1.intersection(data2).collect()
[8, 9, 6, 7]

9. distinct([numTasks]) 
去除数据集中的重复元素。

>>> data1 = sc.parallelize(range(10))
>>> data2 = sc.parallelize(range(6,15))
>>> data1.union(data2).distinct().collect()
[0, 8, 1, 9, 2, 10, 11, 3, 12, 4, 5, 13, 14, 6, 7]

下边的一系列transactions会用的键(Key)这一概念,在进行下列有关Key操作时使用的数据集为记录伦敦各个片区(英文称为ward)中学校和学生人数相关信息的表格,下载地址: 
https://data.london.gov.uk/dataset/london-schools-atlas/resource/64f771ee-38b1-4eff-8cd2-e9ba31b90685# 
下载后将其中命名为WardtoSecSchool_LDS_2015的sheet里边的数据保存为csv格式,删除第一行的表头,并重新命名为school.csv 
数据格式为: 
(Ward_CODE, Ward_NAME, TotalWardPupils, Ward2Sec_Flow_No., Secondary_School_URN, Secondary_School_Name, Pupil_count) 
首先对数据进行一些预处理:

>>> school = sc.textFile("file:///home/yang/下载/school.csv")  
Data = sc.textFile("file:///home/yang/下载/school.csv") 
>>> school.count()  # 共有16796行数据
16796
>>> import re  # 引入python的正则表达式包
>>> rows = school.map(lambda line: re.subn(',[\s]+',': ', line))

注意:1. 从本地读取数据时,代码中要通过 “file://” 前缀指定读取本地文件。Spark shell 默认是读取 HDFS 中的文件,需要先上传文件到 HDFS 中,否则会有“org.apache.hadoop.mapred.InvalidInputException: Input path does not exist: hdfs://localhost:9000/user/hadoop/school.csv”的错误。 
2. 对数据集进行了一下预处理,利用正则匹配替换字符串,由于一些学校的名字的字符串中本身含有逗号,比如“The City Academy, Hackney”, 此时如果利用csv的分隔符’,’进行分割,并不能将名字分割为“The City Academy”和“Hackney”。我们注意到csv的分隔符逗号后边是没有空格的,而名字里边的逗号后边都会有空格(英语书写习惯),因此,先利用re.subn语句对逗号后边含有至少一个空格(正则表达式为’,[\s]+’)的子字符串进行替换,替换为’: ’,然后再进行后续操作。以上即为对这一数据集的预处理过程。

10. groupByKey([numTasks]) 
作用于由键值对(K, V)组成的数据集上,将Key相同的数据放在一起,返回一个由键值对(K, Iterable)组成的数据集。 
注意:1. 如果这一操作是为了后续在每个键上进行聚集(aggregation),比如sum或者average,此时使用reduceByKey或者aggregateByKey的效率更高。2. 默认情况下,输出的并行程度取决于RDD分区的数量,但也可以通过给可选参数numTasks赋值来调整并发任务的数量。

>>> newRows = rows.map(lambda r: r[0].split(','))  
>>> ward_schoolname = newRows .map(lambda r: (r[1], r[5])).groupByKey()  # r[1]为ward的名字,r[5]为学校的名字
>>> ward_schoolname.map(lambda x: {x[0]: list(x[1])}).collect()  # 列出每个ward区域内所有的学校的名字
[{'Stifford Clays': ['William Edwards School', 'Brentwood County High School', "The Coopers' Company and Coborn School", 'Becket Keys Church of England Free School', ...] # 输出结果为在Stifford Clays这个ward里的学校有William Edwards School,Brentwood County High School,The Coopers' Company and Coborn School等等...

11. reduceByKey(func, [numTasks]) 
作用于键值对(K, V)上,按Key分组,然后将Key相同的键值对的Value都执行func操作,得到一个值,注意func的类型必须满足

>>> pupils = newRows.map(lambda r: (r[1], int(r[6])))  # r[1]为ward的名字,r[6]为每个学校的学生数
>>> ward_pupils = pupils.reduceByKey(lambda x, y: x+y)   # 计算各个ward中的学生数
>>> ward_pupils.collect()  # 输出各个ward中的学生数
[('Stifford Clays', 1566), ('Shenley', 1625), ('Southbury', 3526), ('Rainham and Wennington', 769), ('Bromley Town', 574), ('Waltham Abbey Honey Lane', 835), ('Telegraph Hill', 1238), ('Chigwell Village', 1506), ('Gooshays', 2097), ('Edgware', 2585), ('Camberwell Green', 1374), ('Glyndon', 4633),...]

12. aggregateByKey(zeroValue, seqOp, comOp, [numTasks]) 
在于键值对(K, V)的RDD中,按key将value进行分组合并,合并时,将每个value和初始值作为seqOp函数的参数,进行计算,返回的结果作为一个新的键值对(K, V),然后再将结果按照key进行合并,最后将每个分组的value传递给comOp函数进行计算(先将前两个value进行计算,将返回结果和下一个value传给comOp函数,以此类推),将key与计算结果作为一个新的键值对(K, V)输出。 
例子: 上述统计ward内学生人数的操作也可以通过aggregateByKey实现,此时,seqOp和comOp都是进行加法操作,代码如下:

>>> ward_pupils = pupils.aggregateByKey(0, lambda x, y: x+y, lambda x, y: x+y)
>>> ward_pupils.collect()  
[('Stifford Clays', 1566), ('Shenley', 1625), ('Southbury', 3526), ('Rainham and Wennington', 769), ('Bromley Town', 574), ('Waltham Abbey Honey Lane', 835), ('Telegraph Hill', 1238), ('Chigwell Village', 1506), ('Gooshays', 2097), ('Edgware', 2585), ('Camberwell Green', 1374), ('Glyndon', 4633),...]

13. sortByKey([ascending=True], [numTasks]) 
按照Key进行排序,ascending的值默认为True,True/False表示升序还是降序 
例如:将上述ward按照ward名字降序排列,打印出前十个

>>> ward_pupils.sortByKey(False, 4).take(10)
[('Yiewsley', 2560), ('Wormholt and White City', 1455), ('Woodside', 1204), ('Woodhouse', 2930), ('Woodcote', 1214), ('Winchmore Hill', 1116), ('Wilmington', 2243), ('Willesden Green', 1896), ('Whitefoot', 676), ('Whalebone', 2294)]

14. join(otherDataset, [numTasks]) 
类似于SQL中的连接操作,即作用于键值对(K, V)和(K, W)上,返回元组 (K, (V, W)),spark也支持外连接,包括leftOuterJoin,rightOuterJoin和fullOuterJoin。例子:

>>> class1 = sc.parallelize(('Tom', 'Jenny', 'Bob')).map(lambda a: (a, 'attended'))
>>> class2 = sc.parallelize(('Tom', 'Amy', 'Alice', 'John')).map(lambda a: (a, 'attended'))
>>> class1.join(class2).collect()
[('Tom', ('attended', 'attended'))]
>>> class1.leftOuterJoin(class2).collect()
[('Tom', ('attended', 'attended')), ('Jenny', ('attended', None)), ('Bob', ('attended', None))]
>>> class1.rightOuterJoin(class2).collect()
[('John', (None, 'attended')), ('Tom', ('attended', 'attended')), ('Amy', (None, 'attended')), ('Alice', (None, 'attended'))]
>>> class1.fullOuterJoin(class2).collect()
[('John', (None, 'attended')), ('Tom', ('attended', 'attended')), ('Jenny', ('attended', None)), ('Bob', ('attended', None)), ('Amy', (None, 'attended')), ('Alice', (None, 'attended'))]

15. cogroup(otherDataset, [numTasks]) 
作用于键值对(K, V)和(K, W)上,返回元组 (K, (Iterable, Iterable))。这一操作可叫做groupWith

>>> class1 = sc.parallelize(('Tom', 'Jenny', 'Bob')).map(lambda a: (a, 'attended'))
>>> class2 = sc.parallelize(('Tom', 'Amy', 'Alice', 'John')).map(lambda a: (a, 'attended'))
>>> group = class1.cogroup(class2)
>>> group.collect()
[('John', (<pyspark.resultiterable.ResultIterable object at 0x7fb7e808afd0>, <pyspark.resultiterable.ResultIterable object at 0x7fb7e808a1d0>)), ('Tom', (<pyspark.resultiterable.ResultIterable object at 0x7fb7e808a7f0>, <pyspark.resultiterable.ResultIterable object at 0x7fb7e808a048>)), ('Jenny', (<pyspark.resultiterable.ResultIterable object at 0x7fb7e808a9b0>, <pyspark.resultiterable.ResultIterable object at 0x7fb7e808a208>)), ('Bob', (<pyspark.resultiterable.ResultIterable object at 0x7fb7e808ae80>, <pyspark.resultiterable.ResultIterable object at 0x7fb7e8b448d0>)), ('Amy', (<pyspark.resultiterable.ResultIterable object at 0x7fb7e8b44c88>, <pyspark.resultiterable.ResultIterable object at 0x7fb7e8b44588>)), ('Alice', (<pyspark.resultiterable.ResultIterable object at 0x7fb7e8b44748>, <pyspark.resultiterable.ResultIterable object at 0x7fb7e8b44f98>))]
>>> group.map(lambda x: {x[0]: [list(x[1][0]), list(x[1][1])]}).collect()
[{'John': [[], ['attended']]}, {'Tom': [['attended'], ['attended']]}, {'Jenny': [['attended'], []]}, {'Bob': [['attended'], []]}, {'Amy': [[], ['attended']]}, {'Alice': [[], ['attended']]}]

16. cartesian(otherDataset) 
笛卡尔乘积,作用于数据集T和U上,返回(T, U),即数据集中每个元素的两两组合

>>> a = sc.parallelize(('a', 'b', 'c'))
>>> b = sc.parallelize(('d', 'e', 'f'))
>>> a.cartesian(b).collect()
[('a', 'd'), ('a', 'e'), ('a', 'f'), ('b', 'd'), ('b', 'e'), ('b', 'f'), ('c', 'd'), ('c', 'e'), ('c', 'f')]

17. pipe(command, [envVars]) 
将驱动程序中的RDD交给shell处理(外部进程),例如Perl或bash脚本。RDD元素作为标准输入传给脚本,脚本处理之后的标准输出会作为新的RDD返回给驱动程序。

18. coalesce(numPartitions) 
将RDD的分区数减小到numPartitions个。当数据集通过过滤规模减小时,使用这个操作可以提升性能。

19. repartition(numPartitions) 
重组数据,数据被重新随机分区为numPartitions个,numPartitions可以比原来大,也可以比原来小,平衡各个分区。这一操作会将整个数据集在网络中重新洗牌。

20. repartitionAndSortWithinPartitions(partitioner) 
根据给定的partitioner函数重新将RDD分区,并在分区内排序。这比先repartition然后在分区内sort高效,原因是这样迫使排序操作被移到了shuffle阶段。

参考网址:https://www.cnblogs.com/adienhsuan/p/5654485.html
http://blog.csdn.net/zhangyang10d/article/details/53239404
http://blog.csdn.net/zhangyang10d/article/details/53146953?locationNum=13&fps=1
http://blog.csdn.net/egraldloi/article/details/16343733

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值