最近有个需求,ftp主机上有很多目录,目录下层级不一,需要把父目录下所有满足的文件夹下的指定文件下载到HDFS,并且保留目录结构,因为数据在ftp主机已经落地,如果用公司内部的etl工具并不好实现这种层级不一的文件导入,故改用开发代码去实现。
思路:1.遍历ftp指定目录,找到指定文件夹的路径
2.拿到所有文件夹的路径,下载到hdfs
实现(这里只放本地测试代码):
1.FTP连接工具类
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;
import org.apache.log4j.Logger;
import java.io.IOException;
/**
* @ClassName FTPUtils
* @Decription FTP Connect Util Class
* @Author KGC
* @Date 2019/03/21
**/
public class FTPUtils {
private static Logger logger = Logger.getLogger(FTPUtils.class);
/**
* FTP连接
*/
public static FTPClient loginFTP(String host, int port, String userName, String passWord) {
// ftpClient.enterLocalPassiveMode();
FTPClient ftpClient = null;
try {
ftpClient = new FTPClient();
//连接ftp
ftpClient.connect(host, port);
//登录ftp
ftpClient.login(userName, passWord);
ftpClient.enterLocalPassiveMode();
//字符格式
ftpClient.setControlEncoding("UTF-8");
// ftpClient.setFileType(ftpClient.BINARY_FILE_TYPE);
if (!FTPReply.isPositiveCompletion(ftpClient.getReplyCode())) {
logger.error("ftp连接失败,请检查用户名或密码是否错误");
ftpClient.disconnect();
} else {
logger.info("连接成功!");
}
}catch (Exception e){
logger.error("连接失败,检查用户名和密码");
}
return ftpClient;
}
}
很简单,直接使用org.apache.commons.net.ftp.FTPClient中的api
2.遍历ftp文件类
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName TraverseFtpDir, return dir List
* @Decription
* @Author KGC
* @Date DATE
**/
public class TraverseFtpDir {
static List<String> list = new ArrayList<String>();
public static List func(String file, String fileName,FTPClient ftpClient) throws IOException {
FTPFile[] fs =ftpClient.listFiles(file);
//遍历目录,如果是子目录则递归继续遍历
for (FTPFile f : fs) {
// System.out.println(f);
if (f.isDirectory()) {
func(file + f.getName()+ "/", fileName, ftpClient);
if ((f.toString().substring((f.toString().length()) - 8, (f.toString().length()))).equals(fileName)) {
list.add(file + f.getName());
}
}
}
return list;
}
}
简单说下逻辑,ftpClients.listFiles拿到该目录下的所有文件,包括文件夹和文件,然后循环遍历,判断是否为目录(文件夹),如果是则递归该方法,再继续判断是否为指定的文件夹,如果是的话,加入到list。这里需要注意的就是,遍历函数的参数传入的改变,递归调用的时候,参数传入的路径并不是原来第一次传入的路径参数了,而是遍历一次后的子目录的路径: func(file + f.getName()+ “/”, fileName, ftpClient);不然拿到的始终是第一层目录的指定文件路径。
3.下载数据到HDFS
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.log4j.BasicConfigurator;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* @ClassName LoadDataToHdfs
* @Decription get dir and download data to hdfs from it
* @Author KGC
* @Date 2019/3/24
**/
public class LoadDataToHdfs {
public static void loadDatatoHdfs(Configuration conf) throws IOException {
//ftp客户端对象,参数需修改
FTPClient ftp = FTPUtils.loginFTP("10.211.55.6", 21, "wangjuncheng", "wjc5524568");
InputStream inputStream = null;
FSDataOutputStream outputStream = null;
List li = TraverseFtpDir.func("test/", "20190319", ftp);
FileSystem fileSystem = FileSystem.get(conf);
for (int i = 0; i < li.size(); i++) {
if (!li.get(i).equals(null)) {
//拿到该目录下的所有文件
FTPFile[] files = ftp.listFiles(li.get(i).toString());
ftp.changeWorkingDirectory(li.get(i).toString());
for (FTPFile f : files) {
inputStream = ftp.retrieveFileStream(f.getName());
//这里修改/interface下的路径
outputStream = fileSystem.create(new Path(li.get(i).toString()));
IOUtils.copyBytes(inputStream, outputStream, conf, false);
if (inputStream != null) {
inputStream.close();
ftp.completePendingCommand();
}
}
}
}
ftp.disconnect();
}
public static void main(String[] args) throws IOException {
BasicConfigurator.configure();
Configuration conf = new Configuration();
// conf.set("fileSystem.defaultFS", "hdfs://10.211.55.6:9000");
// System.setProperty("HADOOP_USER_NAME", "root");
Path path = new Path("hdfs://10.211.55.6:9000/0001");
FileSystem fs = FileSystem.get(conf);
fs.mkdirs(path);
LoadDataToHdfs.loadDatatoHdfs(conf);
}
}
这里也遇到几个坑,inputstream循环取出来的时候一直报nullException,查资料大部分说是inputstream每次循环后需要手动关闭,即加入 inputStream.close(); ftp.completePendingCommand();这两句,但我是已经加入的,并且并不是第一次循环有值,而是每次循环都为空,后来找找别的原因,其实是在调用方法之前,需要吧ftp目录定位到当前目录,即需加入ftp.changeWorkingDirectory(li.get(i).toString());
这样才能取得到每次的文件路径。