自学内容网 自学内容网

【HBase原理及应用实训课程】第五章 HBase与MapReduce的集成

5.1 HBase数据导入工具

任务介绍

一、任务背景
通常,在我们将数据导入 HBase 时,若是小批量的数据,使用 HBase 提供的 API 就可以满足需求。但是要灌入大量数据的时候,使用 API 的方式导入,会占用大量 Regionserver 的资源,影响该 Regionserver 上其它表的查询。

为了解决这种问题,HBase 官方提供了两种基于 MapReduce 的大量数据导入的方法:importTSV 和 BulkLoad。

二、任务要求
本实训主要有两个任务:

一是使用默认的 importTSV 方法将数据在 Reduce 端直接导入到 HBase 表。
二是使用 BulkLoad 方式进行导入,即通过自定义 MapReduce 程序生成 HFile 文件,再通过命令行方式将 HFile 文件导入 HBase 表。
三、原始数据
下面是电影数据(完整数据集: /root/info/train/chapter5/5.1/movie.csv )。

tt10370822,唐人街探案3,Detective Chinatown 3,2021-02-12,5.3,2.5,8.8
tt13575838,盛夏未来,Upcoming Summer,2021-07-30,7.1,3.5,9.1
tt13696296,中国医生,Chinese Doctors,2021-07-09,6.9,3.5,9.3
... ...

字段含义:

  • tt10370822:IMDb,IMDb编码
  • 唐人街探案3:ch_name,中文名
  • Detective Chinatown 3:eng_name,英文名
  • 2021-02-12:release_time,上映时间
  • 5.3:db_score,豆瓣评分
  • 2.5:db_star,豆瓣星级
  • 8.8:my_score,猫眼评分

知识点介绍

一、importTSV 工具

  1. 概述
    importTSV 是 HBase 提供的一个命令行工具,将存储在 HDFS 上的数据文件,通过指定的分隔符解析后,导入到 HBase 表中。

这样的方式导入数据与正常写入流程不同的是,跳过了 WAL、Memcache 与 Flush 的过程,直接将 HFile 文件移动到 HBase 表空间目录下即可,不影响 HRegionServer 的性能。

importTSV 包含两种方式将数据导入到 HBase 表中:

第一种:使用 TableOutputformat 在 Reduce 端插入数据,但是这种方式导入大批量数据的时候有可能会存在问题,尤其是列比较多的宽表导入的时候,会出现 RegionTooBusyException,导致数据丢失,因此建议在数据量不是特别大并且列不是特别多的情况下使用。
第二种(推荐):先使用 MarpReduce 程序生成 HFile 文件,再执行 LoadIncrementalHFiles 命令,将文件移动到 HBase 表中对应的存储目录下。
2. importTSV 用法
默认情况下,是第一种导入方式,即将数据在 Reduce 端直接导入到 HBase 表中。命令格式如下:

hadoop jar $HBASE_HOME/lib/hbase-server-version.jar importtsv -Dimporttsv.columns=a,b,c
默认直接导入所需的基本参数:

-Dimporttsv.columns=a,b,c:指定导入到HBase表中的列。
:HBase表名。
:数据文件在HDFS上的目录地址(直接写目录地址即可)。
如果数据文件不是使用默认的分隔符“\t”进行分割,需要指定文件分隔符:

‘-Dimporttsv.separator=|’:指定分隔符为管道符。importtsv 默认分隔符为“\t”。
如果使用第二种方法,要生成 HFile 文件,添加如下参数:

‘-Dimporttsv.bulk.output=/path/for/output’:指定生成的 HFile 文件在 HDFS 的存储目录地址。
二、BuckLoad 工具

  1. 概述
    下图很好的解释了 BuckLoad 的导入原理,通过 MapReduce 程序在 HDFS 直接生成 HFile 文件,将 HFile 文件移动到 HBase 中对应表的 HDFS 目录中。
    image.png

其实,importTSV 生成 HFile,再导入 HBase 的方式也是 BuckLoad。但与 BuckLoad 方式不同的是,importTSV 的导入方式,是在命令行进行导入的,不需要我们编写程序,仅需要确定数据文件的格式与 HBase 表中对应的列维度即可,如果我们没法确认,则需要对 importTSV 进行自定义改造。

importTSV 这种方式,是比较友好的,数据格式定义好了,列簇规划好了,直接导入就行。但有些场景,还是需要我们自定义导入程序,这时使用 importTSV 就不太方便了,自定义改造如果不熟,还是比较麻烦的。

这里,我们就可以选择使用 BuckLoad 方式进行导入,BuckLoad 的优势是:通过自定义程序生成 HFile,再进行导入即可,比较灵活。

  1. BuckLoad 程序编写步骤
    BuckLoad 程序编写步骤如下:

  2. 编写 Mapper 程序,注意无论是 Map 还是 Reduce,其输出类型必须是:< ImmutableBytesWritable, Put> 或者 < ImmutableBytesWritable, Keyvalue>。

  3. 编写 map() 方法,包含处理 HDFS 数据的逻辑。

  4. 将处理后生成的 HFile 文件写入 HDFS 指定目录。

  5. 配置 MapReduce 任务的输入、输出格式,输出类型,输入输出数据存储路径等。

  6. 使用 BuckLoad 方式将 HFile 文件导入 HBase 表,有两种方法:

(1)代码:创建 LoadIncrementalHFiles 对象,调用 doBulkLoad() 方法,加载 MapReduce 程序生成的 HFile 到表中即可。

doBulkLoad() 方法有两种,如下图所示。HTable 那种,已经过时了,所以推荐使用第一种,毕竟现在 HBase 都已经使用新的 API 了。
image.png
具体使用方法如下所示:

LoadIncrementalHFiles loader = new LoadIncrementalHFiles(conf);
loader.doBulkLoad(new Path(OUTPUT_PATH),admin,table,connection.getRegionLocator(TableName.valueOf(tableName)));

(2)命令行:在命令行中使用如下命令。

hadoop jar $HBASE_HOME/lib/hbase-server-version.jar completebulkload <生成的HFile路径> <表名称>
## 或者
hbase completebulkload <生成的HFile路径> <表名称>

