一、任务名:
开发最小目录工具
二、任务描述
开发工具,从桶清单文件中列举出所有最小目录,并列举出每一个最小目录中包含的文件总数与文件总量。
最小目录的解释:
有以下几个目录
a/b/1.txt
a/b/2/txt
a/3.txt
a/b/c/
则,最小目录有:
a/b
a/
最小目录包含的对象数为:
a/b:2
a/:1
三、开发思路
这个工作实际上属于目录解析的范畴,与目录解析相关的问题可以通过前缀树来解决
1)前缀树节点开发
有树要先有节点,每一级目录可以视为一个节点。
这个节点包含接下来要去往的目录节点,而这种目录节点可能有很多个,我们必须能快速通过目录名来查找节点,因此选则HashMap,将目录名作为Key,目录节点作为value,nexts=HashMap<Key,Node>。
又因为,每一个目录节点均有可能称为最小目录,那么我们遍历节点的时候应该能够拿出节点中的文件数和文件总大小,故这两个属性也要设置
最后,你不能只知道下面的名字,而不知道自己的名字,所以每个节点也应该有自己的Name
因此Node的构建为PrefixTreeNode:
import java.util.HashMap;
/**
* @author sq
* @date 2023/8/28
* @Description ${}
*/
public class PrefixTreeNode {
String Name;//该节点名称
long file_num = 0l;//该节点叶子结点个数
long file_size = 0l;//该节点叶子接地点总大小
HashMap<String, PrefixTreeNode> nexts = null; //该节点的子节点列表
public PrefixTreeNode() {
}
public String getName() {
return Name;
}
public void setName(String name) {
Name = name;
}
public long getFile_num() {
return file_num;
}
public void setFile_num(long file_num) {
this.file_num = file_num;
}
public long getFile_size() {
return file_size;
}
public void setFile_size(long file_size) {
this.file_size = file_size;
}
public HashMap<String, PrefixTreeNode> getNexts() {
return nexts;
}
public void setNexts(HashMap<String, PrefixTreeNode> nexts) {
this.nexts = nexts;
}
public PrefixTreeNode(String name, HashMap<String, PrefixTreeNode> nexts) {
Name = name;
this.nexts = nexts;
}
}
2)前缀树开发
前缀树其实只需要有一个节点,然后写出构建函数和遍历函数,基本上就可以使用了
①前缀树的构建是将输入的目录字符串通过"/"进行分解,得到字符串数组。
在分解之前就可以判断一下是不是最小目录,如果最后不是以/结尾,那么到文件所在的目录就是最小目录。文件不用加入前缀树。
②前缀树的遍历就是树的正常遍历,用DFS(Depth First Search)比较容易做,遍历每一个节点,如果有文件数和文件大小就输出,没有就去下一层,直到没有下一层
前缀树的代码如下所示:
/**
* @author sq
* @date 2023/8/28
* @Description ${}
*/
import java.io.FileWriter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.List;
public class PrefixTree {
PrefixTreeNode root = null;
long sum_num = 0l;
long sum_size = 0l;
//构建前缀树
public PrefixTree() {
}
public PrefixTree(PrefixTreeNode root) {
this.root = root;
}
public PrefixTreeNode getRoot() {
return root;
}
public void setRoot(PrefixTreeNode root) {
this.root = root;
}
public long getSum_num() {
return sum_num;
}
public void setSum_num(long sum_num) {
this.sum_num = sum_num;
}
public long getSum_size() {
return sum_size;
}
public void setSum_size(long sum_size) {
this.sum_size = sum_size;
}
//构建前缀树函数
public void InsertToPrefixTree(String filePath, long fileSize, PrefixTreeNode root) throws UnsupportedEncodingException {
//0. size如果为0 则为目录(可优化的点)
//1.对filePath进行URL解码
String decodedFilePath = URLDecoder.decode(filePath, "UTF-8");
//2.识别filePath是否以“/”结尾,如果是说明该路径仅为目录,没有文件对象。
boolean contains_file_flag = false;
if (!decodedFilePath.endsWith("/")) {
contains_file_flag = true;
}
//3.将目录进行拆分
String[] split = decodedFilePath.split("/");//“/”
//4.对split数组进行遍历,构建前缀树
PrefixTreeNode head = root;
if (split.length == 1 && contains_file_flag == true) {
//当该函数为文件节点时,不加入目录的前缀树,但对当前节点的file_num与file_size进行修改
head.file_num++;
head.file_size += fileSize;
return;
}
//5.如果是目录则安好一般情况处理
for (int i = 0; i < split.length; i++) {
if (head.nexts == null) {
//初始化一个hashmap
head.nexts = new HashMap<>();
}
if (!head.nexts.containsKey(split[i])) {
//若前缀树中不存在该节点,加入该节点
PrefixTreeNode newNode = new PrefixTreeNode();
head.nexts.put(split[i], newNode);
}
//获取下一个节点对该节点进行一些操作
//将该节点名称进行设置
head.nexts.get(split[i]).Name = split[i];
//判断该目录的下一个节点是不是文件对象,如果是,则该节点为一个最小目录
if (i + 1 == split.length - 1 && contains_file_flag == true) {
head.nexts.get(split[i]).file_num++;
head.nexts.get(split[i]).file_size += fileSize;
break;//不需要加入叶子节点
}
//去遍历下一个节点
head = head.nexts.get(split[i]);
}
}
//遍历前缀树函数
public void TraversePrefixTreeWriteToFile(PrefixTreeNode root, StringBuffer Name, FileWriter writer) throws IOException {
//遍历节点的nexts
if (root.nexts == null) {
//如果该目录下没有nexts,则直接返回
return;
}
HashMap<String, PrefixTreeNode> nexts = root.nexts;
for (String s : nexts.keySet()) {
StringBuffer directoryName = new StringBuffer(Name);
if (!String.valueOf(directoryName).equals("")) {
directoryName.append("/");
}
directoryName.append(s);
//1.如果该目录下包含文件,则输出该目录上级所有目录,并输出该目录下filenum: file_size
if (nexts.get(s).file_num != 0) {
//2.将结果写入文件,去掉最开头的/
writer.write(directoryName + "," + nexts.get(s).file_num + "," + String.valueOf(nexts.get(s).file_size));
writer.write("\n");
System.out.println(directoryName + "," + nexts.get(s).file_num + "," + String.valueOf(nexts.get(s).file_size));
}
//3.以该节点为根节点进行遍历
TraversePrefixTreeWriteToFile(nexts.get(s), directoryName, writer);
}
}
public void TraversePrefixTree(PrefixTreeNode root, StringBuffer Name) {
//遍历节点的nexts
if (root.nexts == null) {
//如果该目录下没有nexts,则直接返回
return;
}
HashMap<String, PrefixTreeNode> nexts = root.nexts;
for (String s : nexts.keySet()) {
StringBuffer directoryName = new StringBuffer(Name);
if (!String.valueOf(directoryName).equals("")) {
directoryName.append("/");
}
directoryName.append(s);
//1.如果该目录下包含文件,则输出该目录上级所有目录,并输出该目录下filenum: file_size
if (nexts.get(s).file_num != 0) {
//2.将结果写入文件,去掉最开头的/
System.out.println(directoryName + "/ 目录包含" + String.valueOf(nexts.get(s).file_num) + "个文件,总大小:" + String.valueOf((double) nexts.get(s).file_size / 1024) + " MB");
}
//3.以该节点为根节点进行遍历
TraversePrefixTree(nexts.get(s), directoryName);
}
}
public void TraversePrefixTreeValid(PrefixTreeNode root, StringBuffer Name) throws IOException {
//遍历节点的nexts
if (root.nexts == null) {
//如果该目录下没有nexts,则直接返回
return;
}
HashMap<String, PrefixTreeNode> nexts = root.nexts;
for (String s : nexts.keySet()) {
StringBuffer directoryName = new StringBuffer(Name);
if (!String.valueOf(directoryName).equals("")) {
directoryName.append("/");
}
directoryName.append(s);
//1.如果该目录下包含文件,则输出该目录上级所有目录,并输出该目录下filenum: file_size
if (nexts.get(s).file_num != 0) {
//2.将结果写入文件,去掉最开头的/
sum_num += nexts.get(s).file_num;
sum_size += nexts.get(s).file_size;
System.out.println(directoryName + "/ 目录包含" + String.valueOf(nexts.get(s).file_num) + "个文件,总大小:" + String.valueOf((double) nexts.get(s).file_size / 1024) + " MB");
}
//3.以该节点为根节点进行遍历
TraversePrefixTreeValid(nexts.get(s), directoryName);
}
}
}
3)对所构建的前缀树进行测试
从csv文件中读取每一行的目录名,和文件大小,按行调用前缀树的构建。总体代码如下:
import com.obs.prefixTree.PrefixTree;
import com.obs.prefixTree.PrefixTreeNode;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
/**
* @author sq
* @date 2023/8/28
* @Description ${}
*/
public class PrefixTreeBuilderfromCsvTest {
public static void main(String[] args) throws IOException {
//获取要处理的桶清单文件以及要写入的文件
String csvFile = "0000018A3A73BC14454759A9F377424D_1.csv";
String fileName = "result.csv";
//1.初始化前缀树
//1.1创建一颗只有根节点的树
PrefixTree tree=new PrefixTree(new PrefixTreeNode("",null));
//2.按行遍历csv文件
String line = "";
String csvSplitBy = ",";
boolean header_flag = true;
int key_index = -1;
int size_index = -1;
int bucket_index=-1;
try (BufferedReader br = new BufferedReader(new FileReader(csvFile))) {
while ((line = br.readLine()) != null) {
String[] data = line.split(csvSplitBy);
if (header_flag) {
for (int i=0;i<data.length;i++) {
if(data[i].equals("Bucket")){
bucket_index=i;
}
if(data[i].equals("Key")){
key_index=i;
}
if(data[i].equals("Size")){
size_index=i;
}
}
header_flag = false;
continue;
}
tree.getRoot().setName(data[bucket_index]);
//如果不是第一行,则按照正常数据处理构造前缀树
tree.InsertToPrefixTree(data[key_index],Long.parseLong(data[size_index]),tree.getRoot());
}
} catch (IOException e) {
e.printStackTrace();
}
//3.遍历前缀树
FileWriter writer = new FileWriter(fileName);
//3.1加入表头
writer.write("Directory" +","+ "FileNumber"+","+"FileSize"); // 写入内容
writer.write("\n"); // 换行
//3.2记录根节点对象数,对象大小
//3.2.1写入到文件
writer.write(tree.getRoot().getName() +","+ tree.getRoot().getFile_num()+","+tree.getRoot().getFile_size()); // 写入内容
writer.write("\n"); // 换行
//3.2.2输出到控制台
System.out.println(tree.getRoot().getName() +","+ tree.getRoot().getFile_num()+","+tree.getRoot().getFile_size());
//3.3 遍历前缀树写入文件
tree.TraversePrefixTreeWriteToFile(tree.getRoot(), new StringBuffer(tree.getRoot().getName()), writer);
//4.关闭写入流
writer.close();
}
}
最后想说一下,树的构建和遍历都不要死记硬背,隶属与递归的问题,都可以使用自然智慧,在尝试中得到普遍逻辑。