2021SC@SDUSC
书接上文,根据分工,我负责对parser包中的源码进行解析,因此我将首先对parser包进行一个基本的概述,然后再来进行其中源码的分析。
一、parser包基本概述
如图所示,parser包内又有诸多子包,分别是charniak, common, dvparser, eval, lexparser, metrics, nndep, server, shiftreduce, tools, ui和webapp。从名字上不难看出,parser包内的代码支撑起了整个程序的语法解析功能。而其中名字诸如dvparser, lexparser的子包,则是存放了具体解析功能分支的功能模块包。其余子包名如common, ui, eval, webapp, tools, server的,则不难理解其存储源码的作用分别是常规源码、用户界面源码、评估源码、网页程序源码、工具源码以及服务器端源码。至于最后剩下的一些子包名字多是缩写,需要进一步分析才能得知其中源码的意义与作用。
二、common子包基本概述
首先,来看所谓的常规源码——即common子包内的源码。打开common子包,可以看到其中有9个java语言编写的源文件,通过IDEA的源文件图标不难看出,ParserGrammar是其中的抽象类,ParserQuery是一个接口,ParserQueryFactory则显然是ParserQuery的抽象工厂,而剩下的均是实现类。根据自顶向下的原则,我将首先分析ParserGrammar这个抽象类(ParserQuery和它的抽象类被囊括在内),然后再来分析其中的剩余实现类。
三、ParserGrammar抽象类源码分析
根据ParserGrammar抽象类的类声明来看,ParserGrammar抽象类实现了两个接口。实现的第一个接口是Function<List<? extends HasWord>, Tree>,第二个则是ParserQueryFactory。
public abstract class ParserGrammar implements Function<List<? extends HasWord>, Tree>, ParserQueryFactory {
第一个接口的语法看着有些难懂,显而易见这是一个语法糖,这里我们稍作解释。
List<?>:是一个泛型,在没有赋值前,表示可以接受任何类型的集合赋值,但赋值之后不能往里面随便添加元素,但可以remove和clear,并非immutable(不可变)集合。List<?>一般作为参数来接收外部集合,或者返回一个具体元素类型的集合,也称为通配符集合。
由此,我们可以知道实现的第一个接口是一个函数接口,他会接收一个List<? extends HasWord>类的对象作为输入,即输入一个元素均为继承自HasWord类的某一子类的对象的列表,然后输出一个Tree类的对象。那么实现了这一个函数接口的ParserGrammar抽象类自然也会实现这一方法。
第二个接口则是ParserQueryFactory接口,之前已经提到过这个接口是ParserQuery的一个抽象工厂,因此实现这个接口的ParserGrammar抽象类也自然会生产出这么一个ParserQuery的对象,这一点在后面可以看得出。
接下来分析该抽象类的具体定义,首先是两个成员变量logger和parserQuery。到这里,已经可以理解第二个接口是如何实现的了,通过这个接口对象作为一个公有抽象变量,这个抽象变量就可以在日后对ParserGrammar抽象类进行解析的时候具体实现这一个接口对象。而静态变量logger则是RedwoodChannels类的对象,这个类我目前还不了解,有待日后分析考证。
private static Redwood.RedwoodChannels logger = Redwood.channels(ParserGrammar.class);
public abstract ParserQuery parserQuery();
该抽象类的成员方法偏多,我们具体地一个个看。众多成员函数中,最重要的就是这个tokenize方法,它将输入的字符串句子通过TokenizerFactory的getTokenizer()方法进行处理,对其进行词法分析之后切分成具体的单词装进token列表中返回。
public List<? extends HasWord> tokenize(String sentence) {
TokenizerFactory<? extends HasWord> tf = treebankLanguagePack().getTokenizerFactory();
Tokenizer<? extends HasWord> tokenizer = tf.getTokenizer(new StringReader(sentence));
List<? extends HasWord> tokens = tokenizer.tokenize();
return tokens;
}
有了tokenize方法,parse方法便在此基础上对于生成的分词列表进行处理,使其变成树形结构。
public Tree parse(String sentence) {
List<? extends HasWord> tokens = tokenize(sentence);
if (getOp().testOptions.preTag) {
Function<List<? extends HasWord>, List<TaggedWord>> tagger = loadTagger();
tokens = tagger.apply(tokens);
}
return parse(tokens);
}
apply方法则通过parse方法对于词语列表同样进行了处理。
public Tree apply(List<? extends HasWord> words) {
return parse(words);
}
这时候,类中又定义了两个临时变量,分别是函数接口对象tagger和字符串taggerPath。有了这两个临时变量,接下来的方法定义才有他的意义所在。
private transient Function<List<? extends HasWord>, List<TaggedWord>> tagger;
private transient String taggerPath;
基于以上两个临时变量,我们可以分析以下的具体函数方法loadTagger以及重载方法lemmatize。由于对于Options类的不了解,暂时跳过对于loadTagger的分析,而对于重载函数lemmatize,则可以根据其注释得知,这是一个用于对词性进行归类分析的分析方法,输入字符串则是非英语分析,输入列表则是英语分析。
public Function<List<? extends HasWord>, List<TaggedWord>> loadTagger() {
Options op = getOp();
if (op.testOptions.preTag) {
synchronized(this) { // TODO: rather coarse synchronization
if (!op.testOptions.taggerSerializedFile.equals(taggerPath)) {
taggerPath = op.testOptions.taggerSerializedFile;
tagger = ReflectionLoading.loadByReflection("edu.stanford.nlp.tagger.maxent.MaxentTagger", taggerPath);
}
return tagger;
}
} else {
return null;
}
}
public List<CoreLabel> lemmatize(String sentence) {
List<? extends HasWord> tokens = tokenize(sentence);
return lemmatize(tokens);
}
/**
* Only works on English, as it is hard coded for using the
* Morphology class, which is English-only
*/
public List<CoreLabel> lemmatize(List<? extends HasWord> tokens) {
List<TaggedWord> tagged;
if (getOp().testOptions.preTag) {
Function<List<? extends HasWord>, List<TaggedWord>> tagger = loadTagger();
tagged = tagger.apply(tokens);
} else {
Tree tree = parse(tokens);
tagged = tree.taggedYield();
}
Morphology morpha = new Morphology();
List<CoreLabel> lemmas = Generics.newArrayList();
for (TaggedWord token : tagged) {
CoreLabel label = new CoreLabel();
label.setWord(token.word());
label.setTag(token.tag());
morpha.stem(label);
lemmas.add(label);
}
return lemmas;
}
剩下的都是抽象方法了,有待实现类定义。
public abstract Tree parse(List<? extends HasWord> words);
public abstract Tree parseTree(List<? extends HasWord> words);
public abstract List<Eval> getExtraEvals();
public abstract List<ParserQueryEval> getParserQueryEvals();
public abstract Options getOp();
public abstract TreebankLangParserParams getTLPParams();
public abstract TreebankLanguagePack treebankLanguagePack();
public abstract String[] defaultCoreNLPFlags();
public abstract void setOptionFlags(String ... flags);
public abstract boolean requiresTags();