如果在导入中发生异常:java.lang.NoClassDefFoundError: org/apache/hadoop/hbase/filter/Filter,原因是 Hadoop 的运行环境中缺少 HBase 支持的 jar 包。

解决办法:在命令前添加如下命令。

export HADOOP_CLASSPATH=$HBASE_HOME/lib/*:classpath# 向Hadoop中添加对HBase的依赖包

任务1:importTSV导入数据

要求:将以下所有语句存储到 /headless/Desktop/hbase.txt 文件中。

要求1:导入 CSV 数据
第一步:创建 HBase 表

  1. 进入 HBase Shell 交互模式,在该模式下,创建名为 “movie” 的命名空间,创建成功后,列出所有的命令空间进行验证。

  2. 在 “movie” 命名空间中创建名为 “movie_csv” 的 HBase 表,该表有两个列簇,分别为:“movie_info” 和 “grade”,版本号分别为2,3。

第二步:将数据上传到 HDFS

  1. 使用 HDFS Shell 的 mkdir 命令在 HDFS 的根目录(/)下级联创建 /import/movie 目录。

  2. 使用 HDFS Shell 的 put 命令将 /root/info/train/chapter5/5.1/movie.csv 文件上传到 HDFS 的 /import/movie 目录下。

  3. 使用 HDFS Shell 的 cat 命令查看/import/movie/movie.csv 文件内容,查看字段分隔符。

第三步:importTSV 导入数据

  1. 在 Linux 命令行执行如下指令查看 HBase 官方自带工具类使用说明:

export HADOOP_CLASSPATH=$HBASE_HOME/lib/*:classpath # 向Hadoop中添加对HBase的依赖包
hadoop jar $HBASE_HOME/lib/hbase-server-1.6.0.jar
其中,importtsv 就是将文本文件(比如:CSV、TSV等格式)数据导入 HBase 表的工具类。

  1. 在 Linux 命令行执行如下指令查看 importtsv 工具类的使用说明:

hadoop jar $HBASE_HOME/lib/hbase-server-1.6.0.jar importtsv
命令执行结果如下图所示:
在这里插入图片描述

参数说明:

-Dimporttsv.columns=a,b,c:指定导入到HBase表中的列。
:HBase表名。
:数据文件在HDFS上的目录地址(直接写目录地址即可,例: /import/movie/movie.csv)。
‘-Dimporttsv.separator=|’:指定分隔符为管道符。importtsv 默认分隔符为“\t”。
3. 执行 importtsv 命令将 HDFS 中 /import/movie/movie.csv CSV 格式数据导入 HBase 的 “movie_csv”表。其中,第一列为 HBASE_ROW_KEY(行键),“ch_name”、“eng_name” 和 “release_time” 属于 “movie_info” 列簇,“db_score”、“db_star”和 “my_score” 属于 “grade” 列簇。

  1. 在 Linux 命令行执行以下命令扫描 movie_csv 表中的所有数据,并将扫描结果写入 /root/software/hbase-1.6.0/hbase.log 日志文件:

echo “scan ‘movie:movie_csv’” | hbase shell -n > /root/software/hbase-1.6.0/hbase.log

任务2:BulkLoad导入数据

一、创建 HBase 表
进入 HBase Shell 交互模式,在 “movie” 命名空间中创建名为 “movie_bl” 的 HBase 表,该表有两个列簇,分别为:“movie_info” 和 “grade”,版本号分别为2,3。

二、创建 Java 项目
第一步:新建 Java 项目
打开 Eclipse,将 Eclipse 工作目录设置为 /root/eclipse-workspace,创建名为 “movie” 的 Java Project,在 “movie” 项目下创建名为 com.mr.movie 的 package 包。在 com.mr.movie 包下创建一个名为 BulkLoadMR 的 class 文件。

第二步:导入依赖 jar 包
右键“movie”项目—>选择“Build Path”—>“Configure Build Path”。之后弹出“Properties for telephone”对话框,选择“Libraries”界面,之后单击“Add External JARs…”。弹出“JAR Selection”对话框,选择“+ Other Locations”—>“Computer”,进入“/root/software/hbase-1.6.0/lib”目录,全选 lib 目录下的所有包(记得排除 ruby 目录),然后单击“open”,最后单击“Apply and Close”应用并关闭窗口。

三、MapReduce 程序编写
第一步:生成 HFile 文件
Map 端程序编写要求

  1. 在 BulkLoadMR 文件中,自定义名为 BulkLoadMapper 的类,该类需要继承(extends)父类 Mapper,Mapper 的输入和输出数据都是 KV 对的形式:

输入 key 为一行文本的起始偏移量,输入 value 为一行文本的内容;
输出 key 为输出到 HBase 中的 RowKey,输出 value 为封装了 HBase 表数据的 Put 对象。
2. BulkLoadMapper 中的用户自定义业务逻辑写在 map() 方法中,具体实现如下:

(1)获取每行文本,使用 toString() 方法将文本转化为字符串;

(2)根据分隔符逗号切分文本;

(3)提取电影数据中的各个字段;;

(4)创建 Put 对象,使用 Put 对象封装需要添加的信息,一个 Put 代表一行,构造函数传入的是 RowKey(即“IMDb编码”);

(5)往Put对象上添加信息(列簇,列,值);

(6)将数据写入 HBase 表,需要指定 RowKey 和 Put 对象。

Driver 端程序编写要求

  1. 创建配置文件对象,要求程序使用本地运行模式,处理的数据在 HDFS 文件系统。另外,手动设置 ZooKeeper 队列名称和端口。

  2. 使用 Job.getInstance() 创建一个 Job 对象。

  3. 使用 Job 对象的setJarByClass() 方法打包作业。

  4. 使用 Job 对象的setInputFormatClass() 方法指定输入文件格式为 TextOutputFormat(默认),并使用 TextOutputFormat 的 addInputPath() 方法 指定数据输入路径为 /import/movie/movie.csv。

  5. 使用 Job 对象的setMapperClass() 方法指定我们自定义的 Mapper 类,使用 Job 对象的setNumReduceTasks() 方法设置 Reduce 任务数为0。

  6. 使用 Job 对象的setMapOutputKeyClass() 方法和setMapOutputValueClass() 方法指定 MapTask 的输出 key-value 类型。

  7. 使用 Job 对象的setOutputFormatClass() 方法指定输出文件格式为 HFileOutputFormat2(HFile格式),并对 HFileOutputFormat2 格式进行配置:

使用连接工厂根据配置器创建与 HBase 之间的连接对象;
创建 Table 对象,与 HBase 表进行通信;
创建 RegionLocator 对象,用来定位 HBase 表的区域位置信息;
使用 HFileOutputFormat2 的 configureIncrementalLoad() 方法设置 HFile 加载到 HBase 的 movie:movie_csv 表;
使用 HFileOutputFormat2 的 setOutputPath() 方法设置 HFile 输出路径为 /bulkload/movie。
8. 使用 Job 对象的waitForCompletion() 方法提交并运行作业,要求将运行进度等信息及时输出给用户。

BulkLoadMR.java 参考代码如下:

package com.mr.movie;

import java.io.IOException;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.RegionLocator;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.mapreduce.HFileOutputFormat2;
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.TextInputFormat;

/**

  • 生成HFile文件

*/
public class BulkLoadMR {
private static final String TABLE_NAME = “movie:movie_bl”; // HBase表名
private static final String CF_MOVIE = “movie_info”; // 列簇1
private static final String CF_GRADE = “grade”; // 列簇2

public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// (1)创建HBase配置对象(继承自Hadoop的Configuration,这里使用父类的引用指向子类的对象的设计)
Configuration config = ???;
// HDFS集群中NameNode的URI,获取DistributedFileSystem实例
config.???("fs.defaultFS", "???");
// 通过config.set()方法进行手动设置。设置ZooKeeper队列名称和端口
config.???("hbase.zookeeper.quorum", "???");
config.???("hbase.zookeeper.property.clientPort", "???");
// (2)新建一个Job任务
Job job = ???;
// (3)将Job所用到的那些类(class)文件,打成jar包
job.???(BulkLoadMR.class);
// (4)指定输入文件格式,默认为TextOutputFormat,并设置数据输入路径
job.???;
???
// (5)指定Mapper类和Reduce的个数
job.???(BulkLoadMapper.class);
job.???;
// (6)指定MapTask的输出key-value类型
job.???;
job.???;
// (7)指定输出文件格式,默认为TextOutputFormat,此处设置成HFile格式
job.???(HFileOutputFormat2.class);
// 使用连接工厂根据配置器创建与HBase之间的连接对象
Connection connect = ???;
// 创建Table对象,与HBase表进行通信
Table table = connect.???(TableName.valueOf(TABLE_NAME));
// 创建RegionLocator对象,用来定位HBase表的区域位置信息
RegionLocator rLocator = connect.???(TableName.valueOf(TABLE_NAME));
// 配置HFileOutputFormat2,设置HFile加载到指定HBase表和HFile输出路径
HFileOutputFormat2.???;
HFileOutputFormat2.???;
// (8)最后给YARN来运行,等着集群运行完成返回反馈信息,客户端退出
boolean waitForCompletion = job.waitForCompletion(true);
System.exit(waitForCompletion ? 0 : 1);
}

