编程规范在现代软件开发中起到了重要的作用。对于开发者而言,能够迅速查知需要遵守的编程规范,对于提升开发效率有重要的意义。本文提出了基于自然语言处理的编程规范搜索方法,利用自然语言处理工具,可以对规范文本进行分析解读,从而能够高效地构建索引,并提供相比普通搜索引擎和文本查找工具更符合预期的搜索结果。同时,本文也给出了配套的代码规范文件解析方法,以及工具的接口设计。
现代的软件工程项目几乎都是由一个团队完成,为了促进合作、提高开发效率、降低维护成本,开发团队常会制定一系列的编程规范。对于开源项目而言,会接受来自其它代码贡献者的提交,为了避免由于贡献者和编程风格和当前项目风格不一致所造成的困扰,制定编程规范是必要的。开发者若要参与一个项目,就需要熟悉该项目所制定的编程规范,并在编写代码时严格遵守。由于不同的项目常常有不同的编程规范,再考虑到编程语言的演变以及编程规范的修改,很难保证开发者在编写代码的时候能够完全正确地了解当前的规范。在代码检查工具尚不能覆盖所有编程规范的情况下,及时查阅编程规范原文是有必要的。而编程规范条目众多,由开发者人工浏览寻找是低效的,所以需要一个便捷的方式让开发者及时了解自己需要遵守的编程规范。
开发者在查找编程规范时,关心的往往是一个特定文本之内,涉及某个编程语言实体的规范条目。目前的搜索引擎,提供的结果往往是一个站点甚至整个互联网范围内符合条件的所有页面,粒度较大,不适合直接使用搜索引擎来查找规范条目。而对于阅读器内的文本查找工具而言,其只能对输入的文本进行机械的匹配,无法明确指出结果文本的所在的条目,而且对输入的关键词顺序和词形都很敏感,无法实现模糊搜索。
本文致力于实现一个适合编程规范的搜索工具,来减少开发者查阅规范的时间,提升开发效率。在构建搜索引擎方面,本文提出了使用自然语言处理工具来进行文本分析与关键词提取的方法。第 2 节介绍了搜索引擎的基本框架,通过专门为编程规范设计优化的索引构建算法和搜索算法,可以提高索引的构建效率,提供更强的模糊搜索能力,并获得更符合预期的搜索结果。考虑到编程规范的文本具有一定的格式,可以利用格式信息来解析文档并构建文档树,这样可以在搜索结果回溯时获得更多信息,第 3 节介绍了这方面的内容。而第 4 节则提供了该工具的编程接口和图形界面两种接口的设计,从而可以为将该工具集成到开发环境中提供参考。
虽然编程规范上描述的是编程语言,但其本身依然是自然语言所描述的。要处理编程规范,就意味着要依赖自然语言处理技术。本项目采用的是 Stanford CoreNLP 工具集,其提供了一系列自然语言处理工具,这些工具大多都是通过标记工具(annotator)的方式提供的,它们可以完成基础的自然语言处理任务。利用这些工具,再结合编程规范的实际需要,开发了这一搜索工具。在本项目中,主要利用到的基础工具有:分词(tokenization)、分句(sentence splitting)、词干化(lemmatization)、词性标注(part of speech)和成分分析(constituent parsing)。
要实现搜索的功能,需要对文本进行分析,提取单词作为索引的键。下面需要解决的是文本中的哪些词是需要提取,哪些词需要舍弃的问题。在没有自然语言处理工具的情况下,只能凭借固定词库查找或者特定正则表达式匹配来提取或者排除某一些词汇,这对待匹配的词库或者正则表达式的完备性有较高的要求。而利用自然语言处理工具,可以对文本进行分析,为判断需要的单词提供更多信息。
编程规范一般描述的是编程语言实体之间的关系,其核心在编程语言实体上。命名实体识别(named entity recognition)工具似乎擅长识别这些内容,其实没有这么简单。该工具的能力依赖训练的数据集,并且只能识别出经过训练的那些实体类型。便编程规范是软件工程专业领域的文本,目前的工具都不可能对这种文本进行训练。而我们在没有充足的语料进行训练的情况下,自然也不可能使其拥有这一能力。所以只能转变思路,寻求不依赖训练语料的、规则性更强的方法。
考虑到编程规范的实体基本都是名词词组,结合一般情况下,搜索引擎使用者输入搜索框的,一般也以名词词组为主这一经验观察,我们决定关注语句中的名词词组。通过成分分析,容易得到文本中所有成分的构成及分类,只要筛选出所有标记为名词词组的成分即可。将名词词组的单词转化为其词干再作为查找的索引,这是为了减少条目数量,并提供一定模糊搜索的能力。
该工具需要储存的数据主要包含两部分的内容:一部分是经过初步解析后,具有一定层次结构的文档树,该树的构造方式在第 3 节将进行详细介绍;另一部分则是以词元
之前已涉及了索引构建的大致思路,下面将详细地介绍索引的构建过程,并补充一些细节。这里仅关注文本中特定的某一句,即分句结果中的一个句标记(sentence annotation)单元。考虑到编程规范中涉及编程语言的关键词,它们也是编程语言的的实体,但是不是名词词组,可能在自然语言中是动词(如 do
)、形容词(如 public
)甚至连词(如 if
)等。这些关键词不能当作自然语言来处理,所以在完成分词、分句后,应该首先辨认出所有的语言关键词并加入到查找表中。语言的关键词数量不会很多,在程序中内置即可。
完成语言关键词识别后,再进行语法分析,获得所有的成分。在这些成分中首先筛选出所有的名词词组,然后还要去除包含了其它名词词组的名词词组,避免重复记录。然后对筛选得到的成分中的每个词进行处理,排除某些没有必要记录的特定词性(如介词、连词、冠词等)以及某些特定模式的词(如数字)。对剩下的词,转化为词元后将其作为键,将该句所在的文档树节点加入到键所对应的标记集合中去。
在分析搜索结果之前,首先建立一个以文档树中所有内容标记
下面要对搜索结果进行排序,令结果
- 先比较
$|r.S|$ ,大的在前; - 若
$|r.S|$ 相等,比较$r.m$ ,大的在前; - 若
$|r.S|$ 和$r.m$ 都相等,比较$r.t$ 所标记的文本长度,小的在前。
对该排序方法可以作以下直观的解释:对于两个搜索结果,首先判断其能满足用户期望的程度,也就是能匹配到到的搜索关键词的个数,能匹配的搜索关键词越多应该符合期望。然后判断其能强化这种期望的程度,即这段文字和用户提供的搜索关键词的总匹配次数,匹配次数越多应该和用户的期望更一致。最后判断文本长度,文本越短的额外信息就更少,更符合用户的期望。将搜索结果排序后,便可以以列表的形式反馈给用户,完成此次搜索过程。
前一节我们都将输入文本看作不带任何格式的纯文本,但实际上编程规范的文本常常是具有一定标记格式的。本文所描述的搜索工具支持 Markdown 语法。Markdown 是一种可以使用普通文本编辑工具编写的标记语言,其标记语法简洁明了,众多编程规范都有以 Markdown 编写的版本。Markdown 中可以用若干个 # 来表示标题的级别,利用这一标记可以来划分文档的层次;用 `` 可以标记行间代码,这样可以将程序语言和自然语言区分开来,分别作不同的处理等。这些为文本层次的划分、待分析文本内容的筛选、搜索结果的回溯等任务提供了额外的信息。
利用 Markdown 的标题级别,可以为文档划分层次,这样可以构成一棵简单的文档树。这里的文档树不同于语法分析树,其不需要表达出语法推导过程,而只是表示各级标题与正文的层级关系。文档树的节点分别标题节点和内容节点。标题节点都是内部节点,任何一级标题节点下都有若干内容子节点,同时还可以有若干级别更低的标题子节点。内容节点都是叶子节点,一个内容节点即为一个内容块,其可以是一个正文段落、一个代码段(即两个 ``` 之间的内容)或者一张表格等。在 2.3 节提到的内容标记,包含了一个内容块及其所在标题节点的引用。所以,一个搜索结果是以内容块为单位返回的。
要构建文档树、划分内容块,就必须要对其中的一些标记进行识别。Markdown 语法较为简单,使用正则表达式足够识别这些标记。下表中列出了在此工具里使用到的正则表达式:
标记 | 正则表达式 |
---|---|
标题 | ^#+.* |
行间代码 | `(.*?)` |
进入代码块 | ^`{3}.+ |
退出代码块 | ^`{3}$ |
表格 | ^|(.*|)+$ |
能够识别特定标记之后,便可以构造文档树了。构造自顶向下进行,逐行扫描源文件,如果遇到标题,获得标题级别,如果小于则在当前标题节点上建立子节点,否则将回溯后再建立子节点。如果遇到内容行,则添加到内容块中。由于内容可以为正文、代码块、表格等,所以还需要额外处理。如果为正文,直接添加为新的内容块即可;如果是代码块,需要额外记录代码块的进入和退出(通过正则表达式匹配),判断是加入到之前的块中还是建立新的块;如果是表格,需要通过前一行的内容是否是表格来判断是加入到之前的块还是建立新的块。
在解析文档时,可以同步进行自然语言处理。在此工具中,只有正文部分会进行处理,而代码块和表格则会略过,前者时因为根本不是自然语言,后者只是作为正文的补充,不是搜索的关注点。
由于此工具使用 Java 编写,故开放 Java API(应用程序编程接口)供开发者使用,所有程序类均存放于 wzh.codeconventions.core
包中。
搜索工具的核心功能通过 Searcher
类的对象来完成。其 build
方法接受两个字符串作为参数,前一个为源文本的路径,后一个为待生成的索引文件的路径。索引文件储存的是索引数据的序列化版本,这样构建了一次索引数据后可以重复加载使用,不用每次运行程序都要建立索引。索引文件建立后,不再需要源文本也可以正常搜索。若搜索工具有更新或者代码规范有修改后,都需要重新建立索引。load
方法接受一个索引文件路径的字符串作为参数,从索引文件反序列化索引数据,载入到当前 Searcher
对象中。如果之前调用过 build
方法,索引数据已经储存在 Searcher
对象中,不需要再调用 load
方法。search
方法接受一个搜索关键词的字符串为参数,其将排好序的结果通过 ArrayList<SearchResult>
的方式返回给开发者。
此外还有一些辅助的类也向开发者开放。如 Node
类表示文档树中的标题节点,Block
类表示内容块,ContentTag
表示内容标记,SearchResult
表示搜索结果项。这些结构在 2、3 节中都已作过介绍,这些类只是它们在程序中的具体表示。通过这些辅助类,可以为搜索结果的反馈提供更多的信息。
为了高效地和用户进行交互,仅提供 API 是无法做到这一点的,需要设计一定的图形界面来实现直观的搜索操作。图形界面需要在用户和 API 之间建立合适的桥梁,将用户的输入经过适当的处理提供给核心程序,并将结果以简洁明了的方式反馈给用户。正如一般的搜索引擎一样,在主界面中需要包含搜索框和结果列表。由于搜索工具需要构建和加载索引,所以在需要为这两个功能分别提供对话框指导用户完成。由于搜索结果可以定位到原文档树的位置,用户界面可以利用这一点显示多于搜索条目自身的内容。比如,当用户双击某一条目时,可以显示出同一节标题下的其余所有内容,共详细参考。
在源代码的 wzh.codeconventions.gui
包中提供了一个用 Swing 实现的图形界面的简单框架。该图形界面程序包括了菜单、文本框、按钮、列表、对话框等基本元素。由于 Swing 中的组件不能直接显示带格式的 Markdown 文本,这里利用了 flexmark-java
这一工具将 Markdown 先转为 HTML 再交给 Swing 中的组件来显示。搜索结果的呈现,是通过 JList
组件显示了一系列标题加正文段落的 HTML。而双击结果后显示的详细列表,则是该标题下所有内容(包括所有正文段落和代码块、表格)转换成的 HTML。这样尽可能地保留了源文本中的所有格式信息,便于用户阅读。这一用户序仅作为参考,开发者还可以根据自己的需要,通过其它框架实现自己的图形界面。
本项目的亮点在于:
- 利用自然语言处理,结合编程规范的实际需要,优化了索引的构建过程,提升了搜索的效率和精度;
- 利用了编程规范文本的格式信息,为文本建立了层次结构,提升了搜索结果的反馈质量;
- 为编程规范的搜索从方法到实现提供了一套合理、完整的框架。
本项目的不足在于:
- 局限于对现有自然语言处理工具的直接使用,缺乏对编程规范文本的适应性;
- 假定了编程规范文本和搜索关键词输入都是完全正确的,对无效输入缺乏纠错能力。
- 可尝试使用编程规范文本对自然语言处理工具进行训练,使自然语言工具对此类文本有更好的适应性;
- 可以探索利用其它方式提升模糊搜索的功能,提高搜索的容错能力;
- 可在进一步调研编程规范搜索的基础上,对搜索结果的排序方式作进一步调整和优化。
- Christopher D. Manning, Mihai Surdeanu, John Bauer, Jenny Finkel, Steven J, Bethard, and David McClosky. 2014. The Stanford CoreNLP Natural Language Processing Toolkit.
- Daniel Jurafsky and James H. Martin. 2008. Speech and Language Processing, 2nd Edition. Prentice Hall.
- John MacFarlane. 2019. CommonMark Spec.
https://spec.commonmark.org/0.29/
. - Google. 2018. Google Java Guide.
https://google.github.io/styleguide/javaguide.html
.