Find in Large File
0. 来源
最近有学弟问我如何从一个大文件中查找,借自己的一些经验,立刻就认为有两种选择:
- 文件稍微小一点的话,使用
Map
就可以了,把 ++label++ 放到key
中,而value
放的是 ++content++; - 文件大一点的话,直接使用数据库吧;
为什么会有这样的想法呢?因为我之前就是这样做的~可是,现在我会怎么做呢?
于是,我做了几个实验想把 大文件查询 做的更优雅些。故,总结于此~~
1. 问题描述
// size of file: 4.55 GB
// source of file: CSDN 用户画像
// one line format: label title content
// problem: find title and content by label given
// for example: D0663128STL中栈和队列的使用方法stl 中优先队列的使用方法...
// label: D0663128
// title: STL中栈和队列的使用方法
// content: stl 中优先队列的使用方法...
// problem: find 'STL中栈和队列的使用方法' and 'stl 中优先队列的使用方法...' by 'D0663128'
2. 想到的几种方法
2.1 顺序查找
顺序查找 是最无脑的方法: ++一个一个的查找,直到找到。并且,当查找下一个时必须要重写开始。++但是,这种方法,有唯一的好处就是不会:OutOfMemoryError
。因为,如果编码正确的话,它会把读过的 Garbage collection
。
2.2 还是使用 Map
初始想法
首先进行预处理,读取所有的文件内容,并且把每个 label title&content
put
在Map
的 key value 中。这样,查找的时候直接get
就可以了。而且,java
还支持 序列化。也就是,我们可以把Map
对象保存在文件中。++以后直接读取序列化得到的文件就可以了,而不需要重新进行预处理。++改进
一开始,我没想到文件还挺大的。若
Map
保存所有的文件内容,那肯定会OutOfMemoryError
。于是,我做出如下改进:1) 还是使用
Map
,其中key
依旧保存的为 label,但是value
保存的是 索引。怎么理解这个索引?
2) 其实 ++索引++ 想法来源于随机读取,即 Java 中的RandomAccessFile
。这里索引由两部分组成:开始位置_结束位置。- 开始位置:即
label
对应的内容的开始处相对于文件的偏移。 - 结束位置:即
label
对应的内容的结束处相对于文件的偏移。
3) 有了这两个 位置,使用RandomAccessFile
可以很快的读取到所要获取的内容;
value
保存的是 索引 后,Map
对象所占的内存空间会小很多,故,就不存在OutOfMemoryError
问题了。经过实验,此时序列化的 Map 对象的文件大小为:32.9 MB 。
此时,查询的时间瓶颈也就落在了:++读取 Map 对象序列化文件,恢复 Map 上了++ 。- 开始位置:即
2.3 使用 B-Tree
首先了解
B-Tree
,详见下面的博客(讲的不错)
B-Tree 详解我的实现
这学期,我在读 Introduction to Algorithms 并实现了上面的一些算法。(GitHub)。其中,也包括
B-Tree
。
但是,我实现的版本没有考虑:++把结点保存到硬盘中++,也就是最后生成的B-Tree
完全在内存中。现在看起来,真艹,这把B-Tree
阉割的不像样,这完全丢掉了它的优点。原本,我打算完全重写,把它的每个结点操作都映射到硬盘上。但,由于时间因素,我只做了如下修改:
- 生成
B-Tree
还是在内存中(没变); B-Tree
构造完成后,我把它的每个结点都保存到硬盘中;- 重写
search
方法,使它从硬盘中的B-Tree
中找;
为什么这么修改?因为,++修改后我完全可以模拟真正
B-Tree
的查找过程++,这样就能分析它的性能了。- 生成
2.4 使用 数据库
针对这个方法,我没有做实验。因为,我发现,要把这个大家伙(文件)导入 MySQL
需要 1~2 小时。坑~
3. 比较
方法一、四就不用说了。这里主要是对方法二、三的对比。
Null | Map | B-Tree |
---|---|---|
预 处 理 | 58.213 s | 88.834 s |
加载时间 | 10.069 s | 0 s |
查询时间 | 10.082 s | 0.202 s |
说明
- 预处理时间:分别指的是
Map
和B-Tree
内存构造 + 保存到硬盘中的时间;
这里 B-Tree 会生成 5720 个文件(因为有 5720 个结点),每个文件的大小平均为 5kb ,所以会慢一点;
加载时间:
Map
指的是从序列化文件恢复到内存的时间。而,B-Tree
为 0s。这里 B-Tree 设为 0 s,是因为它每次查询最多会读三个(因为有三层)大小约为 6kb 的文件(读的这些文件正是需要查询的结点),而我把这些时间都归为 查询时间 了。
查询时间:这里是查询 4个 label 对应 title&content 的时间;
需要说明的是,Map 的查询时间包含了它的 加载时间。
- 预处理时间:分别指的是
结论
我们可以很容易得出,当只查询几个时,
B-Tree
很快,只需要零点几秒。但是,当查询成千上万次时,Map
的效果会越来越好。因为,多次查询会把Map
的加载时间平摊 。
4. source code
Find.java
/**
* @author INotWant.
*/
public interface Find {
/**
* @param label 标号
* @return 标号对应的内容
*/
String findContent(String label);
}
FindInLargeFile.java
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author INotWant.
*/
public class FindInLargeFile {
public static String FILE_PATH = "data/largeFile.txt";
/**
* every find start again
*/
public static class OrderFind implements Find {
@Override
public String findContent(String label) {
String content = "";
try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) {
String line = reader.readLine();
while (line != null) {
String[] split = line.split("\u0001");
if (label.equals(split[0]))
return split[1];
line = reader.readLine();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return content;
}
}
public static class FindUseMap implements Find {
private Map<String, String> map = new HashMap<>();
private final static String OBJECT_FILE = "data/map";
@SuppressWarnings("unchecked")
@Override
public String findContent(String label) {
// if size of map equal 0, initialize map
// map save the start byte of line and the end byte of line for label
File objectFile = new File(OBJECT_FILE);
if (objectFile.exists() && map.size() == 0) {
long startTime = System.currentTimeMillis();
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(objectFile))) {
map = (Map<String, String>) ois.readObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
long endTime = System.currentTimeMillis();
System.out.println("load ::" + (endTime - startTime) / 1000.);
}
if (map.size() == 0) {
try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH));
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(OBJECT_FILE))) {
String line = reader.readLine();
long lineStart = 0;
while (line != null) {
long lineEnd = lineStart + line.getBytes().length - 1;
String[] split = line.split("\u0001");
map.put(split[0], String.valueOf(lineStart) + "_" + String.valueOf(lineEnd));
lineStart = lineEnd + 3;
line = reader.readLine();
}
oos.writeObject(map);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// find label in map
String[] split = map.get(label).split("_");
long lineStart = Long.parseLong(split[0]);
long lineEnd = Long.parseLong(split[1]);
try (RandomAccessFile randomAccessFile = new RandomAccessFile(FILE_PATH, "r")) {
randomAccessFile.seek(lineStart);
byte[] bytes = new byte[(int) (lineEnd - lineStart + 1)];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = randomAccessFile.readByte();
}
return new String(bytes).split("\u0001")[1];
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public static class FindUseBTree implements Find {
// size of degree is determined by block of disk
private BTree<String, String> bTree = new BTree<>(128);
private final static String SAVE_PATH = "data/BTree/";
public FindUseBTree() {
File file = new File(SAVE_PATH);
if (file.exists() && file.isDirectory() && file.listFiles() != null && file.listFiles().length > 0)
return;
createBTree();
saveBTree();
// for releasing memory
bTree = null;
}
private void createBTree() {
try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) {
String line;
long lineStart = 0;
while ((line = reader.readLine()) != null) {
long lineEnd = lineStart + line.getBytes().length - 1;
String[] split = line.split("\u0001");
bTree.insert(split[0], String.valueOf(lineStart) + "_" + String.valueOf(lineEnd));
lineStart = lineEnd + 3;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// file's name show layer & position in layer. number of layer or position start from 1.
// for example, 1_1_1: root,
// 3_1_2: 3nd layer and 3th position coming from 2th layer's 1th position
// x_y_z: 当前为 x 层的,且父结点为 x-1 层第 y 处结点的,第 z 个
private void saveBTree() {
/*
int layerNum = 1;
BTree.BNode<String, String> root = bTree.getRoot();
List<BTree.BNode<String, String>> layerList = new ArrayList<>();
layerList.add(root);
while (layerList.size() > 0) {
List<BTree.BNode<String, String>> newLayerList = new ArrayList<>();
for (int i = 0; i < layerList.size(); i++) {
if (!layerList.get(i).isLeaf())
newLayerList.addAll(layerList.get(i).getChildren());
saveNode(layerNum, i + 1, layerList.get(i));
}
++layerNum;
layerList = newLayerList;
}
*/
int layerNum = 1;
BTree.BNode<String, String> root = bTree.getRoot();
List<List<BTree.BNode<String, String>>> layerListList = new ArrayList<>();
List<BTree.BNode<String, String>> layerList = new ArrayList<>();
layerList.add(root);
layerListList.add(layerList);
while (layerListList.size() > 0) {
List<List<BTree.BNode<String, String>>> newLayerListList = new ArrayList<>();
for (int i = 0; i < layerListList.size(); i++) {
layerList = layerListList.get(i);
for (int j = 0; j < layerList.size(); j++) {
if (!layerList.get(j).isLeaf())
newLayerListList.add(layerList.get(j).getChildren());
saveNode(layerNum, (i + 1) + "_" + (j + 1), layerList.get(j));
}
}
layerListList = newLayerListList;
++layerNum;
}
}
private void saveNode(int layerNum, String posNum, BTree.BNode<String, String> node) {
String fileName = SAVE_PATH + String.valueOf(layerNum) + "_" + posNum;
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName))) {
node.setChildren(null);
oos.writeObject(node);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
private BTree.BNode<String, String> readNode(String pLabel) {
String fileName = SAVE_PATH + pLabel;
BTree.BNode<String, String> bNode = null;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName))) {
bNode = (BTree.BNode<String, String>) ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
return bNode;
}
@Override
public String findContent(String label) {
int layer = 1;
String pLabel = "1_1_1";
String pContent = null; // position of content in file
while (pContent == null) {
BTree.BNode<String, String> bNode = readNode(pLabel);
List<String> keys = bNode.getKeys();
int count = 1;
for (int i = 0; i < keys.size(); i++) {
if (keys.get(i).equals(label)) {
pContent = bNode.getValues().get(i);
break;
}
if (label.compareTo(keys.get(i)) > 0)
++count;
if (label.compareTo(keys.get(i)) < 0)
break;
}
++layer;
pLabel = layer + "_" + pLabel.split("_")[2] + "_" + count;
}
return getContent(pContent);
}
private String getContent(String pContent) {
String[] split = pContent.split("_");
long lineStart = Long.parseLong(split[0]);
long lineEnd = Long.parseLong(split[1]);
try (RandomAccessFile randomAccessFile = new RandomAccessFile(FILE_PATH, "r")) {
randomAccessFile.seek(lineStart);
byte[] bytes = new byte[(int) (lineEnd - lineStart + 1)];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = randomAccessFile.readByte();
}
return new String(bytes).split("\u0001")[1];
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
FindInLargeFile.FILE_PATH = "data/blogContent.txt";
// TEST 1
/*
Timing timing1 = new Timing(new OrderFind());
Timing timing2 = new Timing(new FindUseMap());
timing1.countTime("D0934038");
timing2.countTime("D0934038");
// */
// TEST 2
long startTime = System.currentTimeMillis();
Timing timing = new Timing(new FindUseMap());
String[] labels = {"D0934038", "D0339360", "D0379964", "D0776559"};
timing.countTime(labels);
long endTime = System.currentTimeMillis();
System.out.println("预处理时间 :: " + (endTime - startTime) / 1000.);
// TEST 3
// long startTime = System.currentTimeMillis();
// FindUseBTree findUseBTree = new FindUseBTree();
// long endTime = System.currentTimeMillis();
// System.out.println("预处理时间 :: " + (endTime - startTime) / 1000.);
// TEST 4
// Timing timing = new Timing(new FindUseBTree());
// String[] labels = {"D0934038", "D0339360", "D0379964", "D0776559"};
// timing.countTime(labels);
// TEST 5
// Timing timing1 = new Timing(new FindUseBTree());
// String[] labels = {"D0934038", "D0339360", "D0379964", "D0776559"};
// timing1.countTime(labels);
// Timing timing2 = new Timing(new FindUseMap());
// timing2.countTime(labels);
}
}
Timing.java
import java.util.ArrayList;
import java.util.List;
/**
* @author INotWant.
*/
public class Timing {
private Find findProcess;
public Timing(Find findProcess) {
this.findProcess = findProcess;
}
public void countTime(String label) {
long start = System.currentTimeMillis();
String content = findProcess.findContent(label);
long endTime = System.currentTimeMillis();
System.out.println(label + " :: " + content);
System.out.println("spend time :: " + (endTime - start) / 1000.);
}
public void countTime(String... labels) {
long start = System.currentTimeMillis();
List<String> contents = new ArrayList<>();
for (String label : labels)
contents.add(findProcess.findContent(label));
long endTime = System.currentTimeMillis();
for (int i = 0; i < labels.length; i++) {
System.out.println(labels[i] + " :: " + contents.get(i));
}
System.out.println("spend time :: " + (endTime - start) / 1000.);
}
}
BTree.java
import java.io.Serializable;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* B 树
* 要点 1):返回一个不可修改的集合;
* 要点 2):规范小标的使用!
*
* @author INotWant.
*/
public class BTree<K extends Comparable<K>, V> {
private BNode<K, V> root;
private Integer degree; // 最小度数
public BTree(Integer degree) {
this.degree = degree;
}
public static class BNode<K, V> implements Serializable {
private List<K> keys = new LinkedList<>(); // 关键字集合
private List<V> values = new LinkedList<>(); // 关键字对应的数据
private List<BNode<K, V>> children = new LinkedList<>(); // 子孩子集合
private boolean isLeaf; // 是否为叶节点
private int n; // 关键字的个数
public int getN() {
return n;
}
public List<K> getKeys() {
// 返回一个不可以修改的 关键字集合
return Collections.unmodifiableList(keys);
}
public List<V> getValues() {
return Collections.unmodifiableList(values);
}
public List<BNode<K, V>> getChildren() {
return Collections.unmodifiableList(this.children);
}
public boolean isLeaf() {
return isLeaf;
}
public void setChildren(List<BNode<K, V>> children) {
this.children = children;
}
}
/**
* @return 创建一个 空B树
*/
public BNode<K, V> buildBTree() {
this.root = new BNode<>();
this.root.n = 0;
this.root.isLeaf = true;
return root;
}
/**
* @param key key
* @param value value
*/
public void insert(K key, V value) {
this.root = this.root == null ? buildBTree() : this.root;
if (this.root.n == 2 * degree - 1) {
BNode<K, V> nRoot = new BNode<>();
nRoot.n = 0;
nRoot.isLeaf = false;
nRoot.children.add(this.root);
this.root = nRoot;
split(nRoot, 0);
}
insertNoFull(this.root, key, value);
}
/**
* 分裂结点,添加时不会造成结点关键字大于 2*degree-1
* 【注】:分裂结点使得 B树 从根节点增高
*
* @param pNode 待“分裂”结点的父结点;
* @param c 待“分裂”结点所在父结点的位置;
*/
private void split(BNode<K, V> pNode, int c) {
BNode<K, V> node = pNode.children.get(c);
BNode<K, V> bNode = new BNode<>();
// 新结点度数
bNode.n = degree - 1;
// 新结点关键字以及子孩子
for (int i = 0; i < node.n; i++)
if (i >= degree) {
bNode.keys.add(node.keys.get(i));
bNode.values.add(node.values.get(i));
if (!node.isLeaf)
bNode.children.add(node.children.get(i));
}
if (!node.isLeaf)
bNode.children.add(node.children.get(node.children.size() - 1));
// 新结点是否为叶节点
bNode.isLeaf = node.isLeaf;
// 父结点修改
pNode.keys.add(c, node.keys.get(degree - 1));
pNode.values.add(c, node.values.get(degree - 1));
pNode.children.add(c + 1, bNode);
pNode.n += 1;
// 旧结点修改
for (int i = node.n - 1; i >= degree - 1; i--) {
node.keys.remove(i);
node.values.remove(i);
if (!node.isLeaf)
node.children.remove(i + 1);
}
node.n = degree - 1;
}
// 插入帮助方法,已确定 node 不会满
// 只在叶节点插入
private void insertNoFull(BNode<K, V> node, K key, V value) {
if (node.isLeaf) {
int i = 0;
for (; i < node.n; i++)
if (node.keys.get(i).compareTo(key) >= 0)
break;
node.keys.add(i, key);
node.values.add(i, value);
node.n++;
} else {
int i = 0;
for (; i < node.n; i++)
if (node.keys.get(i).compareTo(key) >= 0)
break;
BNode<K, V> cNode = node.children.get(i);
if (cNode.n == 2 * degree - 1) {
split(node, i);
if (node.keys.get(i).compareTo(key) < 0)
cNode = node.children.get(i + 1);
}
insertNoFull(cNode, key, value);
}
}
/**
* @param key 关键字
* @return 关键字对应的值
*/
public V search(K key) {
BNode<K, V> cNode = this.root;
while (cNode != null) {
int i = 0;
for (; i < cNode.n; i++) {
if (cNode.keys.get(i).compareTo(key) == 0)
return cNode.values.get(i);
else if (cNode.keys.get(i).compareTo(key) > 0)
break;
}
if (!cNode.isLeaf)
cNode = cNode.children.get(i);
else
cNode = null;
}
return null;
}
/**
* @param key 待删除的关键字
* @return 删除成功时,返回关键字对应的“数据”,否则返回 null
*/
public V delete(K key) {
return deleteHelp(this.root, key);
}
// 删除帮组类
private V deleteHelp(BNode<K, V> cNode, K key) {
while (cNode != null) {
int i = 0;
for (; i < cNode.n; i++) {
if (cNode.isLeaf && cNode.keys.get(i).compareTo(key) == 0) {
// 要删除的关键字在叶节点的情况
--cNode.n;
cNode.keys.remove(i);
return cNode.values.remove(i);
} else if (cNode.keys.get(i).compareTo(key) == 0) {
// 要删除的关键字在内部结点的情况
int b1 = i;
int b2 = i + 1;
if (cNode.children.get(b1).n >= degree) {
// 左兄弟结点关键字数大于等于 t 的情况
BNode<K, V> bNode = cNode.children.get(b1);
K rKey = bNode.keys.get(bNode.n - 1);
V rValue = bNode.values.get(bNode.n - 1);
deleteHelp(bNode, rKey);
V resultValue = cNode.values.get(i);
cNode.keys.set(i, rKey);
cNode.values.set(i, rValue);
return resultValue;
} else if (cNode.children.get(b2).n >= degree) {
// 右兄弟结点关键字数大于等于 t 的情况
BNode<K, V> bNode = cNode.children.get(b2);
K rKey = bNode.keys.get(0);
V rValue = bNode.values.get(0);
deleteHelp(bNode, rKey);
V resultValue = cNode.values.get(i);
cNode.keys.set(i, rKey);
cNode.values.set(i, rValue);
return resultValue;
} else {
// 相邻结点都不大于的情况,需要合并
if (cNode.n - 1 == 0)
this.root = cNode.children.get(0);
union(cNode, i);
return deleteHelp(cNode, key);
}
} else if (cNode.keys.get(i).compareTo(key) > 0)
break;
}
if (cNode.isLeaf)
return null;
else {
// 要下降,此时要判断下降至结点的关键字的个数
if (cNode.children.get(i).n >= degree)
cNode = cNode.children.get(i);
else {
// 所至结点关键字数 t-1 的情况
int b1 = i - 1;
int b2 = i + 1;
if (b1 >= 0 && cNode.children.get(b1).n >= degree) {
// 由其左兄弟输送
BNode<K, V> bNode = cNode.children.get(b1);
K rKey = bNode.keys.remove(bNode.n - 1);
V rValue = bNode.values.remove(bNode.n - 1);
BNode<K, V> rChild = null;
if (!bNode.isLeaf)
rChild = bNode.children.remove(bNode.n);
--bNode.n;
K moveKey = cNode.keys.get(i - 1);
V moveValue = cNode.values.get(i - 1);
cNode.keys.set(i - 1, rKey);
cNode.values.set(i - 1, rValue);
cNode = cNode.children.get(i);
((LinkedList<K>) cNode.keys).addFirst(moveKey);
((LinkedList<V>) cNode.values).addFirst(moveValue);
if (!bNode.isLeaf)
((LinkedList<BNode<K, V>>) cNode.children).addFirst(rChild);
++cNode.n;
} else if (b2 < cNode.children.size() && cNode.children.get(b2).n >= degree) {
// 由其右兄弟输送
BNode<K, V> bNode = cNode.children.get(b2);
K rKey = bNode.keys.remove(0);
V rValue = bNode.values.remove(0);
BNode<K, V> rChild = null;
if (!bNode.isLeaf)
rChild = bNode.children.remove(0);
--bNode.n;
K moveKey = cNode.keys.get(i);
V moveValue = cNode.values.get(i);
cNode.keys.set(i, rKey);
cNode.values.set(i, rValue);
cNode = cNode.children.get(i);
((LinkedList<K>) cNode.keys).addLast(moveKey);
((LinkedList<V>) cNode.values).addLast(moveValue);
if (!bNode.isLeaf)
((LinkedList<BNode<K, V>>) cNode.children).addLast(rChild);
++cNode.n;
} else {
int b = b1 >= 0 ? b1 : b2;
if (b == b1)
union(cNode, i - 1);
else
union(cNode, i);
}
}
}
}
return null;
}
// 合并 cNode 结点中 i 和 i+1 子结点至 i 结点上
private void union(BNode<K, V> cNode, int i) {
K moveKey = cNode.keys.remove(i);
V moveValue = cNode.values.remove(i);
BNode<K, V> rChild = cNode.children.remove(i + 1);
--cNode.n;
cNode = cNode.children.get(i);
cNode.keys.add(moveKey);
cNode.keys.addAll(rChild.getKeys());
cNode.values.add(moveValue);
cNode.values.addAll(rChild.getValues());
if (!rChild.isLeaf)
cNode.children.addAll(rChild.getChildren());
cNode.n += 1 + rChild.n;
}
public BNode<K, V> getRoot() {
return root;
}
public static void main(String[] args) {
System.out.println("--------- BEGIN ---------");
BTree<Character, String> bTree = new BTree<>(2);
bTree.insert('M', "Computer");
bTree.insert('D', "Hello");
bTree.insert('H', "Java");
bTree.insert('Q', "C++");
bTree.insert('C', "C");
bTree.insert('I', "SSH");
bTree.insert('A', "Compile");
bTree.insert('B', "Linux");
bTree.insert('T', "GitHub");
bTree.insert('E', "World");
bTree.insert('F', "Python");
bTree.insert('J', "OA");
bTree.insert('K', "CRM");
System.out.println(bTree.search('A'));
System.out.println(bTree.delete('Z'));
System.out.println("---------- END ----------");
}
}
【NOTE】 水平有限,不确保 BTree.java
不存在 BUG !!