/**
 * KEYIN:读到的key是一行文本的起始偏移量 
 * VALUEIN:读到的value是一行文本的内容
 * KEYOUT:ImmutableBytesWritable,不可变类型,行键“IMDb编码” 
 * VALUEOUT:Put类型
 * 
 */
public static class BulkLoadMapper ??? {

@Override
protected void map(LongWritable key, Text value,
Mapper<LongWritable, Text, ImmutableBytesWritable, Put>.Context context)
throws IOException, InterruptedException {
// (1)获取每行文本,将文本转化为字符串
String line = value.???;
// (2)根据分隔符逗号切分文本
String[] movie = line.???;
// (3)提取电影数据中的各个字段
byte[] IMDb = ???;// IMDb编码
byte[] ch_name = ???;// 电影中文名
byte[] eng_name = ???;// 电影英文名
byte[] release_time = ???;// 上映时间
byte[] db_score = ???;// 豆瓣评分
byte[] db_star = ???;// 豆瓣星级
byte[] my_score = ???;// 猫眼评分
// (4)创建Put对象。使用Put对象封装需要添加的信息,一个Put代表一行,构造函数传入的是RowKey(即“IMDb编码”)
Put put = ???;
// (5)往Put对象上添加信息 (列簇,列,值)
put.???(CF_MOVIE.getBytes(), "ch_name".getBytes(), ch_name);// 电影中文名
???// 电影英文名
???// 上映时间
???// 豆瓣评分
???// 豆瓣星级
???// 猫眼评分
// (6)将数据写入HBase表,需要指定RowKey和Put对象
ImmutableBytesWritable rowkey = ???;
context.???(rowkey, put);
}
}

}
第二步:使用 BuckLoad 方式向 HBase 表导入数据

  1. 在 Linux 命令行执行如下指令查看 completebulkload 工具类的使用说明:

