【翻译】TextClassification介绍(二)

2018-11-15 by Liuqingwen | Tags: Android 翻译 | Hits

一、说明

这是一个关于介绍 TextClassification API 的系列文章,总共分三篇,本文是第二篇。上一篇在此:【翻译】TextClassification介绍(一)

原文作者:Mark Allison
阅读时间: 3 分钟
原文链接:https://blog.stylingandroid.com/textclassification-part-2/

二、正文

在 API 26 (奥利奥)中安卓引入了一个新的文字功能系统: TextClassification 。这个系统将会在 API 28 ( π )中进一步改进完善。在本次简短的系列中,我们主要会探讨它是一个什么样的系统,如何使用它,以及如何为它添加一些自定义行为。

上一篇文章中我们研究了如何对文本进行分类的两个不同步骤:首先将选择的文本扩展为可能会被分类为具体类型的小段,接着执行该分类,并确定相关的操作。如果要编写我们自己的文本分类器,我们需要通过覆盖 TextClassifier 接口中相对应的方法来实现这两个步骤,这个接口也是我们将要实现的。两个方法 suggestSelection()classifyText() 都具有两种形式,一种是带有独立的参数,另一种是带有包含所有这些参数的 Request 对象。覆盖 Request 类非常重要,因为另一个方法仅仅是一个简单的包装而已,它使用传递的各个参数构造出一个 Request 实例,然后在方法中调用该 Request 实例。因此,使用这种方式重写意味着我们不需要再做其他的形式了:

StylingAndroidTextClassifier.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class StylingAndroidTextClassifier(
private val context: Context,
private val fallback: TextClassifier,
private val factory: TextClassifierFactory = FrameworkFactory()
) : TextClassifier by fallback {

private val stylingAndroid = "Styling Android"
private val stylingAndroidUri = "https://blog.stylingandroid.com"
private val regex = Regex("Styling\\s?Android", RegexOption.IGNORE_CASE)

override fun suggestSelection(request: TextSelection.Request): TextSelection {
return fallback.suggestSelection(request)
}

override fun classifyText(request: TextClassification.Request): TextClassification {
return fallback.classifyText(request)
}
}

大部分方法都会被代理到一个名为 fallbackTextClassifier 实例,实际上这个实例就是默认的系统分类对象,因此如果我们所自定义的 TextClassifier 对象在检测文本匹配遇到失效时,那么我们将回滚到系统分类对象并得到其分类结果。我们仅需要覆盖两个方法来将执行自定义分类。

我们的自定义文本分类器 TextClassifier 将检测目标字符串 "Styling Android" 并创建一个自定义的操作,这个操作会在浏览器中打开 https://blog.stylingandroid.com" 网页链接,并附有一个自定义的标题和图标。让我们先来看看我们是如何覆盖 suggestSelection() 方法的。在第一篇文章中,我们研究了如何将用户选择的文本,扩展到包含当前选择的最小具体类型文段。我所选择使用的算法有点粗糙,并且很可能不是最高效的(特别是当文本很长的时候),但不管怎样还是能用:首先它从正则表达式 Styling\sAndroid (非大小写敏感)开始搜索整个字符串,因此我们会找到类似 "Styling Android""styling android""StylingAndroid" 的结果,以及所有他们的组合案例;然后它会将每个匹配到的文本范围与当前的选择进行比较,如果当前选择的文本完全落在其中的某一个匹配的范围内,那么将选择扩展至该范围:

StylingAndroidTextClassifier.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
override fun suggestSelection(request: TextSelection.Request): TextSelection {
return findRangeOfMatch(request)
?: fallback.suggestSelection(request)
}

//line 23
private fun findRangeOfMatch(request: TextSelection.Request): TextSelection? {
//line 24
return regex.findAll(request.text)
.firstOrNull { it.range.contains(request.startIndex until request.endIndex) }
?.range
?.let {
factory.buildTextSelection(it.start, it.endInclusive + 1, TextClassifier.TYPE_URL, 1.0f)
}
}

private fun <T : Comparable<T>> ClosedRange<T>.contains(range: ClosedRange<T>) =
contains(range.start) && contains(range.endInclusive)

这里的 findRangeOfMatch() 方法处理了所有的逻辑,如果匹配失败则返回一个空值,因此 Elvis 运算符会在默认的系统文本分类 TextClassifier (第 17 行)上调用 suggestSelection() 方法,这样我们至少能尝试并获取系统所匹配它所支持的类型。

这个 factory 实例是一个对象工厂用于保持代码可以被测试使用,我之前已经介绍过,另外还有几个单元测试可以用来检查它是否按我们所期望的那样运行。

这里的 findRangeOfMatch() 方法首先会搜索字符串中符合正则表达式的所有实例(第 23 行)。这会返回一个 Sequence 对象,它包含所有匹配到的详细信息,我们使用 firstOrNull 方法来筛选出包含当前选择的第一个匹配项,如果没有则返回 null 空值(第 24 行)。最后两行使用工厂方法构造出 TextSelection 对象实例,不过前提是匹配到的包含当前选择的 MatchResult 对象为非空范围。这里的安全调用操作符确保了空值 null 的安全性,但是,如果没有找到有效的匹配项,那么整个方法将返回一个空值 null 。代码中的 contains 扩展方法是一个将整个搜索落入更大范围的便利功能,这个扩展也提高了代码的可读性。

在我们构造 TextSelection 对象时,我们提供了扩展范围的开始下标和结束下标,以及我们将要识别的文本的具体类型(在这里的情形下,我们使用 TextClassifier.TYPE_URL 常量表示其为一个 URL 链接),并传入 1.0f 为可信度得分,因为我们需要确保这是一个正确的匹配项。

在这个工厂方法的实现中使用了 textSelection.Builder 方法并加上这些参数来实现创建出一个 TextSelection 实例:

TextClassifierFactory.kt
1
2
3
4
5
6
7
8
9
10
override fun buildTextSelection(
startIndex: Int,
endIndex: Int,
entityType: String,
confidenceScore: Float
): TextSelection {
return TextSelection.Builder(startIndex, endIndex)
.setEntityType(entityType, confidenceScore)
.build()
}

在接下来的文章里,我们需要实现 classifyText() 方法来执行文本分类,这个我会在本系列的最后一篇文章中详述。

三、总结

这篇文章的源代码可以在这里找到: https://github.com/StylingAndroid/TextClassification/tree/Part2

© 2018 , Mark Allison 。保留所有版权。

我的博客地址: http://liuqingwen.me ,欢迎关注我的微信公众号:
IT自学不成才


Comments: