目录
一、需求及总体设计
1、设计完成后的作业
2、设计完成后的转换
3、处理流程概述
二、具体处理流程
1、作业
1、JS下载文件到本地
2、转换(具体处理见下面)
3、JS重命名
4、Shell组件
5、删除多个文件
2、转换
2.1 文本文件输入
2.2字段选择
2.3 表输入
2.4 JS脚本校验IP
2.5过滤记录
2.6 阻塞数据直到步骤完成
2.7 调用存储过程
需求: 将 HDFS 中的文件解析完成后 将数据写入 SQLServer 数据库,然后将 处理后的 HDFS 文件标记后缀为完成解析,最后将标记后的文件移动到 bak 目录。
此处说明一下:数据发送的 方式要选择 复制发送模式 因为 不合法的 IP 会过滤一部分 数据,发送给其他的 业务处理逻辑 就会减少一部分的数据,所以采用 复制发送模式,让每个处理拿到 HDFS解析后的全部数据。
JS 下载文件到本地,使用了Java自定义封装好的代码实现,将 编写好的代码编译打包放在kettle的 lib包中即可。刚开始我们可以将HDFS中的文件下载到 windows 环境进行测试,待本地测试没有问题后,再将 targetFolder 换成 linux 中的文件路径,进行linux中的测试。(代码附录到最后)
此处从下载到本地的HDFS文件匹配, 就是从找到 本次解析后的文件然后 将 HDFS中的文件找到 加上 ".completed" 后缀,表示此文件已经处理过,之后下次正则匹配就不会将这些文件匹配下载到本地了。此处的 文件的重命名 和前面的 HDFS文件下载到本地都封装到了 HDFSProcess类中,之后会附上代码,大家可以参考。(代码附录到最后)
说明:想用Kettle Shell 组件 做到 HDFS中 文件的移动 ,脚本的命令也很简单 ,mv将 加了 .completed 解析完成后的文件移动到 bak 目录,这块想用 kettle的 Shell 组件实现,另外一个思路是直接在 linux 做一个 定时的文件移动也是可以的。此处 Kettle 的Shell 组件我也是不知道怎么用,我是在linux机器上运行的,刚开始我用的 ".sh" 文件发现不行。我就改为 .bat 文件,想着应该是 kettle不识别 .sh 文件。之后又试了6次路径的配置成功了,之后不知道会不会出现问题,
(1)【Shell】
shell组件是用来执行Windows下的批处理脚本,即bat文件(参考博客https://www.csdn.net/gather_25/MtTaUg4sMzMxOTQtYmxvZwO0O0OO0O0O.html)。但是测试 运行后的日志我发现 kettle 知道正在运行的 platform 是 linux,也是说 .bat 在 linux 平台也是可以运行的,但是linux 平台下 脚本还是 以.sh 结尾。
(2)第二步:编辑组件
我在 linux 运行环境的配置。就配置一个当前脚本的运行路径,就生效了。
此处我的另外一个实现思路是在作业启动的脚本中开启 一个 linux的定时器 ,定时的 将 解析后的 HDFS 的 .completed 文件移动到 bak 目录,也在进一步改进中。
再看一下我整个的ETL的文件有哪些:(chmod +x .bat 之后我改为 .sh 结尾)
将下载到本地的 多个 HDFS文件 删除掉。当然了也可以增加一个 本地文件的备份,看需要。
文本文件这里主要配置三块文件、内容、字段。
1、文件
2、 内容
3、字段
这块由于后面使用“?” 传递流中的字段,使用选择字段调整了 字段的 顺序。
因为业务需要查询 hostname 然后根据查询到的 主机做更新 IP、更新机器配置,所以此处用到了表输入,并且使用了“?” 接收上一步传过来的字段值,目前不知道“?”是否会带来 ETL的风险,进一步的验证测试中。 “?” 号代表上一步传入的条件,第一个“?” 代表上一步第一个字段名称,第二个“?” 代表上一步第二个字段名称,依次类推。
此步骤使用 JS 校验IP, 如果 IP 合法 我们才调用存储过程 更新IP。
var reg = /^((\d)|([1-9]\d)|(1\d{2})|((2[0-4]\d)|(25[0-5])))(\.((\d)|([1-9]\d)|(1\d{2})|((2[0-4]\d)|(25[0-5])))){3}$/;
if (reg.test(IP) == true){
var flag = 1
}else {
var flag = 2
}
此组件在处理 正则,判断时很重要,尤其在处理一个字段的不同业务分离,刚开始不会使用 此组件,百度学习后解决了SQLServer 自身SQL不支持正则判断的问题。
由于需要从表里查出hostname 对应的 key ,然后 解析数据流 里面的字段,不希望流里的数据 没查完表就流向后边,所以使用了阻塞数据直到步骤完成。
调用过程中的返回值名称不要填写,我调用的存储过程是带有输入参数,不带输出参数
运行的结果
两个文件夹的移动到 bak 目录也是成功的。
说明:代码是利用发编译工具 解析的 class 文件,然后自己手巧上去的,正确与否本人没有测试,具体的实现确实是如此,最后要在 resource 中添加 HDFS的 配置文件 core-site.xml 、HDFS-site.xml等配置文件。
package com.kangna.hdfs;
import com.google.common.collect.Sets;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
/**
* @AUTHOR kangll
* @DATE 2020/6/20 10:16
* @DESC: HDFS 工具类
*/
public class HDFSProcess {
private static final Logger logger = LoggerFactory.getLogger(HDFSProcess.class);
private String regex;
private String srcHDFSFolder;
private String destinationFolder;
private FileSystem fileSystem;
public HDFSProcess() {
}
public HDFSProcess(String regex, String srcHDFSFolder, String destinationFolder) {
this.regex = regex.toLowerCase();
this.srcHDFSFolder = srcHDFSFolder;
this.destinationFolder = destinationFolder;
}
/**
* 下载文件到本地
*/
public void downloadFile() throws Exception {
// 提供配置参数对象
Configuration conf = new Configuration();
// 获取文件的状态
List paths = HDFSUtil.listStatus(conf, this.srcHDFSFolder, this.regex);
logger.info("HDFS matched " + paths.size() + " files");
for (Path path : paths) {
String fileName = path.getName();
fileSystem = FileSystem.get(conf);
// 比如: /hadoop/kangna/ods
Path srcPath = new Path(this.srcHDFSFolder + File.separator + fileName);
// 比如:G:\\API\\
String localPath = this.destinationFolder + File.separator + fileName;
FSDataInputStream dataInputStream = fileSystem.open(srcPath);
BufferedReader reader = new BufferedReader(new InputStreamReader(dataInputStream));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(localPath)));
String line = null;
while ((line = reader.readLine()) != null) {
writer.write(line + "\n");
}
reader.close();
writer.close();
}
logger.info("download file successful.");
}
/**
* 文件重命名
*
* @param localPath 本地文件路径
* @param HDFSPath HDFS文件路径
* @param suffix 后缀
*/
public void rename(String localPath, String HDFSPath, String suffix) throws Exception{
Configuration conf = new Configuration();
// 获取本地文件名的集合
Set fileNameList = getFileLocalName(localPath);
logger.info("match local file size : " + fileNameList.size());
System.out.println("matche local file size : " + fileNameList.size());
for (String fileName : fileNameList) {
String src = HDFSPath + File.separator + fileName;
// 比如: 将 kangna.txt 重命名 为 kangna.txt.completed
String target = HDFSPath + File.separator + fileName + suffix;
// 重命名 实现 调用 renameTo
boolean flag = HDFSUtil.renameTo(conf, src, target);
if (flag) {
System.out.println("renamed file " + src + " to " + target + "successful.");
} else {
System.out.println("renamed file " + src + " to " + target + "failed.");
}
System.out.println(fileName);
}
}
public Set getFileLocalName(String localPath) {
System.out.println("listing path is : " + localPath);
// 将正则编译为此类的实例
Pattern pattern = Pattern.compile(this.regex);
Set list = Sets.newHashSet();
File baseFile = new File(localPath);
if ((baseFile.isFile()) || (!baseFile.exists())) {
return list;
}
// 返回文件的抽象路径名数组
File[] files = baseFile.listFiles();
System.out.println("this path has files : " + files.length);
for (File file : files) {
if (file.isDirectory()) {
list.addAll(getFileLocalName(file.getAbsolutePath()));
} else {
list.add(file.getName());
}
}
return list;
}
public static void main(String[] args) throws Exception {
System.out.println(new HDFSProcess());
}
}
package com.kangna.hdfs;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.LocatedFileStatus;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.RemoteIterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
* @AUTHOR kangll
* @DATE 2020/6/20 10:31
* @DESC:
*/
public class HDFSUtil {
private static final Logger logger = LoggerFactory.getLogger(HDFSUtil.class);
// 文件执行中的标记
private static final String FILE_EXECUTING_MARK = ".executing";
// 文件执行后的标记
private static final String FILE_COMPLETED_MARK = ".completed";
private static FileSystem fileSystem;
/**
* 获取文件执行的状态
*
* @param conf
* @param dirPath
* @return
*/
public static List listStatus(Configuration conf, String dirPath) throws Exception {
return listStatus(conf, dirPath, "");
}
/**
* 获取文件执行的状态
*
* @param conf 配置参数
* @param dirPath 文件路径
* @param regex 要匹配的正则
* @return
*/
public static List listStatus(Configuration conf, String dirPath, String regex) throws IOException {
List files = new ArrayList<>();
try {
// 返回配置过的文件系统对象
fileSystem = FileSystem.get(conf);
// 列出指定路径中文件的状态和块位置
RemoteIterator fileStatus = fileSystem.listFiles(new Path(dirPath), true);
Pattern pattern = Pattern.compile(regex);
while (fileStatus.hasNext()) {
LocatedFileStatus file = fileStatus.next();
Path path = file.getPath();
String fileName = path.getName().toLowerCase();
if (regex.equals("")) {
files.add(path);
logger.info("match file : " + fileName);
} else if (pattern.matcher(fileName).matches()) {
files.add(path);
logger.info("match file : " + fileName);
}
}
} finally {
if (fileSystem != null) {
fileSystem.close();
}
}
return files;
}
/**
* 创建文件夹
*
* @param conf
* @param dir
* @return
*/
public static boolean mkdir(Configuration conf, String dir) throws IOException {
boolean flag = false;
try {
fileSystem.get(conf);
if (fileSystem.exists(new Path(dir))) {
flag = true;
} else {
fileSystem.mkdirs(new Path(dir));
}
} finally {
if (fileSystem != null) {
fileSystem.close();
}
}
return flag;
}
/**
* @param conf
* @param src 要重命名的 src 路径
* @param target 重新命名的 target 路径
* @return
* @throws Exception
*/
public static boolean renameTo(Configuration conf, String src, String target) throws Exception {
boolean flag = false;
try {
fileSystem = FileSystem.get(conf);
if (fileSystem.exists(new Path(target))) {
fileSystem.delete(new Path(target), true);
logger.info("target file : " + target + " exist, deleted");
}
// 将路径名 src 重命名为 路径target
flag = fileSystem.rename(new Path(src), new Path(target));
if (flag) {
logger.info("renamed file " + src + " to " + target + " success!");
} else {
logger.info("renamed file " + src + " to " + target + " failed!");
}
} finally {
if (fileSystem != null) {
fileSystem.close();
}
}
return flag;
}
}