export HADOOP_CLASSPATH=$HBASE_HOME/lib/*:classpath # 向Hadoop中添加对HBase的依赖包
hadoop jar $HBASE_HOME/lib/hbase-server-1.6.0.jar completebulkload
命令执行结果如下图所示:
image.png
参数说明:

</path/to/hfileoutputformat-output>:HFile文件所在路径。
:HBase表名。
2. 执行 completebulkload 命令将生成的 HFile 文件导入 HBase 的 movie:movie_bl 表。

  1. 在 Linux 命令行执行以下命令扫描 movie_bl 表中的所有数据,并将扫描结果写入 /root/software/hbase-1.6.0/hbase2.log 日志文件:

echo “scan ‘movie:movie_bl’” | hbase shell -n > /root/software/hbase-1.6.0/hbase2.log
4. 将以上所有语句存储到 /headless/Desktop/hbase.txt 文件中。

5.2 HDFS数据导入HBase

任务介绍

一、任务要求
本实训要求我们根据电影数据,利用 MapReduce 读取 HDFS 数据,分析之后写入 HBase 表。

二、样例数据
下面是电影数据(完整数据集: /root/info/train/chapter5/5.2/movie.txt )。

tt13462900 长津湖 The Battle at Lake Changjin 2021-09-30 7.4 3.5 9.5
tt13364790 你好,李焕英 Hi, Mom 2021-02-12 7.8 4.0 9.5
tt15465312 我和我的父辈 My Country, My Parents 2021-09-30 6.9 3.5 9.5
… …
字段含义:

tt13462900:IMDb,IMDb编码
长津湖:ch_name,中文名
The Battle at Lake Changjin:eng_name,英文名
2021-09-30:release_time,上映时间
7.4:db_score,豆瓣评分
3.5:db_star,豆瓣星级
9.5:my_score,猫眼评分
三、输入输出源
输入源:在 HDFS 上创建 /hdfstohbase/movie/input 目录,将本地 /root/info/train/chapter5/5.2/movie.txt 文件上传至 HDFS 的 /hdfstohbase/movie/input 目录,作为数据源。

输出目标:HBase 的 movie 表。其中,“IMDb编码”为行键。另外,声明 “movie_info” 和 “grade” 列簇,“movie_info” 列簇下包括“中文名”、“英文名”和“上映时间”列,“grade” 列簇下包括“豆瓣评分”、“豆瓣星级”和“猫眼评分”列。

知识点介绍

HDFS 数据导入 HBase

  1. 输入输出
    输入源:HDFS 上的文件。
    输出目标:HBase 表。
  2. 实现方法
    (1)Mapper 函数实现

从 HDFS 读取数据,所以和之前写的 MapReduce 程序一样,Mapper 需要继承 Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> 类。Map 输入 value 为每一行数据,从中取出行键 RowKey 作为 Map 的输出 key,其它数据作为 Map 的输出 value,无需对其进行任何操作,直接通过 context.write(key, value) 传给 Reducer 进行处理。

(2)Reducer 函数实现

写个 Reducer 继承 TableReducer<KEYIN, VALUEIN, KEYOUT>。参数解析:

KEYIN:Map 过程 key 的输出类型。
VALUEIN:Map 过程 value 的输出类型。
KEYOUT:Reduce 输出到 HBase 中的 RowKey 类型,ImmutableBytesWritable 不可变类型。
其中的 reduce() 方法如下:

reduce(KEYIN key, Iterable values,Context context){ }
参数解析:

key:Reducer 的输入 key。
values: 相同 key 的 values 集合。
context:上下文。
(3)Driver(驱动)函数实现

与之前写的 MapReduce 的驱动类不同,在 Job 配置的时候没有配置 job.setReduceClass(),而是用以下方法执行 Reducer 类:

TableMapReduceUtil.initTableReducerJob(table, reducer, job);
该方法指明了在执行 Job 的 Reduce 过程时的详情。

参数解析:

table:数据输出目标是 HBase 表。
reducer:通过 Reducer 类执行 Reduce 过程。
job:作业对象。

任务1:创建MapReduce项目

要求1:新建 MapReduce 项目
打开 Eclipse,将 Eclipse 工作目录设置为 /root/eclipse-workspace,创建名为 “movie” 的Map/Reduce Project,在 “movie” 项目下创建名为 com.mr.movie 的 package 包。在 com.mr.movie 包下创建一个名为 HDFSToHBase 的 class 文件。

要求2:导入依赖 jar 包
右键“movie”项目—>选择“Build Path”—>“Configure Build Path”。之后弹出“Properties for telephone”对话框,选择“Libraries”界面,之后单击“Add External JARs…”。弹出“JAR Selection”对话框,选择“+ Other Locations”—>“Computer”,进入“/root/software/hbase-1.6.0/lib”目录,全选 lib 目录下的所有包(记得排除 ruby 目录),然后单击“open”,最后单击“Apply and Close”应用并关闭窗口。

任务2:Map端程序编写

注意:??? 部分需要自己填充完整。

Map 端实现方法
从 HDFS 读取数据,所以和之前写的 MapReduce 程序一样,Mapper 需要继承 Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> 类。Map 输入 value 为每一行数据,从中取出行键 RowKey (“IMDb编码”)作为 Map 的输出 key,其它数据作为 Map 的输出 value,无需对其进行任何操作,直接通过 context.write(key, value) 传给 Reducer 进行处理。

Map 端程序编写要求

  1. 在 HDFSToHBase 文件中,自定义名为 HDFSToHBaseMapper 的类,该类需要继承(extends)父类 Mapper,Mapper 的输入和输出数据都是 KV 对的形式:

输入 key 为一行文本的起始偏移量,输入 value 为一行文本的内容;
输出 key 为“IMDb编码” (行键),输出 value 为除去“IMDb编码”的其它数据。
2. HDFSToHBaseMapper 中的用户自定义业务逻辑写在 map() 方法中,具体实现如下:
 (1)获取每行文本,使用 toString() 方法将文本转化为字符串;
 (2)使用 indexOf() 方法返回文本中第一次出现“\t”的索引;
 (3)提取第一列“IMDb编码”,作为 HBase 表行键;
 (4)获取除第一列“IMDb编码”外的数据;
 (5)将第一列“IMDb编码”(行键)作为输出 key,将除去“IMDb编码”的其它数据作为输出 value,无需做任何处理交给 Reducer 。

HDFSToHBaseMapper 类的参考代码如下:

/**
 * KEYIN:读到的key是一行文本的起始偏移量 
 * VALUEIN:读到的value是一行文本的内容,从中取出“IMDb编码”(行键)作为输出key,其它数据作为输出value
 * 
 */
public static class HDFSToHBaseMapper extends Mapper<???, ???, ???, ???> {

@Override
protected void map(??? key, ??? value, Mapper<???, ???, ???, ???>.Context context)
throws IOException, InterruptedException {
// (1)获取每行文本,将文本转化为字符串
String line = ???;
// (2)返回数据中第一次出现“\t”的索引
int index = ???;
// (3)提取第一列“IMDb编码”,作为HBase表行键
String IMDb = ???;
// (4)获取除第一列“IMDb编码”外的数据
String movie_info = ???;
// (5)将第一列“IMDb编码”(行键)作为key,其它数据作为value
???
}
}

任务3:Reduce端程序编写

注意:??? 部分需要自己填充完整。

