单词计数:将集合中出现的相同的单词,进行计数。
基础的wordcount
基础的wordcount中,我们只需要对包含字符串的List[String]进行单词计数即可。
我们要对这个List进行wordcount:
val stringList: List[String] = List(
"hello",
"hello scala",
"hello spark scala",
"hello flink scala",
"hello java",
"darren"
)
流程分析
- 首先,我们需要将这个stringList中每一个String元素按照空格拆分为单词数组Array[String],得到一个 List[Array[String]] ,然后将这些 List[Array[String]] 进行扁平化处理,得到拆分后的 List[String] :
//将List中字符串按照空格拆分为Array[String]数组
val stringList1: List[Array[String]] = stringList.map(string => string.split(" "))
//将stringList1进行扁平化
val stringList2: List[String] = stringList1.flatten
这两步操作可以通过flatmap进行简化,将map和flat合并到一步进行:
//将list按照空格拆分后进行扁平化
val stringList12: List[String] = stringList.flatMap(string => string.split(" "))
- 然后我们将得到的扁平化List按照字符串聚合,形成一个 String -> List[String] 的Map映射,这个Map的元素为 单词 -> 当前单词全部出现的列表 :
//将stringList2依据单词本身进行聚合,得到string -> List[String]的map
val stringList3 = stringList2.groupBy(string => string)
println(stringList3)
- 接着,我们按照得到的映射,统计每个List中的元素,形成一个 String -> Int 的Map映射,这个Map映射的元素为 单词 -> 出现次数 :
//将stringList3进行map统计,变成一个(字符 -> 出现次数)的Map
val countMap = stringList3.map((kv: (String, List[String])) => (kv._1, kv._2.size))
println(countMap)
- 最后,我们将这个映射转换成 List[(String, Int)] 的集合,按照 Tuple._2 进行降序排列,取出前三个元素形成结果集合即可,这个集合的元素为 (单词, 出现次数)的二元组 :
//将countMap转化为List[Map],并且降序排序取出前三个元素
val wordCount: List[(String, Int)] = countMap.toList.sortWith((kv1, kv2) => kv1._2 > kv2._2).take(3)
println(wordCount)
简单wordcount函数展示
基于上述流程可以构建一个简单wordcount函数:
//简单wordcount函数
def simpleWordCount(list: List[String]): List[(String, Int)] = {
list
.flatMap(elem => elem.split(" "))
//对list进行按照空格拆分 & 扁平化处理
.groupBy(elem => elem)
//按照字符串元素的聚合,形成string -> List[String]的Map
// 比如: Map(("a" -> List["a", "a", "a"]), ("b" -> List["b", "b", "b"]))
.map(kv => kv._1 -> kv._2.size)
//按照List.size(出现次数)进行统计,形成String -> Int的Map
// 比如:Map(("a" -> 3), ("b", -> 4))
.toList.sortWith((kv1, kv2) => kv1._2 > kv2._2).take(3)
//形成List[(String, Int)],按照出现次数进行降序排序,并且取出前三个元素,将该结果直接返回
}
复杂的wordcount
现在我们有一个已经进行预统计的单词集合,这个集合已经标记了每个字符串出现的次数(可以理解为分布式文本统计结果的初步聚合),我们需要对其进行进一步的单词计数,统计每一个单词总共出现的次数:
//预统计字符串出现次数元组
val stringList: List[(String, Int)] = List(
("hello", 1),
("hello scala", 2),
("hello spark scala", 3),
("hello flink scala", 2),
("hello java", 1),
("darren", 9)
)
我们很容易想到将这个List按照字符串出现次数进行拼接,转换为普通List,然后按照简单wordcount那样进行单词计数:
//简单wordcount函数
def simpleWordCount(list: List[String]): List[(String, Int)] = {
//对list进行按照空格拆分 & 扁平化处理
list.flatMap(string => string.split(" ")).
//对stringList进行按照字符串元素的聚合,形成string -> List[String]的Map
// 比如: Map(("a" -> List["a", "a", "a"]), ("b" -> List["b", "b", "b"]))
groupBy(string => string).
//将groupMap按照List.size(出现次数)进行统计,形成String -> Int的Map
// 比如:Map(("a" -> 3), ("b", -> 4))
map(kv => kv._1 -> kv._2.size).
//将countMap形成List[(String, Int)],按照出现次数进行降序排序,并且取出前三个元素,将该结果直接返回
toList.sortWith((kv1, kv2) => kv1._2 > kv2._2).take(3)
}
//将预统计字符串变为普通字符串
// 即将每个字符串按出现次数进行拼接
val strings1 = stringList.map(kv => (kv._1 + " ") * kv._2)
println(strings1)
val result1 = simpleWordCount(strings1)
println(result1)
流程分析
但是这种方式浪费了预统计处理,并没能成功利用。我们在这里提供一种基于预统计结果进行处理的wordcount:
- 原始List的元素是 (String, Int) 的一系列Tuple2,我们可以对String进行拆分,然后根据出现次数Int,将Tuple拆分为 Array[(String, Int)] ,对其进行扁平化合并,实现按照空格拆分单词同时又能利用预统计结果:
//根据预统计对每一个字符串进行拆分,得到一个(String, Int)元组数组,然后进行扁平化
// 比如("hello scala", 2),拆分后结果为:Array(("hello", 2), ("scala", 2))
val list = stringList.flatMap(tuple => {
val stringArr: Array[String] = tuple._1.split(" ")
stringArr.map(elem => elem -> tuple._2)
})
println(list)
- 然后对于新的扁平化集合,我们就可以按照Tuple._1进行分组,将相同单词的元组划分到一类,得到 String -> List[(String, Int)] 的Map,其中的 List[(String, Int)] 为当前单词以及出现次数的元组,在分布式场景中可以理解为多个文件中的单词出现情况:
//对这个元组List按照tuple._1进行分组聚合,形成一个string -> List[(String, Int)]的map
// 比如:对hello有关元组进行聚合,得到:hello -> List[("hello", 1), ("hello", 2)]
val groupList = list.groupBy(tuple => tuple._1)
println(groupList)
- 对于这个Map,我们就可以对 List[(String, Int)] 进行求和,获取该单词的出现次数了。我们可以使用map方法,对于Map中每一个kv,将其转换成kv._2中的tuple._2的和:
//对groupList中每一组kv进行value的合并,得到String -> Int的结果,并排序,取出前三
val resultList = groupList.map(kv => {
//统计每个单词出现的次数
// 可以通过kv._2的List[(String, Int)]调用map,获取List中元组的第二个元素,然后进行求和
// 即:kv._2.map(tuple => tuple._2).sum
kv._1 -> kv._2.map(tuple => tuple._2).sum
})
println(resultList)
- 我们对这个处理过程进行参数说明。groupList.map(kv => ) 中的kv是分组Map的每一个元组,这个元组是 (String, List[(String, Int)]) 类型的。比如:scala -> List((scala,2), (scala,3), (scala,2))。
- 我们需要将kv._2 (即 List[(String, Int)]) 进行求和转换,就需要对kv._2再进行一次map操作,即:kv._2.map(tuple => tuple._2).sum 。这个操作中的tuple就是kv._2中的 (String, Int) 元组,我们对这个 kv._2 转换成只包含 Int 的List,比如:List((scala,2), (scala,3), (scala,2)) 转换成 List(2, 3, 2)。
- 最后我们构建 String -> Int 的映射,即:kv._1 -> kv._2.map(tuple => tuple._2).sum ,也就是 当前单词 -> 在所有文档出现次数 的映射。
- 最后我们进行 toList 转换,构建成有序集合,然后根据 出现次数(tuple._2) 进行sortWith排序,最后 使用 take 取出前三即可:
val result = resultList.toList.sortWith((kv1, kv2) => kv1._2 > kv2._2).take(3)
println(result)
复杂wordcount函数展示
def complexWordCount(list: List[(String, Int)]): List[(String, Int)] = {
list
.flatMap(tuple => tuple._1.split(" ").map(string => string -> tuple._2))
//对每一个元组的字符串都进行拆分,得到Array[String]数组,
// 对该数组进行map处理,将元素与元组出现次数一起,得到Array[(元组中单词, 元组中单词出现次数)],
// 比如:("hello scala", 2) -> Array(("hello", 2), ("scala", 2))
// 并将Array进行扁平化
// 结果为:
// List((hello,1), (hello,2), (scala,2), (hello,3), (spark,3),
// (scala,3), (hello,2), (flink,2), (scala,2), (hello,1),
// (java,1), (darren,9)
// )
.groupBy(tuple => tuple._1)
//按照单词进行分组,得到 单词 -> List[该单词所有出现的元组] 的Map映射
// 结果为:
// Map(darren -> List((darren,9)), java -> List((java,1)), flink -> List((flink,2)),
// spark -> List((spark,3)), scala -> List((scala,2), (scala,3), (scala,2)),
// hello -> List((hello,1), (hello,2), (hello,3), (hello,2), (hello,1))
// )
.map(kv => kv._1 -> kv._2.map(tuple => tuple._2).sum)
//对分组Map进行转换,得到 单词 -> 出现总次数的Map映射
// kv即分组Map集合的键值对 单词 -> List[该单词所有出现的元组]
// kv._1为当前单词
// kv._2为List[该单词出现的所有元组]
// kv._2.map(tuple => tuple._2)就是将kv._2转换成只有单词出现次数的List
.toList.sortWith((kv1, kv2) => kv1._2 > kv2._2).take(3)
//将 单词 -> 出现总次数的Map映射构建成有序集合List,
// 按照出现次数kv._2进行降序排列,
// 然后取前三名
}
println(complexWordCount(stringList))