Analysis 包分析
算法和数据结构分析 :
由于 Analysis 包比较简单 , 不详述了 !
算法 : 基于机械分词 1-gram,2-gram,HMM( 如果使用 ICTCLAS 接口的话 )
数据结构 : 部分源码用到了 Set ,HashTable,HashMap
认真理解 Token
Lucene 中的 Analysis 包专门用于完成对于索引文件的分词 .Lucene 中的 Token 是一个非常重要的概念 .
看一下其源码实现:
public final class Token {
String termText; // the text of the term
int startOffset; // start in source text
int endOffset; // end in source text
String type = "word"; // lexical type
private int positionIncrement = 1;
public Token(String text, int start, int end)
public Token(String text, int start, int end, String typ)
public void setPositionIncrement(int positionIncrement)
public int getPositionIncrement() { return positionIncrement; }
public final String termText() { return termText; }
public final int startOffset() { return startOffset; }
public void setStartOffset(int givenStartOffset)
public final int endOffset() { return endOffset; }
public void setEndOffset(int givenEndOffset)
public final String type() { return type; }
public String toString()
}
下面编一段代码来看一下
TestToken.java
package org.apache.lucene.analysis.test;
import org.apache.lucene.analysis.*;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import java.io.*;
public class TestToken
{
public static void main(String[] args)
{
String string = new String(" 我爱天大 , 但我更爱中国 ");
//Analyzer analyzer = new StandardAnalyzer();
Analyzer analyzer = new TjuChineseAnalyzer();
//Analyzer analyzer= new StopAnalyzer();
TokenStream ts = analyzer.tokenStream("dummy",new StringReader(string));
Token token;
try
{
int n=0;
while ( (token = ts.next()) != null)
{
System.out.println((n++)+"->"+token.toString());
}
}
catch(IOException ioe)
{
ioe.printStackTrace();
}
}
} 注意看其结果如下所示
0->Token's (termText,startOffset,endOffset,type,positionIncrement) is:( 我 ,0,1,<CJK>,1)
1->Token's (termText,startOffset,endOffset,type,positionIncrement) is:( 爱 ,1,2,<CJK>,1)
2->Token's (termText,startOffset,endOffset,type,positionIncrement) is:( 天 ,2,3,<CJK>,1)
3->Token's (termText,startOffset,endOffset,type,positionIncrement) is:( 大 ,3,4,<CJK>,1)
4->Token's (termText,startOffset,endOffset,type,positionIncrement) is:( 但 ,5,6,<CJK>,1)
5->Token's (termText,startOffset,endOffset,type,positionIncrement) is:( 我 ,6,7,<CJK>,1)
6->Token's (termText,startOffset,endOffset,type,positionIncrement) is:( 更 ,7,8,<CJK>,1)
7->Token's (termText,startOffset,endOffset,type,positionIncrement) is:( 爱 ,8,9,<CJK>,1)
8->Token's (termText,startOffset,endOffset,type,positionIncrement) is:( 中 ,9,10,<CJK>,1)
9->Token's (termText,startOffset,endOffset,type,positionIncrement) is:( 国 ,10,11,<CJK>,1)
注意 : 其中 ”,” 被 StandardAnalyzer 给过滤掉了 , 所以大家注意第 4 个 Token 直接 startOffset 从 5 开始 .
如果改用 StopAnalyzer()
0->Token's (termText,startOffset,endOffset,type,positionIncrement) is:( 我爱天大 ,0,4,word,1)
1->Token's (termText,startOffset,endOffset,type,positionIncrement) is:( 但我更爱中国 ,5,11,word,1)
改用 TjuChineseAnalyzer( 我写的 , 下文会讲到如何去写 )
0->Token's (termText,startOffset,endOffset,type,positionIncrement) is:( 爱 ,3,4,word,1)
1->Token's (termText,startOffset,endOffset,type,positionIncrement) is:( 天大 ,6,8,word,1)
2->Token's (termText,startOffset,endOffset,type,positionIncrement) is:( 更 ,19,20,word,1)
3->Token's (termText,startOffset,endOffset,type,positionIncrement) is:( 爱 ,22,23,word,1)
4->Token's (termText,startOffset,endOffset,type,positionIncrement) is:( 中国 ,25,27,word,1)
讲明白了 Token, 咱们来看以下其他的东西
一个 TokenStream 是用来走访 Token 的 iterator( 迭代器 )
看一下其源代码 :
public abstract class TokenStream {
public abstract Token next() throws IOException;
public void close() throws IOException {}
}
一个 Tokenizer , is-a TokenStream (派生自 TokenStream ),其输入为 Reader
看一下其源码如下:
public abstract class Tokenizer extends TokenStream {
protected Reader input;
protected Tokenizer() {}
protected Tokenizer(Reader input) {
this.input = input;
}
public void close() throws IOException {
input.close();
}
}
一个 TokenFilter is–a TokenStream( 派生自 TokenStream) ,其义如名就是用来完成对 TokenStream 的过滤操作,譬如
去 StopWords ,将 Token 变为小写等。
源码如下:
public abstract class TokenFilter extends TokenStream {
protected TokenStream input;
protected TokenFilter() {}
protected TokenFilter(TokenStream input) {
this.input = input;
}
public void close() throws IOException {
input.close();
}
}
一个 Analyzer 就是一个 TokenStream 工厂
看一下其源码就:
public abstract class Analyzer {
public TokenStream tokenStream(String fieldName, Reader reader)
{
return tokenStream(reader);
}
public TokenStream tokenStream(Reader reader)
{
return tokenStream(null, reader);
}
}
好,现在咱们来看一下 Lucene 的 Analysis 包下面的各个类文件都是用来干什么的。按照字典排序。
Analysis 包中的源码详解
Analyzer.java 上文已经讲过。
CharTokenizer.java 此类为简单一个抽象类,用来对基于字符的进行简单分词( tokenizer )
LetterTokenizer.java 两个非字符之间的字符串定义为 token (举例来说英文单词由空白隔开,那个两个空白之间的字符串即被定义为一个 token 。备注:对于绝大多数欧洲语言来说,这个类工作效能很好。当时对于不用空白符分割的亚洲语言 , 效能极差(譬如中日韩)。)
LowerCaseFilter.java is-a TokenFilter 用于将字母小写化
LowerCaseTokenizer is-a Tokenizer 功能上等价于 LetterTokenizer + LowerCaseFilter
PerFieldAnalyzerWrapper 是一个 Analyzer ,因为继承自 Analyzer 当不同的域( Field )需要不同的语言分析器( Analyzer )时,这个 Analyzer 就派上了用场。使用成员函数 addAnalyzer 可以增加一个非缺省的基于某个 Field 的 analyzer 。很少使用。
PorterStemFilter.java 使用词干抽取算法对每一个 token 流进行词干抽取。
PorterStemmer.java 有名的 P-stemming 算法
SimpleAnalyzer.java
StopAnalyzer.java 具有过滤停用词的功能
StopFilter.java StopFilter 为一个 Filter ,主要用于从 token 流中去除 StopWords
Token.java 上面已讲 .
TokenFilter.java 上面已经讲了
Tokenizer.java 上面已经讲了
TokenStream.java 上面已经讲了
WhitespaceAnalyzer.java
WhitespaceTokenizer.java 只是按照 space 区分 Token.
由于 Lucene 的 analyisis 包下的 Standard 包下的 StandardAnalyzer() 功能很强大 , 而且支持 CJK 分词 , 我们简要说一下 .
此包下的文件是有 StandardTokenizer.jj 经过 javac 命令生成的 . 由于是机器自动生成的代码 , 可能可读性很差 , 想了解的话好好看看那个 StandardTokenizer.jj 文件就会比较明了了 .
Lucene 常用的 Analyzer 功能概述 .
WhitespaceAnalyzer: 仅仅是去除空格,对字符没有 lowcase 化 , 不支持中文
SimpleAnalyzer: 功能强于 WhitespaceAnalyzer, 将除去 letter 之外的符号全部过滤掉 , 并且将所有的字符 lowcase 化 , 不支持中文
StopAnalyzer:StopAnalyzer 的功能超越了 SimpleAnalyzer ,在 SimpleAnalyzer 的基础上
增加了去除 StopWords 的功能 , 不支持中文
StandardAnalyzer: 英文的处理能力同于 StopAnalyzer. 支持中文采用的方法为单字切分 .
ChineseAnalyzer: 来自于 Lucene 的 sand box. 性能类似于 StandardAnalyzer, 缺点是不支持中英文混和分词 .
CJKAnalyzer:chedong 写的 CJKAnalyzer 的功能在英文处理上的功能和 StandardAnalyzer 相同
但是在汉语的分词上,不能过滤掉标点符号,即使用二元切分
TjuChineseAnalyzer: 我写的 , 功能最为强大 .TjuChineseAnlyzer 的功能相当强大 , 在中文分词方面由于其调用的为 ICTCLAS 的 java 接口 . 所以其在中文方面性能上同与 ICTCLAS. 其在英文分词上采用了 Lucene 的 StopAnalyzer, 可以去除 stopWords, 而且可以不区分大小写 , 过滤掉各类标点符号 .
各个 Analyzer 的功能已经比较介绍完毕了 , 现在咱们应该学写 Analyzer, 如何 diy 自己的 analyzer 呢 ??
如何 DIY 一个 Analyzer
咱们写一个 Analyzer, 要求有一下功能
(1) 可以处理中文和英文 , 对于中文实现的是单字切分 , 对于英文实现的是以空格切分 .
(2) 对于英文部分要进行小写化 .
(3) 具有过滤功能 , 可以人工设定 StopWords 列表 . 如果不是人工设定 , 系统会给出默认的 StopWords 列表 .
(4) 使用 P-stemming 算法对于英文部分进行词缀处理 .
代码如下:
public final class DiyAnalyzer
extends Analyzer
{
private Set stopWords;
public static final String[] CHINESE_ENGLISH_STOP_WORDS =
{
"a", "an", "and", "are", "as", "at", "be", "but", "by",
"for", "if", "in", "into", "is", "it",
"no", "not", "of", "on", "or", "s", "such",
"t", "that", "the", "their", "then", "there", "these",
"they", "this", "to", "was", "will", "with",
"我", "我们"
};
public DiyAnalyzer()
{
this.stopWords=StopFilter.makeStopSet(CHINESE_ENGLISH_STOP_WORDS);
}
public DiyAnalyzer(String[] stopWordList)
{
this.stopWords=StopFilter.makeStopSet(stopWordList);
}
public TokenStream tokenStream(String fieldName, Reader reader)
{
TokenStream result = new StandardTokenizer(reader);
result = new LowerCaseFilter(result);
result = new StopFilter(result, stopWords);
result = new PorterStemFilter(result);
return result;
}
public static void main(String[] args)
{
//好像英文的结束符号标点.,StandardAnalyzer不能识别
String string = new String("我爱中国,我爱天津大学!I love China!Tianjin is a City");
Analyzer analyzer = new DiyAnalyzer();
TokenStream ts = analyzer.tokenStream("dummy", new StringReader(string));
Token token;
try
{
while ( (token = ts.next()) != null)
{
System.out.println(token.toString());
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}
可以看见其后的结果如下:
Token's (termText,startOffset,endOffset,type,positionIncrement) is:(爱,1,2,<CJK>,1)
Token's (termText,startOffset,endOffset,type,positionIncrement) is:(中,2,3,<CJK>,1)
Token's (termText,startOffset,endOffset,type,positionIncrement) is:(国,3,4,<CJK>,1)
Token's (termText,startOffset,endOffset,type,positionIncrement) is:(爱,6,7,<CJK>,1)
Token's (termText,startOffset,endOffset,type,positionIncrement) is:(天,7,8,<CJK>,1)
Token's (termText,startOffset,endOffset,type,positionIncrement) is:(津,8,9,<CJK>,1)
Token's (termText,startOffset,endOffset,type,positionIncrement) is:(大,9,10,<CJK>,1)
Token's (termText,startOffset,endOffset,type,positionIncrement) is:(学,10,11,<CJK>,1)
Token's (termText,startOffset,endOffset,type,positionIncrement) is:(i,12,13,<ALPHANUM>,1)
Token's (termText,startOffset,endOffset,type,positionIncrement) is:(love,14,18,<ALPHANUM>,1)
Token's (termText,startOffset,endOffset,type,positionIncrement) is:(china,19,24,<ALPHANUM>,1)
Token's (termText,startOffset,endOffset,type,positionIncrement) is:(tianjin,25,32,<ALPHANUM>,1)
Token's (termText,startOffset,endOffset,type,positionIncrement) is:(citi,39,43,<ALPHANUM>,1)
到此为止这个简单的但是功能强大的分词器就写完了,下面咱们可以尝试写一个功能更强大的分词器.
如何DIY一个功能更加强大Analyzer
譬如你有词典,然后你根据正向最大匹配法或者逆向最大匹配法写了一个分词方法,却想在Lucene中应用,很简单
你只要把他们包装成Lucene的TokenStream就好了.下边我以调用中科院写的ICTCLAS接口为例,进行演示.你去中科院
网站可以拿到此接口的free版本,谁叫你没钱呢,有钱,你就可以购买了.哈哈
好,由于ICTCLAS进行分词之后,在Java中,中间会以两个空格隔开!too easy,我们直接使用继承Lucene的
WhiteSpaceTokenizer就好了.
所以TjuChineseTokenizer 看起来像是这样.
public class TjuChineseTokenizer extends WhitespaceTokenizer
{
public TjuChineseTokenizer(Reader readerInput)
{
super(readerInput);
}
}
而TjuChineseAnalyzer看起来象是这样
public final class TjuChineseAnalyzer
extends Analyzer
{
private Set stopWords;
/** An array containing some common English words that are not usually useful
for searching. */
/*
public static final String[] CHINESE_ENGLISH_STOP_WORDS =
{
"a", "an", "and", "are", "as", "at", "be", "but", "by",
"for", "if", "in", "into", "is", "it",
"no", "not", "of", "on", "or", "s", "such",
"t", "that", "the", "their", "then", "there", "these",
"they", "this", "to", "was", "will", "with",
"我", "我们"
};
*/
/** Builds an analyzer which removes words in ENGLISH_STOP_WORDS. */
public TjuChineseAnalyzer()
{
stopWords = StopFilter.makeStopSet(StopWords.SMART_CHINESE_ENGLISH_STOP_WORDS);
}
/** Builds an analyzer which removes words in the provided array. */
//提供独自的stopwords
public TjuChineseAnalyzer(String[] stopWords)
{
this.stopWords = StopFilter.makeStopSet(stopWords);
}
/** Filters LowerCaseTokenizer with StopFilter. */
public TokenStream tokenStream(String fieldName, Reader reader)
{
try
{
ICTCLAS splitWord = new ICTCLAS();
String inputString = FileIO.readerToString(reader);
//分词中间加入了空格
String resultString = splitWord.paragraphProcess(inputString);
System.out.println(resultString);
TokenStream result = new TjuChineseTokenizer(new StringReader(resultString));
result = new LowerCaseFilter(result);
//使用stopWords进行过滤
result = new StopFilter(result, stopWords);
//使用p-stemming算法进行过滤
result = new PorterStemFilter(result);
return result;
}
catch (IOException e)
{
System.out.println("转换出错");
return null;
}
}
public static void main(String[] args)
{
String string = "我爱中国人民";
Analyzer analyzer = new TjuChineseAnalyzer();
TokenStream ts = analyzer.tokenStream("dummy", new StringReader(string));
Token token;
System.out.println("Tokens:");
try
{
int n=0;
while ( (token = ts.next()) != null)
{
System.out.println((n++)+"->"+token.toString());
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}对于此程序的输出接口可以看一下
0->Token's (termText,startOffset,endOffset,type,positionIncrement) is:(爱,3,4,word,1)
1->Token's (termText,startOffset,endOffset,type,positionIncrement) is:(中国,6,8,word,1)
2->Token's (termText,startOffset,endOffset,type,positionIncrement) is:(人民,10,12,word,1)
OK,经过这样一番讲解,你已经对Lucene的Analysis包认识的比较好了,当然如果你想更加了解,还是认真读读源码才好,
呵呵,源码说明一切!