Reduce 端实现方法
该 Reducer 的作用是接收 Mapper 传过来的数据,将数据写入指定的 HBase 表。此实训中的 Reducer 需要继承 TableReducer<KEYIN, VALUEIN, KEYOUT> 类,参数为:

KEYIN:Mapper 的输出 key 类型
VALUEIN:Mapper 的输出 value 类型
KEYOUT:Reducer 输出到 HBase 中的 RowKey 类型,ImmutableBytesWritable 不可变类型 
Reduce 端程序编写要求

  1. 在 HDFSToHBase 文件中,自定义名为 HDFSToHBaseReducer 的类,该类需要继承(extends)父类 TableReducer:

输入 key 为“IMDb编码” (行键),输入 value 为除去“IMDb编码”的其它数据。
输出 key 为输出到 HBase 中的 RowKey,输出 value 为封装了 HBase 表数据的 Put 对象。
2. HDFSToHBaseReducer 中的用户自定义业务逻辑写在 reduce() 方法中,具体实现如下:

(1)获取行键;

(2)使用 for 循环遍历 values;

(3)根据分隔符“\t”切分文本;

(4)提取各列的值;

(5)创建 Put 对象,使用 Put 对象封装需要添加的信息,一个 Put 代表一行,构造函数传入的是 RowKey;

(6)往Put对象上添加信息(列簇,列,值);

(7)将数据写入 HBase 表,需要指定 RowKey 和 Put 对象。

HDFSToHBaseReducer 类的参考代码如下:

private static final String CF_MOVIE = "???"; // 列簇1
private static final String CF_GRADE = "???"; // 列簇2


/**
 * Reducer类继承TableReducer类 
 * KEYIN:对应Mapper输出的KEYOUT,即第一列“IMDb编码”(行键)
 * VALUEIN:对应Mapper输出的VALUEOUT,即除第一列外的其它数据
 * KEYOUT:ImmutableBytesWritable,不可变类型,reduce输出到HBase中的RowKey类型
 * VALUEOUT:Mutation类型
 *
 */
public static class HDFSToHBaseReducer extends ???<???, ???, ???> {

@Override
protected void reduce(Text key, Iterable<Text> value,
Reducer<Text, Text, ImmutableBytesWritable, Mutation>.Context context)
throws IOException, InterruptedException {
// (1)获取行键
String rowkeyStr = ???;

// (2)遍历value
for (??? movie_info : value) {
// (3)根据分隔符“\t”切分文本
String[] movie = movie_info.???;

// (4)提取各列的值
byte[] ch_name = ???;// 电影中文名
byte[] eng_name = ???;// 电影英文名
byte[] release_time = ???;// 上映时间
byte[] db_score = ???;// 豆瓣评分
byte[] db_star = ???;// 豆瓣星级
byte[] my_score = ???;// 猫眼评分

// (5)创建Put对象。使用Put对象封装需要添加的信息,一个Put代表一行,构造函数传入的是RowKey
Put put = ???;

// (6)往Put对象上添加信息 (列簇,列,值)
put.???(CF_MOVIE.getBytes(), "ch_name".getBytes(), ch_name);// 电影中文名
???// 电影英文名
???// 上映时间
???// 豆瓣评分
???// 豆瓣星级
???// 猫眼评分

// (7)将数据写入HBase表,需要指定RowKey和Put对象
ImmutableBytesWritable rowkey = new ImmutableBytesWritable(???);
context.???(rowkey, put);
}
}

任务4:Driver端程序编写

注意:??? 部分需要自己填充完整。

Driver 端实现方法
与之前写的 MapReduce 的驱动类不同,在 Job 配置的时候不需要配置job.setReduceClass() ,而是用以下方法执行 Reducer 类:

TableMapReduceUtil.initTableReducerJob(table, reducer, job);
该方法指明了在执行 Job 的 Reduce 过程时的详情信息。参数为:

table:数据输出目标是 HBase 表。
reducer:需要执行的 Reducer 类。
job:作业对象。
Driver 端程序编写要求

  1. MapReuce 程序 Driver 端编写:

(1)创建配置文件对象,要求程序使用本地运行模式,处理的数据在 HDFS 文件系统。另外,手动设置 ZooKeeper 队列名称和端口。

(2)使用 Job.getInstance() 创建一个 Job 对象。

(3)创建 HBase 表:

使用连接工厂根据配置器创建与 HBase 之间的连接对象;
获取表管理类 Admin 的实例,用来管理 HBase 数据库的表信息;
创建表描述类对象,定义表的名称;
通过表描述对象往表中添加列簇;
判断 HBase 表是否存在,若是存在就先删除,若是不存在则创建。
 (4)使用 Job 对象的setMapperClass() 方法指定我们自定义的 Mapper 类,使用 TableMapReduceUtil 工具类提供的initTableReducerJob() 方法指定我们自定义的 Reducer 类。

(5)使用 Job 对象的setMapOutputKeyClass() 方法和setMapOutputValueClass() 方法指定 MapTask 的输出 key-value 类型,使用 Job 对象的setOutputKeyClass() 方法和setOutputValueClass() 方法指定 ReduceTask 的输出 key-value 类型。

(6)指定该 MapReduce 程序数据的输入路径:

使用 HDFS Shell 的 mkdir 命令在 HDFS 的根目录(/)下级联创建 /hdfstohbase/movie/input 目录。
使用 HDFS Shell 的 put 命令将 /root/info/train/chapter5/5.2/movie.txt 文件上传到 HDFS 的 /hdfstohbase/movie/input 目录下。
使用 FileInputFormat.setInputPaths() 方法设置输入文件目录为 /hdfstohbase/input。
 (7)使用 Job 对象的waitForCompletion() 方法提交并运行作业。

Driver 端参考代码如下:

private static final String TABLE_NAME = "movie"; // HBase表名

public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// (1)创建HBase配置对象(继承自Hadoop的Configuration,这里使用父类的引用指向子类的对象的设计)
Configuration config = ???;
// HDFS集群中NameNode的URI,获取DistributedFileSystem实例
config.???("fs.defaultFS", "hdfs://???");
// 通过config.set()方法进行手动设置。设置ZooKeeper队列名称和端口
config.???("hbase.zookeeper.quorum", "???");
config.???("hbase.zookeeper.property.clientPort", "???");
// (2)新建一个Job任务
Job job = Job.???;
/*
 * (3)以下这一段代码是为了创建一张名为movie的HBase表
 */
// 1)使用连接工厂根据配置器创建与HBase之间的连接对象
Connection connect = ConnectionFactory.???;
// 2)获取表管理类Admin的实例,用来管理HBase数据库的表信息
Admin admin = connect.???;
// 3)创建表描述类对象,定义表的名称
TableName tName = TableName.???(TABLE_NAME);// 表名称
HTableDescriptor desc = ???;
// 4)通过表描述对象往表中添加列簇
desc.???(new HColumnDescriptor(CF_MOVIE.getBytes()));// 列簇1
???// 列簇2
// 5)判断HBase表是否存在,若是存在就先删除,若是不存在则创建
if (admin.???(tName)) {
admin.???(tName);// 禁用表
admin.???(tName);// 删除表
}
admin.???(desc);// 创建表
// (4)指定Mapper类和Reducer类
job.???(HDFSToHBaseMapper.class);
// TableMapReduceUtil为HBase提供的工具类
TableMapReduceUtil.???;
// (5)指定MapTask和ReduceTask的输出key-value类型
job.???;
job.???;
job.???;
job.???;
// (6)指定该MapReduce程序数据的输入路径
FileInputFormat.???(job, ???("/hdfstohbase/movie/input"));
// (7)最后给YARN来运行,等着集群运行完成返回反馈信息,客户端退出
boolean waitForCompletion = job.???;
System.exit(waitForCompletion ? 0 : 1);
}
  1. 在 Linux 命令行执行以下命令统计 movie 表数据总行数,并将统计结果写入 /root/software/hbase-1.6.0/hbase.log 日志文件:

