您的位置:首页 > 编程语言 > Java开发

结巴分词JAVA版本源码解析(一)

2018-01-09 01:20 1581 查看
话不多说,有了之前的铺垫直接上代码,代码我加了很多自己的注释,应该是比较清楚了,基本分为三个层次来讲,这一部分设计到第一步,即为,预处理,包括初始化等,是分词的第一步,基本包括词典词库的载入,字典树词典的初始化以及填充等工作,生成字典树,还有一点我的代码顺序基本是按照调用顺序来的,重点理解的地方我会标红
1.初始化调用不用多讲,通过初始化调用,及构造方法中的调用,载入用户和搜狗细胞词库,这里值得注意一点的是,实例的产生作者用了线程安全的单例模式,这也就是说在多线程调用的情况下不会出现线程安全的问题
protected void setUp() throws Exception {
WordDictionary.getInstance().init(Paths.get("conf"));
}
public void init(Path configFile) {
String abspath = configFile.toAbsolutePath().toString();
System.out.println("initialize user dictionary:" + abspath);
synchronized (WordDictionary.class) {
if (loadedPath.contains(abspath))
return;

DirectoryStream<Path> stream;
try {
stream = Files.newDirectoryStream(configFile, String.format(Locale.getDefault(), "*%s", USER_DICT_SUFFIX));
for (Path path: stream){
System.err.println(String.format(Locale.getDefault(), "loading dict %s", path.toString()));
singleton.loadUserDict(path);
}
loadedPath.add(abspath);
} catch (IOException e) {
// TODO Auto-generated catch block
// e.printStackTrace();
System.err.println(String.format(Locale.getDefault(), "%s: load user dict failure!", configFile.toString()));
}
}
}

//构造方法加载词典
private WordDictionary() {
this.loadDict();
}

public static WordDictionary getInstance() {
if (singleton == null) {
synchronized (WordDictionary.class) {
if (singleton == null) {
singleton = new WordDictionary();
return singleton;
}
}
}
return singleton;
}

public void loadUserDict(Path userDict) {
loadUserDict(userDict, StandardCharsets.UTF_8);
}

//加载用户词典和细胞词库,初始化时完成
public void loadUserDict(Path userDict, Charset charset) {
try {
BufferedReader br = Files.newBufferedReader(userDict, charset);
long s = System.currentTimeMillis();
int count = 0;
while (br.ready()) {
String line = br.readLine();
String[] tokens = line.split("[\t ]+");//匹配制表符号一次或多次,然后每行切分

if (tokens.length < 1) {
// Ignore empty line
continue;
}

String word = tokens[0];//前一位置为词,后一个位置是词的频度

double freq = 3.0d;//默认为3
if (tokens.length == 2)
freq = Double.valueOf(tokens[1]);
word = addWord(word);
freqs.put(word, Math.log(freq / total));//对频率取log,放在map结构中
count++;
}
System.out.println(String.format(Locale.getDefault(), "user dict %s load finished, tot words:%d, time elapsed:%dms", userDict.toString(), count, System.currentTimeMillis() - s));
br.close();
}
catch (IOException e) {
System.err.println(String.format(Locale.getDefault(), "%s: load user dict failure!", userDict.toString()));
}
}
2.填词,Dictsegment表示字典树的一个分支,也可以理解为一个节点,存储字符,节点的子节点采用数组(DictSegment[])或map(Map(Character, DictSegment))存储,选用标准根据子节点的数量而定。如果子节点的数量小于等于ARRAY_LENGTH_LIMIT,采用数组存储;如果子节点的数量大于ARRAY_LENGTH_LIMIT,采用Map存储。ARRAY_LENGTH_LIMIT默认为3。这样做好处在于,节省内存空间。因为HashMap的方式的方式,肯定是需要预先分配内存的,就可能会存在浪费的现象,但如果全都采用数组去存组(后续采用二分的方式查找),你就无法获得O(1)的算法复杂度。所以这里采用了两者方式,当子节点数很少时,用数组存储,当子结点数较多时候,则全部迁至hashMap中去。在构建过程中,会将每个词一步步地加入到字典树中,这是一个递归的过程
private String addWord(String word) {
if (null != word && !"".equals(word.trim())) {
String key = word.trim().toLowerCase(Locale.getDefault());
_dict.fillSegment(key.toCharArray());
return key;
}
else
return null;
}
private synchronized void fillSegment(char[] charArray, int begin, int length, int enabled){
// 获取字典表中的汉字对象
Character beginChar = new Character(charArray[begin]);
Character keyChar = charMap.get(beginChar);//用以检验字典表中是否含有该中文字符
// 字典中没有该字,则将其添加入字典
if (keyChar == null) {//填入词中keychar和beginchar相同
charMap.put(beginChar, beginChar);
keyChar = beginChar;
}

// 搜索当前节点的存储,查询对应keyChar的keyChar,如果没有则创建
DictSegment ds = lookforSegment(keyChar, enabled);
if (ds != null) {
// 处理keyChar对应的segment
if (length > 1) {
// 词元还没有完全加入词典树
ds.fillSegment(charArray, begin + 1, length - 1, enabled);
}
else if (length == 1) {
// 已经是词元的最后一个char,设置当前节点状态为enabled,
// enabled=1表明一个完整的词,enabled=0表示从词典中屏蔽当前词
ds.nodeState = enabled;
}
}

}

private DictSegment lookforSegment(Character keyChar, int create) {//在分支中查找

DictSegment ds = null;

if (this.storeSize <= ARRAY_LENGTH_LIMIT) {
// 获取数组容器,如果数组未创建则创建数组
DictSegment[] segmentArray = getChildrenArray();
// 搜寻数组
DictSegment keySegment = new DictSegment(keyChar);
//由于二分查找,数组需要有序从而下面排序
int position = Arrays.binarySearch(segmentArray, 0, this.storeSize, keySegment);
if (position >= 0) {
ds = segmentArray[position];
}

// 遍历数组后没有找到对应的segment
if (ds == null && create == 1) {
ds = keySegment;
if (this.storeSize < ARRAY_LENGTH_LIMIT) {
// 数组容量未满,使用数组存储
segmentArray[this.storeSize] = ds;
// segment数目+1
this.storeSize++;
Arrays.sort(segmentArray, 0, this.storeSize);

}
else {
// 数组容量已满,切换Map存储
// 获取Map容器,如果Map未创建,则创建Map
Map<Character, DictSegment> segmentMap = getChildrenMap();
// 将数组中的segment迁移到Map中
migrate(segmentArray, segmentMap);
// 存储新的segment
segmentMap.put(keyChar, ds);
// segment数目+1 , 必须在释放数组前执行storeSize++ , 确保极端情况下,不会取到空的数组
this.storeSize++;
// 释放当前的数组引用
this.childrenArray = null;
}

}

}
else {
// 获取Map容器,如果Map未创建,则创建Map
Map<Character, DictSegment> segmentMap = getChildrenMap();
// 搜索Map
ds = (DictSegment) segmentMap.get(keyChar);
if (ds == null && create == 1) {
// 构造新的segment
ds = new DictSegment(keyChar);
segmentMap.put(keyChar, ds);
// 当前节点存储segment数目+1
this.storeSize++;
}
}

return ds;
}


3.由于要进行排序,作者重写了比较器public int compareTo(DictSegment o) {
// 对当前节点存储的char进行比较
return this.nodeChar.compareTo(o.nodeChar);
}
这样第一步基本完成了,我们加载了细胞词库,用户词库,作者训练的语料库,并生成了对应的字典树结构,字典树中,采用两种方式存储分支节点,第一步大功告成还是比较简单的
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: