MapReduce应用实例


1.各个部门的总工资如何进行问题分析的?
2.各个部门的总工资处理流程是怎样的?
3.个部门的人数和平均工资又是如何得到的?
4.个部门的人数和平均工资代码如何实现?






案例所用包全部下载:链接: http://pan.baidu.com/s/1sjNyDIX 密码:
游客,如果您要查看本帖隐藏内容请 回复

1、环境说明
部署节点操作系统为CentOS,防火墙和SElinux禁用,创建了一个shiyanlou用户并在系统根目录下创建/app目录,用于存放Hadoop等组件运行包。因为该目录用于安装hadoop等组件程序,用户对shiyanlou必须赋予rwx权限(一般做法是root用户在根目录下创建/app目录,并修改该目录拥有者为shiyanlou(chown –R shiyanlou:shiyanlou /app)。
Hadoop搭建环境:
l  虚拟机操作系统: CentOS6.6  64位,单核,1G内存
l  JDK:1.7.0_55 64位
l  Hadoop:1.1.2
2、准备测试数据
测试数据包括两个文件dept(部门)和emp(员工),其中各字段用逗号分隔:
dept文件内容:
[Bash shell] 纯文本查看 复制代码
?
1
2
3
4
10,ACCOUNTING,NEW YORK
20,RESEARCH,DALLAS
30,SALES,CHICAGO
40,OPERATIONS,BOSTON

emp文件内容:
[Bash shell] 纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
7369,SMITH,CLERK,7902,17-12月-80,800,,20
7499,ALLEN,SALESMAN,7698,20-2月-81,1600,300,30
7521,WARD,SALESMAN,7698,22-2月-81,1250,500,30
7566,JONES,MANAGER,7839,02-4月-81,2975,,20
7654,MARTIN,SALESMAN,7698,28-9月-81,1250,1400,30
7698,BLAKE,MANAGER,7839,01-5月-81,2850,,30
7782,CLARK,MANAGER,7839,09-6月-81,2450,,10
7839,KING,PRESIDENT,,17-11月-81,5000,,10
7844,TURNER,SALESMAN,7698,08-9月-81,1500,0,30
7900,JAMES,CLERK,7698,03-12月-81,950,,30
7902,FORD,ANALYST,7566,03-12月-81,3000,,20
7934,MILLER,CLERK,7782,23-1月-82,1300,,10

在/home/shiyanlou/install-pack/class6目录可以找到这两个文件,把这两个文件上传到HDFS中/class6/input目录中,执行如下命令:
[Bash shell] 纯文本查看 复制代码
?
1
2
3
4
5
cd /home/shiyanlou/install-pack/class6
hadoop fs - mkdir -p /class6/input
hadoop fs -copyFromLocal dept /class6/input
hadoop fs -copyFromLocal emp /class6/input
hadoop fs - ls /class6/input

3、应用案例3.1 测试例子1:求各个部门的总工资3.1.1 问题分析
MapReduce中的join分为好几种,比如有最常见的 reduce side join、map side join和semi join 等。reduce join 在shuffle阶段要进行大量的数据传输,会造成大量的网络IO效率低下,而map side join 在处理多个小表关联大表时非常有用 。
Map side join是针对以下场景进行的优化:两个待连接表中,有一个表非常大,而另一个表非常小,以至于小表可以直接存放到内存中。这样我们可以将小表复制多份,让每个map task内存中存在一份(比如存放到hash table中),然后只扫描大表:对于大表中的每一条记录key/value,在hash table中查找是否有相同的key的记录,如果有,则连接后输出即可。为了支持文件的复制,Hadoop提供了一个类DistributedCache,使用该类的方法如下:
(1)用户使用静态方法DistributedCache.addCacheFile()指定要复制的文件,它的参数是文件的URI(如果是HDFS上的文件,可以这样:hdfs://jobtracker:50030/home/XXX/file)。JobTracker在作业启动之前会获取这个URI列表,并将相应的文件拷贝到各个TaskTracker的本地磁盘上。
(2)用户使用DistributedCache.getLocalCacheFiles()方法获取文件目录,并使用标准的文件读写API读取相应的文件。
在下面代码中,将会把数据量小的表(部门dept)缓存在内存中,在Mapper阶段对员工部门编号映射成部门名称,该名称作为key输出到Reduce中,在Reduce中计算按照部门计算各个部门的总工资。
3.1.2 处理流程图
3.1.3 测试代码
Q1SumDeptSalary.java代码(vi编辑代码是不能存在中文):

[Java] 纯文本查看 复制代码
?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
 
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.filecache.DistributedCache;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
 
public class Q1SumDeptSalary extends Configured implements Tool {
 
     public static class MapClass extends Mapper<LongWritable, Text, Text, Text> {
 
         // 用于缓存 dept文件中的数据
         private Map<String, String> deptMap = new HashMap<String, String>();
         private String[] kv;
 
         // 此方法会在Map方法执行之前执行且执行一次
         @Override
         protected void setup(Context context) throws IOException, InterruptedException {
             BufferedReader in = null ;
             try {
 
                 // 从当前作业中获取要缓存的文件
                 Path[] paths = DistributedCache.getLocalCacheFiles(context.getConfiguration());
                 String deptIdName = null ;
                 for (Path path : paths) {
 
                     // 对部门文件字段进行拆分并缓存到deptMap中
                     if (path.toString().contains( "dept" )) {
                         in = new BufferedReader( new FileReader(path.toString()));
                         while ( null != (deptIdName = in.readLine())) {
                             
                             // 对部门文件字段进行拆分并缓存到deptMap中
                             // 其中Map中key为部门编号,value为所在部门名称
                             deptMap.put(deptIdName.split( "," )[ 0 ], deptIdName.split( "," )[ 1 ]);
                         }
                     }
                 }
             } catch (IOException e) {
                 e.printStackTrace();
             } finally {
                 try {
                     if (in != null ) {
                         in.close();
                     }
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
         }
 
public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
 
             // 对员工文件字段进行拆分
             kv = value.toString().split( "," );
 
             // map join: 在map阶段过滤掉不需要的数据,输出key为部门名称和value为员工工资
             if (deptMap.containsKey(kv[ 7 ])) {
                 if ( null != kv[ 5 ] && ! "" .equals(kv[ 5 ].toString())) {
                     context.write( new Text(deptMap.get(kv[ 7 ].trim())), new Text(kv[ 5 ].trim()));
                 }
             }
         }
     }
 
     public static class Reduce extends Reducer<Text, Text, Text, LongWritable> {
 
public void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
 
             // 对同一部门的员工工资进行求和
             long sumSalary = 0 ;
             for (Text val : values) {
                 sumSalary += Long.parseLong(val.toString());
             }
 
             // 输出key为部门名称和value为该部门员工工资总和
             context.write(key, new LongWritable(sumSalary));
         }
     }
 
     @Override
     public int run(String[] args) throws Exception {
 
         // 实例化作业对象,设置作业名称、Mapper和Reduce类
         Job job = new Job(getConf(), "Q1SumDeptSalary" );
         job.setJobName( "Q1SumDeptSalary" );
         job.setJarByClass(Q1SumDeptSalary. class );
         job.setMapperClass(MapClass. class );
         job.setReducerClass(Reduce. class );
 
         // 设置输入格式类
         job.setInputFormatClass(TextInputFormat. class );
 
         // 设置输出格式
         job.setOutputFormatClass(TextOutputFormat. class );
         job.setOutputKeyClass(Text. class );
         job.setOutputValueClass(Text. class );
 
         // 第1个参数为缓存的部门数据路径、第2个参数为员工数据路径和第3个参数为输出路径
     String[] otherArgs = new GenericOptionsParser(job.getConfiguration(), args).getRemainingArgs();
     DistributedCache.addCacheFile( new Path(otherArgs[ 0 ]).toUri(), job.getConfiguration());
         FileInputFormat.addInputPath(job, new Path(otherArgs[ 1 ]));
         FileOutputFormat.setOutputPath(job, new Path(otherArgs[ 2 ]));
 
         job.waitForCompletion( true );
         return job.isSuccessful() ? 0 : 1 ;
     }
 
     /**
      * 主方法,执行入口
      * @param args 输入参数
      */
     public static void main(String[] args) throws Exception {
         int res = ToolRunner.run( new Configuration(), new Q1SumDeptSalary(), args);
         System.exit(res);
     }
}


3.1.4 编译并打包代码
进入/app/hadoop-1.1.2/myclass/class6目录中新建Q1SumDeptSalary.java程序代码(代码页可以使用/home/shiyanlou/install-pack/class6/Q1SumDeptSalary.java文件)
cd /app/hadoop-1.1.2/myclass/class6
vi Q1SumDeptSalary.java
编译代码
javac -classpath ../../hadoop-core-1.1.2.jar:../../lib/commons-cli-1.2.jar Q1SumDeptSalary.java
把编译好的代码打成jar包(如果不打成jar形式运行会提示class无法找到的错误)
jar cvf ./Q1SumDeptSalary.jar ./Q1SumDept*.class
mv *.jar ../..
rm Q1SumDept*.class
3.1.5 运行并查看结果
运行Q1SumDeptSalary时需要输入部门数据路径、员工数据路径和输出路径三个参数,需要注意的是hdfs的路径参数路径需要全路径,否则运行会报错:
l  部门数据路径:hdfs://hadoop:9000/class6/input/dept,部门数据将缓存在各运行任务的节点内容中,可以提供处理的效率
l  员工数据路径:hdfs://hadoop:9000/class6/input/emp
l  输出路径:hdfs://hadoop:9000/class6/out1

运行如下命令:
cd /app/hadoop-1.1.2
hadoop jar Q1SumDeptSalary.jar Q1SumDeptSalary hdfs://hadoop:9000/class6/input/dept hdfs://hadoop:9000/class6/input/emp hdfs://hadoop:9000/class6/out1
运行成功后,刷新CentOS HDFS中的输出路径/class6/out1目录,打开part-r-00000文件
hadoop fs -ls /class6/out1
hadoop fs -cat /class6/out1/part-r-00000
可以看到运行结果:
ACCOUNTING8750
RESEARCH6775
SALES  9400
3.2 测试例子2:求各个部门的人数和平均工资3.2.1 问题分析
求各个部门的人数和平均工资,需要得到各部门工资总数和部门人数,通过两者相除获取各部门平均工资。首先和问题1类似在Mapper的Setup阶段缓存部门数据,然后在Mapper阶段抽取出部门编号和员工工资,利用缓存部门数据把部门编号对应为部门名称,接着在Shuffle阶段把传过来的数据处理为部门名称对应该部门所有员工工资的列表,最后在Reduce中按照部门归组,遍历部门所有员工,求出总数和员工数,输出部门名称和平均工资。
3.2.2 处理流程图
3.2.3 编写代码
Q2DeptNumberAveSalary.java代码:

[Java] 纯文本查看 复制代码
?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
 
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.filecache.DistributedCache;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
 
public class Q2DeptNumberAveSalary extends Configured implements Tool {
 
     public static class MapClass extends Mapper<LongWritable, Text, Text, Text> {
 
         // 用于缓存 dept文件中的数据
         private Map<String, String> deptMap = new HashMap<String, String>();
         private String[] kv;
 
         // 此方法会在Map方法执行之前执行且执行一次
         @Override
         protected void setup(Context context) throws IOException, InterruptedException {
             BufferedReader in = null ;
             try {
                 // 从当前作业中获取要缓存的文件
                 Path[] paths = DistributedCache.getLocalCacheFiles(context.getConfiguration());
                 String deptIdName = null ;
                 for (Path path : paths) {
 
                     // 对部门文件字段进行拆分并缓存到deptMap中
                     if (path.toString().contains( "dept" )) {
                         in = new BufferedReader( new FileReader(path.toString()));
                         while ( null != (deptIdName = in.readLine())) {
                             
                             // 对部门文件字段进行拆分并缓存到deptMap中
                             // 其中Map中key为部门编号,value为所在部门名称
                             deptMap.put(deptIdName.split( "," )[ 0 ], deptIdName.split( "," )[ 1 ]);
                         }
                     }
                 }
             } catch (IOException e) {
                 e.printStackTrace();
             } finally {
                 try {
                     if (in != null ) {
                         in.close();
                     }
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
         }
 
     public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
 
             // 对员工文件字段进行拆分
             kv = value.toString().split( "," );
 
             // map join: 在map阶段过滤掉不需要的数据,输出key为部门名称和value为员工工资
             if (deptMap.containsKey(kv[ 7 ])) {
                 if ( null != kv[ 5 ] && ! "" .equals(kv[ 5 ].toString())) {
                     context.write( new Text(deptMap.get(kv[ 7 ].trim())), new Text(kv[ 5 ].trim()));
                 }
             }
         }
     }
 
     public static class Reduce extends Reducer<Text, Text, Text, Text> {
 
     public void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
 
             long sumSalary = 0 ;
             int deptNumber = 0 ;
 
             // 对同一部门的员工工资进行求和
             for (Text val : values) {
                 sumSalary += Long.parseLong(val.toString());
                 deptNumber++;
             }
 
             // 输出key为部门名称和value为该部门员工工资平均值
     context.write(key, new Text( "Dept Number:" + deptNumber + ", Ave Salary:" + sumSalary / deptNumber));
         }
     }
 
     @Override
     public int run(String[] args) throws Exception {
 
         // 实例化作业对象,设置作业名称、Mapper和Reduce类
         Job job = new Job(getConf(), "Q2DeptNumberAveSalary" );
         job.setJobName( "Q2DeptNumberAveSalary" );
         job.setJarByClass(Q2DeptNumberAveSalary. class );
         job.setMapperClass(MapClass. class );
         job.setReducerClass(Reduce. class );
 
         // 设置输入格式类
         job.setInputFormatClass(TextInputFormat. class );
 
         // 设置输出格式类
         job.setOutputFormatClass(TextOutputFormat. class );
         job.setOutputKeyClass(Text. class );
         job.setOutputValueClass(Text. class );
 
         // 第1个参数为缓存的部门数据路径、第2个参数为员工数据路径和第3个参数为输出路径
     String[] otherArgs = new GenericOptionsParser(job.getConfiguration(), args).getRemainingArgs();
         DistributedCache.addCacheFile( new Path(otherArgs[ 0 ]).toUri(), job.getConfiguration());
         FileInputFormat.addInputPath(job, new Path(otherArgs[ 1 ]));
         FileOutputFormat.setOutputPath(job, new Path(otherArgs[ 2 ]));
 
         job.waitForCompletion( true );
         return job.isSuccessful() ? 0 : 1 ;
     }
 
     /**
      * 主方法,执行入口
      * @param args 输入参数
      */
     public static void main(String[] args) throws Exception {
         int res = ToolRunner.run( new Configuration(), new Q2DeptNumberAveSalary(), args);
         System.exit(res);
     }
}


3.2.4 编译并打包代码
进入/app/hadoop-1.1.2/myclass/class6目录中新建Q2DeptNumberAveSalary.java程序代码(代码页可以使用/home/shiyanlou/install-pack/class6/Q2DeptNumberAveSalary.java文件)
cd /app/hadoop-1.1.2/myclass/class6
vi Q2DeptNumberAveSalary.java
编译代码
javac -classpath ../../hadoop-core-1.1.2.jar:../../lib/commons-cli-1.2.jar Q2DeptNumberAveSalary.java
把编译好的代码打成jar包,如果不打成jar形式运行会提示class无法找到的错误
jar cvf ./Q2DeptNumberAveSalary.jar ./Q2DeptNum*.class
mv *.jar ../..
rm Q2DeptNum*.class
3.2.5 运行并查看结果
运行Q2DeptNumberAveSalary时需要输入部门数据路径、员工数据路径和输出路径三个参数,需要注意的是hdfs的路径参数路径需要全路径,否则运行会报错:
l  部门数据路径:hdfs://hadoop:9000/class6/input/dept,部门数据将缓存在各运行任务的节点内容中,可以提供处理的效率
l  员工数据路径:hdfs://hadoop:9000/class6/input/emp
l  输出路径:hdfs://hadoop:9000/class6/out2

运行如下命令:
cd /app/hadoop-1.1.2
hadoop jar Q2DeptNumberAveSalary.jar Q2DeptNumberAveSalary hdfs://hadoop:9000/class6/input/dept hdfs://hadoop:9000/class6/input/emp hdfs://hadoop:9000/class6/out2
运行成功后,刷新CentOS HDFS中的输出路径/class6/out2目录
hadoop fs -ls /class6/out2
hadoop fs -cat /class6/out2/part-r-00000
打开part-r-00000文件,可以看到运行结果:
ACCOUNTINGDept Number:3,Ave Salary:2916
RESEARCHDept Number:3,Ave Salary:2258
SALES  Dept Number:6,Ave Salary:1566
3.3 测试例子
3:求每个部门最早进入公司的员工姓名3.3.1 问题分析
求每个部门最早进入公司员工姓名,需要得到各部门所有员工的进入公司日期,通过比较获取最早进入公司员工姓名。首先和问题1类似在Mapper的Setup阶段缓存部门数据,然后Mapper阶段抽取出key为部门名称(利用缓存部门数据把部门编号对应为部门名称),value为员工姓名和进入公司日期,接着在Shuffle阶段把传过来的数据处理为部门名称对应该部门所有员工+进入公司日期的列表,最后在Reduce中按照部门归组,遍历部门所有员工,找出最早进入公司的员工并输出。

3.3.2 处理流程图


3.3.3 编写代码

[Java] 纯文本查看 复制代码
?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
 
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.filecache.DistributedCache;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
 
public class Q3DeptEarliestEmp extends Configured implements Tool {
 
     public static class MapClass extends Mapper<LongWritable, Text, Text, Text> {
 
         // 用于缓存 dept文件中的数据
         private Map<String, String> deptMap = new HashMap<String, String>();
         private String[] kv;
 
         // 此方法会在Map方法执行之前执行且执行一次
         @Override
         protected void setup(Context context) throws IOException, InterruptedException {
             BufferedReader in = null ;
             try {
                 // 从当前作业中获取要缓存的文件
                 Path[] paths =     DistributedCache.getLocalCacheFiles(context.getConfiguration());
                 String deptIdName = null ;
                 for (Path path : paths) {
                     if (path.toString().contains( "dept" )) {
                         in = new BufferedReader( new FileReader(path.toString()));
                         while ( null != (deptIdName = in.readLine())) {
 
                             // 对部门文件字段进行拆分并缓存到deptMap中
                             // 其中Map中key为部门编号,value为所在部门名称
                             deptMap.put(deptIdName.split( "," )[ 0 ], deptIdName.split( "," )[ 1 ]);
                         }
                     }
                 }
             } catch (IOException e) {
                 e.printStackTrace();
             } finally {
                 try {
                     if (in != null ) {
                         in.close();
                     }
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
         }
 
         public void map(LongWritable key, Text value, Context context) throws IOException,         InterruptedException {
 
             // 对员工文件字段进行拆分
             kv = value.toString().split( "," );
 
             // map join: 在map阶段过滤掉不需要的数据
             // 输出key为部门名称和value为员工姓名+","+员工进入公司日期
             if (deptMap.containsKey(kv[ 7 ])) {
                 if ( null != kv[ 4 ] && ! "" .equals(kv[ 4 ].toString())) {
                     context.write( new Text(deptMap.get(kv[ 7 ].trim())), new Text(kv[ 1 ].trim()                     + "," + kv[ 4 ].trim()));
                 }
             }
         }
     }
 
     public static class Reduce extends Reducer<Text, Text, Text, Text> {
 
         public void reduce(Text key, Iterable<Text> values, Context context) throws IOException,         InterruptedException {
 
             // 员工姓名和进入公司日期
             String empName = null ;
             String empEnterDate = null ;
 
             // 设置日期转换格式和最早进入公司的员工、日期
             DateFormat df = new SimpleDateFormat( "dd-MM月-yy" );
 
             Date earliestDate = new Date();
             String earliestEmp = null ;
 
             // 遍历该部门下所有员工,得到最早进入公司的员工信息
             for (Text val : values) {
                 empName = val.toString().split( "," )[ 0 ];
                 empEnterDate = val.toString().split( "," )[ 1 ].toString().trim();
                 try {
                     System.out.println(df.parse(empEnterDate));
                     if (df.parse(empEnterDate).compareTo(earliestDate) < 0 ) {
                         earliestDate = df.parse(empEnterDate);
                         earliestEmp = empName;
                     }
                 } catch (ParseException e) {
                     e.printStackTrace();
                 }
             }
 
             // 输出key为部门名称和value为该部门最早进入公司员工
             context.write(key, new Text( "The earliest emp of dept:" + earliestEmp + ", Enter             date:" + new SimpleDateFormat( "yyyy-MM-dd" ).format(earliestDate)));
         }
     }
 
     @Override
     public int run(String[] args) throws Exception {
 
         // 实例化作业对象,设置作业名称
         Job job = new Job(getConf(), "Q3DeptEarliestEmp" );
         job.setJobName( "Q3DeptEarliestEmp" );
 
         // 设置Mapper和Reduce类
         job.setJarByClass(Q3DeptEarliestEmp. class );
         job.setMapperClass(MapClass. class );
         job.setReducerClass(Reduce. class );
 
         // 设置输入格式类
         job.setInputFormatClass(TextInputFormat. class );
 
         // 设置输出格式类
         job.setOutputFormatClass(TextOutputFormat. class );
         job.setOutputKeyClass(Text. class );
         job.setOutputValueClass(Text. class );
 
         // 第1个参数为缓存的部门数据路径、第2个参数为员工数据路径和第三个参数为输出路径
     String[] otherArgs = new GenericOptionsParser(job.getConfiguration(), args).getRemainingArgs();
     DistributedCache.addCacheFile( new Path(otherArgs[ 0 ]).toUri(), job.getConfiguration());
         FileInputFormat.addInputPath(job, new Path(otherArgs[ 1 ]));
         FileOutputFormat.setOutputPath(job, new Path(otherArgs[ 2 ]));
 
         job.waitForCompletion( true );
         return job.isSuccessful() ? 0 : 1 ;
     }
 
     /**
      * 主方法,执行入口
      * @param args 输入参数
      */
     public static void main(String[] args) throws Exception {
         int res = ToolRunner.run( new Configuration(), new Q3DeptEarliestEmp(), args);
         System.exit(res);
     }
}


3.3.4 编译并打包代码
进入/app/hadoop-1.1.2/myclass/class6目录中新建Q3DeptEarliestEmp.java程序代码(代码页可以使用/home/shiyanlou/install-pack/class6/Q3DeptEarliestEmp.java文件)
cd /app/hadoop-1.1.2/myclass/class6
vi Q3DeptEarliestEmp.java
编译代码
javac -classpath ../../hadoop-core-1.1.2.jar:../../lib/commons-cli-1.2.jar Q3DeptEarliestEmp.java
把编译好的代码打成jar包,如果不打成jar形式运行会提示class无法找到的错误
jar cvf ./Q3DeptEarliestEmp.jar ./Q3DeptEar*.class
mv *.jar ../..
rm Q3DeptEar*.class
3.3.5 运行并查看结果
运行Q3DeptEarliestEmp时需要输入部门数据路径、员工数据路径和输出路径三个参数,需要注意的是hdfs的路径参数路径需要全路径,否则运行会报错:
l  部门数据路径:hdfs://hadoop:9000/class6/input/dept,部门数据将缓存在各运行任务的节点内容中,可以提供处理的效率
l  员工数据路径:hdfs://hadoop:9000/class6/input/emp
l  输出路径:hdfs://hadoop:9000/class6/out3

运行如下命令:
cd /app/hadoop-1.1.2
hadoop jar Q3DeptEarliestEmp.jar Q3DeptEarliestEmp hdfs://hadoop:9000/class6/input/dept hdfs://hadoop:9000/class6/input/emp hdfs://hadoop:9000/class6/out3
运行成功后,刷新CentOS HDFS中的输出路径/class6/out3目录
hadoop fs -ls /class6/out3
hadoop fs -cat /class6/out3/part-r-00000
打开part-r-00000文件,可以看到运行结果:
ACCOUNTINGThe earliest emp of dept:CLARK, Enter date:1981-06-09
RESEARCHThe earliest emp of dept:SMITH, Enter date:1980-12-17
SALES  The earliest emp of dept:ALLEN, Enter date:1981-02-20
3.4 测试例子4:求各个城市的员工的总工资3.4.1 问题分析
求各个城市员工的总工资,需要得到各个城市所有员工的工资,通过对各个城市所有员工工资求和得到总工资。首先和测试例子1类似在Mapper的Setup阶段缓存部门对应所在城市数据,然后在Mapper阶段抽取出key为城市名称(利用缓存数据把部门编号对应为所在城市名称),value为员工工资,接着在Shuffle阶段把传过来的数据处理为城市名称对应该城市所有员工工资,最后在Reduce中按照城市归组,遍历城市所有员工,求出工资总数并输出。
3.4.2 处理流程图
3.4.3 编写代码
[Java] 纯文本查看 复制代码
?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
 
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.filecache.DistributedCache;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
 
public class Q4SumCitySalary extends Configured implements Tool {
 
     public static class MapClass extends Mapper<LongWritable, Text, Text, Text> {
 
         // 用于缓存 dept文件中的数据
         private Map<String, String> deptMap = new HashMap<String, String>();
         private String[] kv;
 
         // 此方法会在Map方法执行之前执行且执行一次
         @Override
         protected void setup(Context context) throws IOException, InterruptedException {
             BufferedReader in = null ;
             try {
                 // 从当前作业中获取要缓存的文件
                 Path[] paths = DistributedCache.getLocalCacheFiles(context.getConfiguration());
                 String deptIdName = null ;
                 for (Path path : paths) {
                     if (path.toString().contains( "dept" )) {
                         in = new BufferedReader( new FileReader(path.toString()));
                         while ( null != (deptIdName = in.readLine())) {
 
                             // 对部门文件字段进行拆分并缓存到deptMap中
                             // 其中Map中key为部门编号,value为所在城市名称
                             deptMap.put(deptIdName.split( "," )[ 0 ], deptIdName.split( "," )[ 2 ]);
                         }
                     }
                 }
             } catch (IOException e) {
                 e.printStackTrace();
             } finally {
                 try {
                     if (in != null ) {
                         in.close();
                     }
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
         }
 
         public void map(LongWritable key, Text value, Context context) throws IOException,         InterruptedException {
 
             // 对员工文件字段进行拆分
             kv = value.toString().split( "," );
 
             // map join: 在map阶段过滤掉不需要的数据,输出key为城市名称和value为员工工资
             if (deptMap.containsKey(kv[ 7 ])) {
                 if ( null != kv[ 5 ] && ! "" .equals(kv[ 5 ].toString())) {
                     context.write( new Text(deptMap.get(kv[ 7 ].trim())), new Text(kv[ 5 ].trim()));
                 }
             }
         }
     }
 
     public static class Reduce extends Reducer<Text, Text, Text, LongWritable> {
 
         public void reduce(Text key, Iterable<Text> values, Context context) throws IOException,         InterruptedException {
 
             // 对同一城市的员工工资进行求和
             long sumSalary = 0 ;
             for (Text val : values) {
                 sumSalary += Long.parseLong(val.toString());
             }
 
             // 输出key为城市名称和value为该城市工资总和
             context.write(key, new LongWritable(sumSalary));
         }
     }
 
     @Override
     public int run(String[] args) throws Exception {
 
         // 实例化作业对象,设置作业名称
         Job job = new Job(getConf(), "Q4SumCitySalary" );
         job.setJobName( "Q4SumCitySalary" );
 
         // 设置Mapper和Reduce类
         job.setJarByClass(Q4SumCitySalary. class );
         job.setMapperClass(MapClass. class );
         job.setReducerClass(Reduce. class );
 
         // 设置输入格式类
         job.setInputFormatClass(TextInputFormat. class );
 
         // 设置输出格式类
         job.setOutputFormatClass(TextOutputFormat. class );
         job.setOutputKeyClass(Text. class );
         job.setOutputValueClass(Text. class );
 
         // 第1个参数为缓存的部门数据路径、第2个参数为员工数据路径和第3个参数为输出路径
     String[] otherArgs = new GenericOptionsParser(job.getConfiguration(), args).getRemainingArgs();
     DistributedCache.addCacheFile( new Path(otherArgs[ 0 ]).toUri(), job.getConfiguration());
         FileInputFormat.addInputPath(job, new Path(otherArgs[ 1 ]));
         FileOutputFormat.setOutputPath(job, new Path(otherArgs[ 2 ]));
 
         job.waitForCompletion( true );
         return job.isSuccessful() ? 0 : 1 ;
     }
 
     /**
      * 主方法,执行入口
      * @param args 输入参数
      */
     public static void main(String[] args) throws Exception {
         int res = ToolRunner.run( new Configuration(), new Q4SumCitySalary(), args);
         System.exit(res);
     }
}



3.4.4 编译并打包代码
进入/app/hadoop-1.1.2/myclass/class6目录中新建Q4SumCitySalary.java程序代码(代码页可以使用/home/shiyanlou/install-pack/class6/Q4SumCitySalary.java文件)
cd /app/hadoop-1.1.2/myclass/class6
vi Q4SumCitySalary.java
编译代码
javac -classpath ../../hadoop-core-1.1.2.jar:../../lib/commons-cli-1.2.jar Q4SumCitySalary.java
把编译好的代码打成jar包,如果不打成jar形式运行会提示class无法找到的错误
jar cvf ./Q4SumCitySalary.jar ./Q4SumCity*.class
mv *.jar ../..
rm Q4SumCity*.class
3.4.5 运行并查看结果
运行Q4SumCitySalary时需要输入部门数据路径、员工数据路径和输出路径三个参数,需要注意的是hdfs的路径参数路径需要全路径,否则运行会报错:
l  部门数据路径:hdfs://hadoop:9000/class6/input/dept,部门数据将缓存在各运行任务的节点内容中,可以提供处理的效率
l  员工数据路径:hdfs://hadoop:9000/class6/input/emp
l  输出路径:hdfs://hadoop:9000/class6/out4

运行如下命令:
cd /app/hadoop-1.1.2
hadoop jar Q4SumCitySalary.jar Q4SumCitySalary hdfs://hadoop:9000/class6/input/dept hdfs://hadoop:9000/class6/input/emp hdfs://hadoop:9000/class6/out4
运行成功后,刷新CentOS HDFS中的输出路径/class6/out4目录
hadoop fs -ls /class6/out4
hadoop fs -cat /class6/out4/part-r-00000
打开part-r-00000文件,可以看到运行结果:
CHICAGO  9400
DALLAS     6775
NEW YORK     8750
3.5 测试例子5:列出工资比上司高的员工姓名及其工资3.5.1 问题分析
求工资比上司高的员工姓名及工资,需要得到上司工资及上司所有下属员工,通过比较他们工资高低得到比上司工资高的员工。在Mapper阶段输出经理数据和员工对应经理表数据,其中经理数据key为员工编号、value为"M,该员工工资",员工对应经理表数据key为经理编号、value为"E,该员工姓名,该员工工资";然后在Shuffle阶段把传过来的经理数据和员工对应经理表数据进行归组,如编号为7698员工,value中标志M为自己工资,value中标志E为其下属姓名及工资;最后在Reduce中遍历比较员工与经理工资高低,输出工资高于经理的员工。
3.5.2 处理流程图
3.5.3 编写代码
[Java] 纯文本查看 复制代码
?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
import java.io.IOException;
import java.util.HashMap;
 
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
 
public class Q5EarnMoreThanManager extends Configured implements Tool {
 
     public static class MapClass extends Mapper<LongWritable, Text, Text, Text> {
 
         public void map(LongWritable key, Text value, Context context) throws IOException,         InterruptedException {
 
             // 对员工文件字段进行拆分
             String[] kv = value.toString().split( "," );
 
             // 输出经理表数据,其中key为员工编号和value为M+该员工工资
             context.write( new Text(kv[ 0 ].toString()), new Text( "M," + kv[ 5 ]));
 
             // 输出员工对应经理表数据,其中key为经理编号和value为(E,该员工姓名,该员工工资)
             if ( null != kv[ 3 ] && ! "" .equals(kv[ 3 ].toString())) {
                 context.write( new Text(kv[ 3 ].toString()), new Text( "E," + kv[ 1 ] + "," + kv[ 5 ]));
             }
         }
     }
 
     public static class Reduce extends Reducer<Text, Text, Text, Text> {
 
         public void reduce(Text key, Iterable<Text> values, Context context) throws IOException,         InterruptedException {
 
             // 定义员工姓名、工资和存放部门员工Map
             String empName;
             long empSalary = 0 ;
             HashMap<String, Long> empMap = new HashMap<String, Long>();
             
             // 定义经理工资变量
             long mgrSalary = 0 ;
 
             for (Text val : values) {
                 if (val.toString().startsWith( "E" )) {
                     // 当是员工标示时,获取该员工对应的姓名和工资并放入Map中
                     empName = val.toString().split( "," )[ 1 ];
                     empSalary = Long.parseLong(val.toString().split( "," )[ 2 ]);
                     empMap.put(empName, empSalary);
                 } else {
                     // 当时经理标志时,获取该经理工资
                     mgrSalary = Long.parseLong(val.toString().split( "," )[ 1 ]);
                 }
             }
 
             // 遍历该经理下属,比较员工与经理工资高低,输出工资高于经理的员工
             for (java.util.Map.Entry<String, Long> entry : empMap.entrySet()) {
                 if (entry.getValue() > mgrSalary) {
                     context.write( new Text(entry.getKey()), new Text( "" + entry.getValue()));
                 }
             }
         }
     }
 
     @Override
     public int run(String[] args) throws Exception {
 
         // 实例化作业对象,设置作业名称
         Job job = new Job(getConf(), "Q5EarnMoreThanManager" );
         job.setJobName( "Q5EarnMoreThanManager" );
 
         // 设置Mapper和Reduce类
         job.setJarByClass(Q5EarnMoreThanManager. class );
         job.setMapperClass(MapClass. class );
         job.setReducerClass(Reduce. class );
 
         // 设置输入格式类
         job.setInputFormatClass(TextInputFormat. class );
 
         // 设置输出格式类
         job.setOutputFormatClass(TextOutputFormat. class );
         job.setOutputKeyClass(Text. class );
         job.setOutputValueClass(Text. class );
 
         // 第1个参数为员工数据路径和第2个参数为输出路径
String[] otherArgs = new GenericOptionsParser(job.getConfiguration(), args).getRemainingArgs();
         FileInputFormat.addInputPath(job, new Path(otherArgs[ 0 ]));
         FileOutputFormat.setOutputPath(job, new Path(otherArgs[ 1 ]));
 
         job.waitForCompletion( true );
         return job.isSuccessful() ? 0 : 1 ;
     }
 
     /**
      * 主方法,执行入口
      * @param args 输入参数
      */
     public static void main(String[] args) throws Exception {
         int res = ToolRunner.run( new Configuration(), new Q5EarnMoreThanManager(), args);
         System.exit(res);
     }
}



3.5.4 编译并打包代码
进入/app/hadoop-1.1.2/myclass/class6目录中新建Q5EarnMoreThanManager.java程序代码(代码页可以使用/home/shiyanlou/install-pack/class6/Q5EarnMoreThanManager.java文件)
cd /app/hadoop-1.1.2/myclass/class6
vi Q5EarnMoreThanManager.java
编译代码
javac -classpath ../../hadoop-core-1.1.2.jar:../../lib/commons-cli-1.2.jar Q5EarnMoreThanManager.java
把编译好的代码打成jar包,如果不打成jar形式运行会提示class无法找到的错误
jar cvf ./Q5EarnMoreThanManager.jar ./Q5EarnMore*.class
mv *.jar ../..
rm Q5EarnMore*.class
3.5.5 运行并查看结果
运行Q5EarnMoreThanManager运行的员工数据路径和输出路径两个参数,需要注意的是hdfs的路径参数路径需要全路径,否则运行会报错:
l  员工数据路径:hdfs://hadoop:9000/class6/input/emp
l  输出路径:hdfs://hadoop:9000/class6/out5

运行如下命令:
cd /app/hadoop-1.1.2
hadoop jar Q5EarnMoreThanManager.jar Q5EarnMoreThanManager hdfs://hadoop:9000/class6/input/emp hdfs://hadoop:9000/class6/out5
运行成功后,刷新CentOS HDFS中的输出路径/class6/out5目录
hadoop fs -ls /class6/out5
hadoop fs -cat /class6/out5/part-r-00000
打开part-r-00000文件,可以看到运行结果:
FORD  3000
3.6  测试例子6:列出工资比公司平均工资要高的员工姓名及其工资3.6.1 问题分析
求工资比公司平均工资要高的员工姓名及工资,需要得到公司的平均工资和所有员工工资,通过比较得出工资比平均工资高的员工姓名及工资。这个问题可以分两个作业进行解决,先求出公司的平均工资,然后与所有员工进行比较得到结果;也可以在一个作业进行解决,这里就得使用作业setNumReduceTasks方法,设置Reduce任务数为1,保证每次运行一个reduce任务,从而能先求出平均工资,然后进行比较得出结果。
在Mapper阶段输出两份所有员工数据,其中一份key为0、value为该员工工资,另外一份key为0、value为"该员工姓名 ,员工工资";然后在Shuffle阶段把传过来数据按照key进行归组,在该任务中有key值为0和1两组数据;最后在Reduce中对key值0的所有员工求工资总数和员工数,获得平均工资;对key值1,比较员工与平均工资的大小,输出比平均工资高的员工和对应的工资。
3.6.2 处理流程图
3.6.3 编写代码
[Java] 纯文本查看 复制代码
?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
 
public class Q6HigherThanAveSalary extends Configured implements Tool {
 
     public static class MapClass extends Mapper<LongWritable, Text, IntWritable, Text> {
 
         public void map(LongWritable key, Text value, Context context) throws IOException,         InterruptedException {
 
             // 对员工文件字段进行拆分
             String[] kv = value.toString().split( "," );
 
             // 获取所有员工数据,其中key为0和value为该员工工资
             context.write( new IntWritable( 0 ), new Text(kv[ 5 ]));
 
             // 获取所有员工数据,其中key为0和value为(该员工姓名 ,员工工资)
             context.write( new IntWritable( 1 ), new Text(kv[ 1 ] + "," + kv[ 5 ]));
         }
     }
 
     public static class Reduce extends Reducer<IntWritable, Text, Text, Text> {
 
         // 定义员工工资、员工数和平均工资
         private long allSalary = 0 ;
         private int allEmpCount = 0 ;
         private long aveSalary = 0 ;
         
         // 定义员工工资变量
         private long empSalary = 0 ;
 
         public void reduce(IntWritable key, Iterable<Text> values, Context context) throws         IOException, InterruptedException {
 
             for (Text val : values) {
                 if ( 0 == key.get()) {
                     // 获取所有员工工资和员工数
                     allSalary += Long.parseLong(val.toString());
                     allEmpCount++;
                     System.out.println( "allEmpCount = " + allEmpCount);
                 } else if ( 1 == key.get()) {
                     if (aveSalary == 0 ) {
                         aveSalary = allSalary / allEmpCount;
                         context.write( new Text( "Average Salary = " ), new Text( "" + aveSalary));
                         context.write( new Text( "Following employees have salarys higher than                         Average:" ), new Text( "" ));
                     }
 
                     // 获取员工的平均工资
                     System.out.println( "Employee salary = " + val.toString());
                     aveSalary = allSalary / allEmpCount;
                     
                     // 比较员工与平均工资的大小,输出比平均工资高的员工和对应的工资
                     empSalary = Long.parseLong(val.toString().split( "," )[ 1 ]);
                     if (empSalary > aveSalary) {
                         context.write( new Text(val.toString().split( "," )[ 0 ]), new Text( "" +                         empSalary));
                     }
                 }
             }
         }
     }
 
     @Override
     public int run(String[] args) throws Exception {
 
         // 实例化作业对象,设置作业名称
         Job job = new Job(getConf(), "Q6HigherThanAveSalary" );
         job.setJobName( "Q6HigherThanAveSalary" );
 
         // 设置Mapper和Reduce类
         job.setJarByClass(Q6HigherThanAveSalary. class );
         job.setMapperClass(MapClass. class );
         job.setReducerClass(Reduce. class );
 
         // 必须设置Reduce任务数为1 # -D mapred.reduce.tasks = 1
         // 这是该作业设置的核心,这样才能够保证各reduce是串行的
         job.setNumReduceTasks( 1 );
 
         // 设置输出格式类
         job.setMapOutputKeyClass(IntWritable. class );
         job.setMapOutputValueClass(Text. class );
 
         // 设置输出键和值类型
         job.setOutputFormatClass(TextOutputFormat. class );
         job.setOutputKeyClass(Text. class );
         job.setOutputValueClass(LongWritable. class );
 
         // 第1个参数为员工数据路径和第2个参数为输出路径
String[] otherArgs = new GenericOptionsParser(job.getConfiguration(), args).getRemainingArgs();
         FileInputFormat.addInputPath(job, new Path(otherArgs[ 0 ]));
         FileOutputFormat.setOutputPath(job, new Path(otherArgs[ 1 ]));
 
         job.waitForCompletion( true );
         return job.isSuccessful() ? 0 : 1 ;
     }
 
     /**
      * 主方法,执行入口
      * @param args 输入参数
      */
     public static void main(String[] args) throws Exception {
         int res = ToolRunner.run( new Configuration(), new Q6HigherThanAveSalary(), args);
         System.exit(res);
     }
}


3.6.4 编译并打包代码
进入/app/hadoop-1.1.2/myclass/class6目录中新建Q5EarnMoreThanManager.java程序代码(代码页可以使用/home/shiyanlou/install-pack/class6/Q6HigherThanAveSalary.java文件)
cd /app/hadoop-1.1.2/myclass/class6
vi Q6HigherThanAveSalary.java
编译代码
javac -classpath ../../hadoop-core-1.1.2.jar:../../lib/commons-cli-1.2.jar Q6HigherThanAveSalary.java
把编译好的代码打成jar包,如果不打成jar形式运行会提示class无法找到的错误
jar cvf ./Q6HigherThanAveSalary.jar ./Q6HigherThan*.class
mv *.jar ../..
rm Q6HigherThan*.class
3.6.5 运行并查看结果
运行Q6HigherThanAveSalary运行的员工数据路径和输出路径两个参数,需要注意的是hdfs的路径参数路径需要全路径,否则运行会报错:
l  员工数据路径:hdfs://hadoop:9000/class6/input/emp
l  输出路径:hdfs://hadoop:9000/class6/out6

运行如下命令:
cd /app/hadoop-1.1.2
hadoop jar Q6HigherThanAveSalary.jar Q6HigherThanAveSalary hdfs://hadoop:9000/class6/input/emp hdfs://hadoop:9000/class6/out6
运行成功后,刷新CentOS HDFS中的输出路径/class6/out6目录
hadoop fs -ls /class6/out6
hadoop fs -cat /class6/out6/part-r-00000
打开part-r-00000文件,可以看到运行结果:
Average Salary = 2077
Following employees have salarys higher than Average:   
FORD  3000
CLARK2450
KING   5000
JONES2975
BLAKE  2850
3.7 测试例子7:列出名字以J开头的员工姓名及其所属部门名称3.7.1 问题分析
求名字以J开头的员工姓名机器所属部门名称,只需判断员工姓名是否以J开头。首先和问题1类似在Mapper的Setup阶段缓存部门数据,然后在Mapper阶段判断员工姓名是否以J开头,如果是抽取出员工姓名和员工所在部门编号,利用缓存部门数据把部门编号对应为部门名称,转换后输出结果。
3.7.2 处理流程图
3.7.3 编写代码
[Java] 纯文本查看 复制代码
?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
 
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.filecache.DistributedCache;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
 
public class Q7NameDeptOfStartJ extends Configured implements Tool {
 
     public static class MapClass extends Mapper<LongWritable, Text, Text, Text> {
 
         // 用于缓存 dept文件中的数据
         private Map<String, String> deptMap = new HashMap<String, String>();
         private String[] kv;
 
         // 此方法会在Map方法执行之前执行且执行一次
         @Override
         protected void setup(Context context) throws IOException, InterruptedException {
             BufferedReader in = null ;
             try {
 
                 // 从当前作业中获取要缓存的文件
                 Path[] paths = DistributedCache.getLocalCacheFiles(context.getConfiguration());
                 String deptIdName = null ;
                 for (Path path : paths) {
 
                     // 对部门文件字段进行拆分并缓存到deptMap中
                     if (path.toString().contains( "dept" )) {
                         in = new BufferedReader( new FileReader(path.toString()));
                         while ( null != (deptIdName = in.readLine())) {
                             
                             // 对部门文件字段进行拆分并缓存到deptMap中
                             // 其中Map中key为部门编号,value为所在部门名称
                             deptMap.put(deptIdName.split( "," )[ 0 ], deptIdName.split( "," )[ 1 ]);
                         }
                     }
                 }
             } catch (IOException e) {
                 e.printStackTrace();
             } finally {
                 try {
                     if (in != null ) {
                         in.close();
                     }
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
         }
 
         public void map(LongWritable key, Text value, Context context) throws IOException,         InterruptedException {
 
             // 对员工文件字段进行拆分
             kv = value.toString().split( "," );
 
             // 输出员工姓名为J开头的员工信息,key为员工姓名和value为员工所在部门名称
             if (kv[ 1 ].toString().trim().startsWith( "J" )) {
                 context.write( new Text(kv[ 1 ].trim()), new Text(deptMap.get(kv[ 7 ].trim())));
             }
         }
     }
 
     @Override
     public int run(String[] args) throws Exception {
 
         // 实例化作业对象,设置作业名称
         Job job = new Job(getConf(), "Q7NameDeptOfStartJ" );
         job.setJobName( "Q7NameDeptOfStartJ" );
 
         // 设置Mapper和Reduce类
         job.setJarByClass(Q7NameDeptOfStartJ. class );
         job.setMapperClass(MapClass. class );
 
         // 设置输入格式类
         job.setInputFormatClass(TextInputFormat. class );
 
         // 设置输出格式类
         job.setOutputFormatClass(TextOutputFormat. class );
         job.setOutputKeyClass(Text. class );
         job.setOutputValueClass(Text. class );
 
         // 第1个参数为缓存的部门数据路径、第2个参数为员工数据路径和第3个参数为输出路径
String[] otherArgs = new GenericOptionsParser(job.getConfiguration(), args).getRemainingArgs();
     DistributedCache.addCacheFile( new Path(otherArgs[ 0 ]).toUri(), job.getConfiguration());
         FileInputFormat.addInputPath(job, new Path(otherArgs[ 1 ]));
         FileOutputFormat.setOutputPath(job, new Path(otherArgs[ 2 ]));
 
         job.waitForCompletion( true );
         return job.isSuccessful() ? 0 : 1 ;
     }
 
     /**
      * 主方法,执行入口
      * @param args 输入参数
      */
     public static void main(String[] args) throws Exception {
         int res = ToolRunner.run( new Configuration(), new Q7NameDeptOfStartJ(), args);
         System.exit(res);
     }
}


3.7.4 编译并打包代码
进入/app/hadoop-1.1.2/myclass/class6目录中新建Q7NameDeptOfStartJ.java程序代码(代码页可以使用/home/shiyanlou/install-pack/class6/Q7NameDeptOfStartJ.java文件)
cd /app/hadoop-1.1.2/myclass/class6
vi Q7NameDeptOfStartJ.java
编译代码
javac -classpath ../../hadoop-core-1.1.2.jar:../../lib/commons-cli-1.2.jar Q7NameDeptOfStartJ.java
把编译好的代码打成jar包,如果不打成jar形式运行会提示class无法找到的错误
jar cvf ./Q7NameDeptOfStartJ.jar ./Q7NameDept*.class
mv *.jar ../..
rm Q7NameDept*.class
3.7.5 运行并查看结果
运行Q7NameDeptOfStartJ时需要输入部门数据路径、员工数据路径和输出路径三个参数,需要注意的是hdfs的路径参数路径需要全路径,否则运行会报错:
l  部门数据路径:hdfs://hadoop:9000/class6/input/dept,部门数据将缓存在各运行任务的节点内容中,可以提供处理的效率
l  员工数据路径:hdfs://hadoop:9000/class6/input/emp
l  输出路径:hdfs://hadoop:9000/class6/out7
运行如下命令:
cd /app/hadoop-1.1.2
hadoop jar Q7NameDeptOfStartJ.jar Q7NameDeptOfStartJ hdfs://hadoop:9000/class6/input/dept hdfs://hadoop:9000/class6/input/emp hdfs://hadoop:9000/class6/out7
运行成功后,刷新CentOS HDFS中的输出路径/class6/out7目录
hadoop fs -ls /class6/out7
hadoop fs -cat /class6/out7/part-r-00000
打开part-r-00000文件,可以看到运行结果:
JAMESSALES
JONESRESEARCH
3.8 测试例子8:列出工资最高的头三名员工姓名及其工资3.8.1 问题分析
求工资最高的头三名员工姓名及工资,可以通过冒泡法得到。在Mapper阶段输出经理数据和员工对应经理表数据,其中经理数据key为0值、value为"员工姓名,员工工资";最后在Reduce中通过冒泡法遍历所有员工,比较员工工资多少,求出前三名。
3.8.2 处理流程图
3.8.3 编写代码
[Java] 纯文本查看 复制代码
?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import java.io.IOException;
 
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
 
public class Q8SalaryTop3Salary extends Configured implements Tool {
 
     public static class MapClass extends Mapper<LongWritable, Text, IntWritable, Text> {
 
         public void map(LongWritable key, Text value, Context context) throws IOException,         InterruptedException {
 
             // 对员工文件字段进行拆分
             String[] kv = value.toString().split( "," );
 
             // 输出key为0和value为员工姓名+","+员工工资
             context.write( new IntWritable( 0 ), new Text(kv[ 1 ].trim() + "," + kv[ 5 ].trim()));
         }
     }
 
     public static class Reduce extends Reducer<IntWritable, Text, Text, Text> {
 
         public void reduce(IntWritable key, Iterable<Text> values, Context context) throws         IOException, InterruptedException {
 
             // 定义工资前三员工姓名
             String empName;
             String firstEmpName = "" ;
             String secondEmpName = "" ;
             String thirdEmpName = "" ;
             
             // 定义工资前三工资
             long empSalary = 0 ;
             long firstEmpSalary = 0 ;
             long secondEmpSalary = 0 ;
             long thirdEmpSalary = 0 ;
 
             // 通过冒泡法遍历所有员工,比较员工工资多少,求出前三名
             for (Text val : values) {
                 empName = val.toString().split( "," )[ 0 ];
                 empSalary = Long.parseLong(val.toString().split( "," )[ 1 ]);
                 
                 if (empSalary > firstEmpSalary) {
                     thirdEmpName = secondEmpName;
                     thirdEmpSalary = secondEmpSalary;
                     secondEmpName = firstEmpName;
                     secondEmpSalary = firstEmpSalary;
                     firstEmpName = empName;
                     firstEmpSalary = empSalary;
                 } else if (empSalary > secondEmpSalary) {
                     thirdEmpName = secondEmpName;
                     thirdEmpSalary = secondEmpSalary;
                     secondEmpName = empName;
                     secondEmpSalary = empSalary;
                 } else if (empSalary > thirdEmpSalary) {
                     thirdEmpName = empName;
                     thirdEmpSalary = empSalary;
                 }
             }
             
             // 输出工资前三名信息
             context.write( new Text( "First employee name:" + firstEmpName), new Text( "Salary:"             + firstEmpSalary));
             context.write( new Text( "Second employee name:" + secondEmpName), new                     Text( "Salary:" + secondEmpSalary));
             context.write( new Text( "Third employee name:" + thirdEmpName), new Text( "Salary:"             + thirdEmpSalary));
         }
     }
 
     @Override
     public int run(String[] args) throws Exception {
 
         // 实例化作业对象,设置作业名称
         Job job = new Job(getConf(), "Q8SalaryTop3Salary" );
         job.setJobName( "Q8SalaryTop3Salary" );
 
         // 设置Mapper和Reduce类
         job.setJarByClass(Q8SalaryTop3Salary. class );
         job.setMapperClass(MapClass. class );
         job.setReducerClass(Reduce. class );
         job.setMapOutputKeyClass(IntWritable. class );
         job.setMapOutputValueClass(Text. class );
 
         // 设置输入格式类
         job.setInputFormatClass(TextInputFormat. class );
 
         // 设置输出格式类
         job.setOutputKeyClass(Text. class );
         job.setOutputFormatClass(TextOutputFormat. class );
         job.setOutputValueClass(Text. class );
 
         // 第1个参数为员工数据路径和第2个参数为输出路径
         String[] otherArgs = new GenericOptionsParser(job.getConfiguration(),                     args).getRemainingArgs();
         FileInputFormat.addInputPath(job, new Path(otherArgs[ 0 ]));
         FileOutputFormat.setOutputPath(job, new Path(otherArgs[ 1 ]));
 
         job.waitForCompletion( true );
         return job.isSuccessful() ? 0 : 1 ;
     }
 
     /**
      * 主方法,执行入口
      * @param args 输入参数
      */
     public static void main(String[] args) throws Exception {
         int res = ToolRunner.run( new Configuration(), new Q8SalaryTop3Salary(), args);
         System.exit(res);
     }
}


3.8.4 编译并打包代码
进入/app/hadoop-1.1.2/myclass/class6目录中新建Q8SalaryTop3Salary.java程序代码(代码页可以使用/home/shiyanlou/install-pack/class6/Q8SalaryTop3Salary.java文件)
cd /app/hadoop-1.1.2/myclass/class6
vi Q8SalaryTop3Salary.java
编译代码
javac -classpath ../../hadoop-core-1.1.2.jar:../../lib/commons-cli-1.2.jar Q8SalaryTop3Salary.java
把编译好的代码打成jar包,如果不打成jar形式运行会提示class无法找到的错误
jar cvf ./Q8SalaryTop3Salary.jar ./Q8SalaryTop3*.class
mv *.jar ../..
rm Q8SalaryTop3*.class
3.8.5 运行并查看结果
运行Q8SalaryTop3Salary运行的员工数据路径和输出路径两个参数,需要注意的是hdfs的路径参数路径需要全路径,否则运行会报错:
l  员工数据路径:hdfs://hadoop:9000/class6/input/emp
l  输出路径:hdfs://hadoop:9000/class6/out8
运行如下命令:
cd /app/hadoop-1.1.2
hadoop jar Q8SalaryTop3Salary.jar Q8SalaryTop3Salary hdfs://hadoop:9000/class6/input/emp hdfs://hadoop:9000/class6/out8
运行成功后,刷新CentOS HDFS中的输出路径/class6/out8目录
hadoop fs -ls /class6/out8
hadoop fs -cat /class6/out8/part-r-00000
打开part-r-00000文件,可以看到运行结果:
First employee name:KING    Salary:5000
Second employee name:FORD    Salary:3000
Third employee name:JONESSalary:2975
3.9  测试例子9:将全体员工按照总收入(工资+提成)从高到低排列3.9.1 问题分析
求全体员工总收入降序排列,获得所有员工总收入并降序排列即可。在Mapper阶段输出所有员工总工资数据,其中key为员工总工资、value为员工姓名,在Mapper阶段的最后会先调用job.setPartitionerClass对数据进行分区,每个分区映射到一个reducer,每个分区内又调用job.setSortComparatorClass设置的key比较函数类排序。由于在本作业中Map的key只有0值,故能实现对所有数据进行排序。
3.9.2 处理流程图
3.9.3    编写代码
[Java] 纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import java.io.IOException;
 
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
 
public class Q9EmpSalarySort extends Configured implements Tool {
 
     public static class MapClass extends Mapper<LongWritable, Text, IntWritable, Text> {
 
         public void map(LongWritable key, Text value, Context context) throws IOException,         InterruptedException {
 
             // 对员工文件字段进行拆分
             String[] kv = value.toString().split( "," );
 
             // 输出key为员工所有工资和value为员工姓名
             int empAllSalary = "" .equals(kv[ 6 ]) ? Integer.parseInt(kv[ 5 ]) :                             Integer.parseInt(kv[ 5 ]) + Integer.parseInt(kv[ 6 ]);
             context.write( new IntWritable(empAllSalary), new Text(kv[ 1 ]));
         }
     }
 
     /**
      * 递减排序算法
      */
     public static class DecreaseComparator extends IntWritable.Comparator {
         public int compare(WritableComparable a, WritableComparable b) {
             return - super .compare(a, b);
         }
 
         public int compare( byte [] b1, int s1, int l1, byte [] b2, int s2, int l2) {
             return - super .compare(b1, s1, l1, b2, s2, l2);
         }
     }
 
     @Override
     public int run(String[] args) throws Exception {
 
         // 实例化作业对象,设置作业名称
         Job job = new Job(getConf(), "Q9EmpSalarySort" );
         job.setJobName( "Q9EmpSalarySort" );
 
         // 设置Mapper和Reduce类
         job.setJarByClass(Q9EmpSalarySort. class );
         job.setMapperClass(MapClass. class );
 
         // 设置输出格式类
         job.setMapOutputKeyClass(IntWritable. class );
         job.setMapOutputValueClass(Text. class );
         job.setSortComparatorClass(DecreaseComparator. class );
 
         // 第1个参数为员工数据路径和第2个参数为输出路径
         String[] otherArgs = new GenericOptionsParser(job.getConfiguration(),                     args).getRemainingArgs();
         FileInputFormat.addInputPath(job, new Path(otherArgs[ 0 ]));
         FileOutputFormat.setOutputPath(job, new Path(otherArgs[ 1 ]));
 
         job.waitForCompletion( true );
         return job.isSuccessful() ? 0 : 1 ;
     }
 
     /**
      * 主方法,执行入口
      * @param args 输入参数
      */
     public static void main(String[] args) throws Exception {
         int res = ToolRunner.run( new Configuration(), new Q9EmpSalarySort(), args);
         System.exit(res);
     }
}



3.9.4 编译并打包代码
进入/app/hadoop-1.1.2/myclass/class6目录中新建Q9EmpSalarySort.java程序代码(代码页可以使用/home/shiyanlou/install-pack/class6/Q9EmpSalarySort.java文件)
cd /app/hadoop-1.1.2/myclass/class6
vi Q9EmpSalarySort.java
编译代码
javac -classpath ../../hadoop-core-1.1.2.jar:../../lib/commons-cli-1.2.jar Q9EmpSalarySort.java
把编译好的代码打成jar包,如果不打成jar形式运行会提示class无法找到的错误
jar cvf ./Q9EmpSalarySort.jar ./Q9EmpSalary*.class
mv *.jar ../..
rm Q9EmpSalary*.class
3.9.5 运行并查看结果
运行Q9EmpSalarySort运行的员工数据路径和输出路径两个参数,需要注意的是hdfs的路径参数路径需要全路径,否则运行会报错:
l  员工数据路径:hdfs://hadoop:9000/class6/input/emp
l  输出路径:hdfs://hadoop:9000/class6/out9
运行如下命令:
cd /app/hadoop-1.1.2
hadoop jar Q9EmpSalarySort.jar Q9EmpSalarySort hdfs://hadoop:9000/class6/input/emp hdfs://hadoop:9000/class6/out9
运行成功后,刷新CentOS HDFS中的输出路径/class6/out9目录
hadoop fs -ls /class6/out9
hadoop fs -cat /class6/out9/part-r-00000
打开part-r-00000文件,可以看到运行结果:
5000    KING
3000    FORD
2975    JONES
2850    BLAKE
......

下一篇:MapReduce应用案例2
http://www.aboutyun.com/thread-14932-1-1.html
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值