echo “count ‘movie’” | hbase shell -n > /root/software/hbase-1.6.0/hbase.log

5.3 HBase数据导入HDFS

任务介绍

一、任务要求
本实训要求我们根据电影数据,利用 MapReduce 读取 HBase 表数据写入 HDFS。

二、样例数据
下面是电影数据(完整数据集: /root/info/train/chapter5/5.3/movie.txt )。

tt13462900 长津湖 The Battle at Lake Changjin 2021-09-30 7.4 3.5 9.5
tt13364790 你好,李焕英 Hi, Mom 2021-02-12 7.8 4.0 9.5
tt15465312 我和我的父辈 My Country, My Parents 2021-09-30 6.9 3.5 9.5
… …
字段含义:

tt13462900:IMDb,IMDb编码
长津湖:ch_name,中文名
The Battle at Lake Changjin:eng_name,英文名
2021-09-30:release_time,上映时间
7.4:db_score,豆瓣评分
3.5:db_star,豆瓣星级
9.5:my_score,猫眼评分
三、输入输出源
输入源:HBase 的 movie 表。

输出目标:HDFS 的 /hbasetohdfs/movie/output 目录。

知识点介绍

HBase 数据导入 HDFS

  1. 输入输出
    输入源:HBase 表。
    输出目标:HDFS 上的文件。
  2. 实现方法
    (1)Mapper 函数实现

从 HBase 读取数据,所以和之前写的 MapReduce 程序不一样,Mapper 需要继承 TableMapper<KEYOUT, VALUEOUT> 抽象类,TableMapper 类专门用于完成 MapReduce 中 Map 过程与 HBase 表之间的操作。参数解析:

KEYOUT:Mapper 的输出 key 类型。
VALUEOUT:Mapper 的输出 value 类型。
其中的 map() 方法如下:

map(ImmutableBytesWritable key, Result value,Context context){ }
参数解析:

key:HBase 表的行键 rowkey。
value:rowkey 对应的记录集合。
context:上下文。
此处的 map() 核心实现:是遍历 key 对应的记录集合 value,将其组合成一条记录通过 content.write(key,value) 填充到 <key,value> 键值对中。

(2)Reducer 函数实现

写个 Reducer 继承 Reducer<KEYIN,VALUEIN,KEYOUT,VALUEOUT>。直接输出 Map 输出的 <key,value> 键值对,没有对其做任何处理。

(3)Driver(驱动)函数实现

与之前写的 MapReduce 的驱动类不同,在 Job 配置的时候没有配置 job.setMapperClass(),而是用以下方法执行 Mapper 类:

TableMapReduceUtil.initTableMapperJob(table, scan, mapper, outputKeyClass, outputValueClass, job);
该方法指明了在执行 Job 的 Map 过程时的详情。

参数解析:

table:数据输入源,即 HBase 表。
reducer:通过扫描读入对象 scan 对表进行全表扫描,为 Map 过程提供数据源输入。
mapper:通过 Mapper 类执行 Map 过程。
outputKeyClass:Map 过程的输出 key 类型。
outputValueClass:Map 过程的输出 value 类型。
job:作业对象。
特别注意:这里声明的是一个最简单的扫描读入对象 scan,进行表扫描读取数据,其中 scan 可以配置参数。

任务1:创建HBase表

要求:将以下所有语句存储到 /headless/Desktop/hbase.txt 文件中。

创建 HBase 表
进入 HBase Shell 交互模式,在该模式下,创建名为 “movie” 的 HBase 表,该表有两个列簇,分别为:“movie_info” 和 “grade”,版本号分别为2,3。

导入数据

  1. 使用 HDFS Shell 的 mkdir 命令在 HDFS 的根目录(/)下级联创建 /import/movie 目录。

  2. 使用 HDFS Shell 的 put 命令将 /root/info/train/chapter5/5.3/movie.txt 文件上传到 HDFS 的 /import/movie 目录下。

  3. 执行 importtsv 命令将 HDFS 中 /import/movie/movie.txt TSV 格式数据导入 HBase 的 “movie”表。其中,第一列为 HBASE_ROW_KEY(行键),“ch_name”、“eng_name” 和 “release_time” 属于 “movie_info” 列簇,“db_score”、“db_star”和 “my_score” 属于 “grade” 列簇。

  4. 在 Linux 命令行执行以下命令扫描 movie 表中的所有数据,并将扫描结果写入 /root/software/hbase-1.6.0/hbase.log 日志文件:

echo “scan ‘movie’” | hbase shell -n > /root/software/hbase-1.6.0/hbase.log
/root/software/hbase-1.6.0/hbase.log 日志文件部分内容如下:
在这里插入图片描述

任务2:创建MapReduce项目

要求1:新建 MapReduce 项目
打开 Eclipse,将 Eclipse 工作目录设置为 /root/eclipse-workspace,创建名为 “movie” 的Map/Reduce Project,在 “movie” 项目下创建名为 com.mr.movie 的 package 包。在 com.mr.movie 包下创建一个名为 HBaseToHDFS 的 class 文件。

要求2:导入依赖 jar 包
右键“movie”项目—>选择“Build Path”—>“Configure Build Path”。之后弹出“Properties for telephone”对话框,选择“Libraries”界面,之后单击“Add External JARs…”。弹出“JAR Selection”对话框,选择“+ Other Locations”—>“Computer”,进入“/root/software/hbase-1.6.0/lib”目录,全选 lib 目录下的所有包(记得排除 ruby 目录),然后单击“open”,最后单击“Apply and Close”应用并关闭窗口。

任务3:Map端程序编写

注意:??? 部分需要自己填充完整。

Map 端实现方法
从 HBase 读取数据,所以和之前写的 MapReduce 程序不一样,Mapper 需要继承 TableMapper<KEYOUT, VALUEOUT> 抽象类,TableMapper 类专门用于完成 MapReduce 中 Map 过程与 HBase 表之间的操作。Map 输入 key 为 HBase 表的行键 RowKey,输入 value 为行键 RowKey 对应的记录集合。从中取出行键 RowKey (“IMDb编码”)作为 Map 的输出 key,将行键对应的列簇、列及对应值组成一条记录作为 Map 的输出 value,无需对其进行任何操作,直接通过 context.write(key, value) 传给 Reducer 进行处理。

Map 端程序编写要求

  1. 在 HBaseToHDFS 文件中,自定义名为 HBaseToHDFSMapper 的类,该类需要继承(extends)父类 TableMapper,Mapper 的输入和输出数据都是 KV 对的形式:

输入 key 为 movie 表中的行键 RowKey ,输入 value 为行键 RowKey 对应的一行数据的结果集 Result;
输出 key 为“IMDb编码” ,输出 value 为行键“IMDb编码” 对应的列簇、列及对应值组成一条记录。
2. HBaseToHDFSMapper 中的用户自定义业务逻辑写在 map() 方法中,具体实现如下:

(1)获取行键“IMDb编码”;

(2)使用 value 的 listCells() 方法得到一个 Cell 集合;

(3)使用 for 循环遍历 Cell 集合,依次获取列簇、列以及对应值;

(4)将“IMDb编码”作为输出 key,将获取的列簇、列以及对应值作为输出 value,无需做任何处理交给 Reducer 。

HBaseToHDFSMapper 类的参考代码如下:

package com.mr.movie;

import java.io.IOException;
import java.util.List;

import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.mapreduce.TableMapper;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

/**
 * 编写MapReduce程序从HBase表movie中读取数据,然后存储到HDFS中
 *
 */
public class HBaseToHDFS {
/**
 * 继承了TableMapper<Text,Text>抽象类,TableMapper类专门用于完成MR中Map过程与HBase表之间的操作
 * Mapper的输入key-value类型:<ImmutableBytesWritable,Result>
 * Mapper的输出key-value类型:由用户自己制定
 * 
 */
public static class HBaseToHDFSMapper ??? {
Text value_text = ???;

/**
 * 输入key:HBase中的行键rowkey 
 * 输入value:HBase表中某一个rowkey对应的Result结果集
 * 
 * 此处的map()核心实现是遍历key对应的记录集合value,
 * 将其组合成一条记录通过content.write(key,value)填充到输出<key,value>键值对中
 */
@Override
protected void map(ImmutableBytesWritable key, Result value,
Mapper<ImmutableBytesWritable, Result, ???, ???>.Context context)
throws IOException, InterruptedException {
// (1)获取行键“IMDb编码”
String m_rowkey = Bytes.toString(???);
// (2)通过result得到一个Cell集合
List<Cell> m_list = ???;
// (3)for循环遍历Cell集合,依次获取列簇、列以及对应值
for (Cell m_cell : m_list) {
// 分别对每个cell打印,获取列簇、列以及对应值
String family = ???;// 列簇
String qualifier = ???;// 列
String values = ???;// 值
// (4)将“IMDb编码”作为输出key,将获取的列簇、列以及对应值作为输出value
value_text.???(family + "\t" + qualifier + "\t" + values + "\t");
???;
}
}
}
}

任务4:Reduce端程序编写

注意:??? 部分需要自己填充完整。

Reduce 端实现方法
该 Reducer 的作用是接收 Mapper 传过来的数据,将数据写入 HDFS。此实训中的 Reducer 需要继承 Reducer<KEYIN,VALUEIN,KEYOUT,VALUEOUT> 类,不需对其做任何处理,直接输出 Map 输出的 <key,value> 键值对即可。

Reduce 端程序编写要求

  1. 在 HBaseToHDFS 文件中,自定义名为 HBaseToHDFSReducer 的类,该类需要继承(extends)父类 Reducer,Reducer 的输入和输出数据都是 KV 对的形式::

输入 key 为“IMDb编码”(行键),输入 value 为行键“IMDb编码” 对应的列簇、列及对应值组成一条记录。
输出 key 为“IMDb编码” ,输出 value 为行键“IMDb编码” 对应的列簇、列及对应值组成一条记录。
2. HBaseToHDFSReducer 中的用户自定义业务逻辑写在 reduce() 方法中,具体实现如下:

(1)使用 for 循环遍历 values;

(2)将输入 key 作为输出 key,将遍历出的 value 作为输出 value。

任务5:Driver端程序编写

注意:??? 部分需要自己填充完整。

Driver 端实现方法
与之前写的 MapReduce 的驱动类不同,在 Job 配置的时候不需要配置job.setMapperClass() ,而是用以下方法执行 Mapper 类:

TableMapReduceUtil.initTableMapperJob(table, scan, mapper, outputKeyClass, outputValueClass, job);
该方法指明了在执行 Job 的 Map 过程时的详情信息。参数为:

table:数据输入源,即 HBase 表。
reducer:通过扫描读入对象 scan 对表进行全表扫描,为 Map 过程提供数据源输入。
mapper:通过 Mapper 类执行 Map 过程。
outputKeyClass:Map 过程的输出 key 类型。
outputValueClass:Map 过程的输出 value 类型。
job:作业对象。
Driver 端程序编写要求
MapReduce 程序 Driver 端编写:

(1)创建配置文件对象,要求程序使用本地运行模式,处理的数据在 HDFS 文件系统。另外,手动设置 ZooKeeper 队列名称和端口。

(2)使用 Job.getInstance() 创建一个 Job 对象。

(3)获取一个 scan 实例,用于获取 HBase 表数据。

(4)使用 TableMapReduceUtil 工具类提供的initTableMapperJob() 方法指定我们自定义的 Mapper 类。使用 Job 对象的setReducerClass() 方法指定我们自定义的 Reducer 类。

(5)使用 Job 对象的setOutputKeyClass() 方法和setOutputValueClass() 方法指定 ReduceTask 的输出 key-value 类型。

(6)指定该 MapReduce 程序数据的输出路径。使用 FileOutputFormat.setOutputPath() 方法设置输出文件目录为 /hbasetohdfs/movie/output。

(7)使用 Job 对象的waitForCompletion() 方法提交并运行作业。

Driver 端参考代码如下:

private static final String TABLE_NAME = "movie"; // HBase表名

public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// (1)创建HBase配置对象(继承自Hadoop的Configuration,这里使用父类的引用指向子类的对象的设计)
Configuration config = ???;
// HDFS集群中NameNode的URI,获取DistributedFileSystem实例
config.???("???", "???");
// 通过config.set()方法进行手动设置。设置ZooKeeper队列名称和端口
config.???("hbase.zookeeper.quorum", "???");
config.???("hbase.zookeeper.property.clientPort", "???");
// (2)新建一个Job任务
Job job = Job.???(config);
// (3)获取一个scan实例,用于获取HBase表数据
Scan scan = ???;
// (4)指定Mapper类和Reducer类
???.???(TABLE_NAME, scan, HBaseToHDFSMapper.class, ???.class, ???.class, job);
job.???(HBaseToHDFSReducer.class);
// (5)指定ReduceTask的输出key-value类型
job.???;
job.???;
// (6)指定该MapReduce程序数据的输出路径
Path outpath = ???("/hbasetohdfs/movie/output");
// 获取fs对象
FileSystem fs = FileSystem.???(config);
if (fs.???(outpath)) { // 如果输出目录存在
fs.???(outpath, ???);// 递归删除输出目录
}
FileOutputFormat.???(job, outpath);
// (7)最后给YARN来运行,等着集群运行完成返回反馈信息,客户端退出
boolean waitForCompletion = job.waitForCompletion(true);
System.exit(waitForCompletion ? 0 : 1);
}

5.4 HBase的WordCount

任务介绍

一、任务要求
本实训要求我们利用 MapReduce 读取 HBase 表数据实现 WordCount 词频统计,并将结果写入 HBase。

二、样例数据
下面是单词数据(完整数据集: /root/info/train/chapter5/5.4/words.txt )。

01 Welcome to Apache HBase
02 Apache HBase is the Hadoop database
03 Use Apache HBase when you need random realtime read/write access to your Big Data
… …
字段含义:

01:序号
Welcome to Apache HBase:文本内容
三、输入输出源
输入源:HBase 的 words 表。

输出目标:HBase 的 wordcount 表。

知识点介绍

HBase 的 WordCount

  1. 输入输出
    输入源:HBase 表1。
    输出目标:HBase 表2。
  2. 实现方法
    (1)Mapper 函数实现

从 HBase 读取数据,所以和之前写的 MapReduce 程序不一样,Mapper 需要继承 TableMapper<KEYOUT, VALUEOUT> 抽象类,TableMapper 类专门用于完成 MapReduce 中 Map 过程与 HBase 表之间的操作。参数解析:

KEYOUT:Mapper 的输出 key 类型。
VALUEOUT:Mapper 的输出 value 类型。
其中的 map() 方法如下:

map(ImmutableBytesWritable key, Result value,Context context){ }
参数解析:

key:HBase 表的行键 rowkey。
value:rowkey 对应的记录集合。
context:上下文。
(2)Reducer 函数实现

写个 Reducer 继承 TableReducer<KEYIN, VALUEIN, KEYOUT>。参数解析:

KEYIN:Map 过程 key 的输出类型。
VALUEIN:Map 过程 value 的输出类型。
KEYOUT:Reduce 输出到 HBase 中的 RowKey 类型,ImmutableBytesWritable 不可变类型。
其中的 reduce() 方法如下:

reduce(KEYIN key, Iterable values,Context context){ }
参数解析:

key:Reducer 的输入 key。
values: 相同 key 的 values 集合。
context:上下文。
(3)Driver(驱动)函数实现

与之前写的 MapReduce 的驱动类不同,在 Job 配置的时候不需要配置job.setMapperClass()和job.setReduceClass(),而是用以下方法执行 Mapper 和 Reducer 类:

TableMapReduceUtil.initTableMapperJob(table, scan, mapper, outputKeyClass, outputValueClass, job);
TableMapReduceUtil.initTableReducerJob(table, reducer, job);
initTableMapperJob()方法指明了在执行 Job 的 Map 过程时的详情信息。参数为:

table:数据输入源,即 HBase 表。
scan:通过扫描读入对象 scan 对表进行全表扫描,为 Map 过程提供数据源输入。
mapper:通过 Mapper 类执行 Map 过程。
outputKeyClass:Map 过程的输出 key 类型。
outputValueClass:Map 过程的输出 value 类型。
job:作业对象。
initTableReducerJob()方法指明了在执行 Job 的 Reduce 过程时的详情信息。参数为:

table:数据输出目标是 HBase 表。
reducer:需要执行的 Reducer 类。
job:作业对象。


原文地址:https://blog.csdn.net/qq_23934063/article/details/140949198

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!