1. 背景

近日项目要求基于爬取的影视评论信息,抽取影视的关键字信息。考虑到影视评论数据量较大,因此采用Spark处理框架。关键词提取的处理主要包含分词+算法抽取两部分。目前分词工具包较为主流的,包括哈工大的LTP以及HanLP,而关键词的抽取算法较多,包括TF-IDF、TextRank、互信息等。本次任务主要基于LTP、HanLP、Ac双数组进行分词,采用TextRank、互信息以及TF-IDF结合的方式进行关键词抽取。

说明:本项目刚开始接触,因此效果层面需迭代调优。

2. 技术选型

(1) 词典

1) 基于HanLP项目提供的词典数据,具体可参见HanLP的github

2) 考虑到影视的垂直领域特性,引入腾讯的嵌入的汉语词,参考该地址

(2) 分词

1) LTP分词服务:基于Docker Swarm部署多副本集服务,通过HTTP协议请求,获取分词结果(部署方法可百度); 也可以直接在本地加载,放在内存中调用,效率更高(未尝试)

2) AC双数组:基于AC双数组,采用最长匹配串,采用HanLP中的AC双数组分词器

(3) 抽取

1) 经典的TF-IDF:基于词频统计实现

2) TextRank:借鉴于PageRank算法,基于HanLP提供的接口

3) 互信息:基于HanLP提供的接口

3. 实现代码

(1) 代码结构

1) 代码将分词服务进行函数封装,基于不同的名称,执行名称指定的分词

2) TextRank、互信息、LTP、AC双数组等提取出分词或短语,最后均通过TF-IDF进行统计计算

(2) 整体代码

1) 主体代码:细节层面与下载的原始评论数据结构有关,因此无需过多关注,只需关注下主体流程即可

  1. def extractFilmKeyWords(algorithm: String): Unit ={
  2. // 测试
  3. println(HanLPSpliter.getInstance.seg("如何看待《战狼2》中的爱国情怀?"))
  4.  
  5. val sc = new SparkContext(new SparkConf().setAppName("extractFileKeyWords").set("spark.driver.maxResultSize", "3g"))
  6.  
  7. val baseDir = "/work/ws/video/parse/key_word"
  8.  
  9. import scala.collection.JavaConversions._
  10. def extractComments(sc: SparkContext, inputInfo: (String, String)): RDD[(String, List[String])] = {
  11. sc.textFile(s"$baseDir/data/${inputInfo._2}")
  12. .map(data => {
  13. val json = JSONObjectEx.fromObject(data.trim)
  14. if(null == json) ("", List())
  15. else{
  16. val id = json.getStringByKeys("_id")
  17. val comments: List[String] = json.getArrayInfo("comments", "review").toList
  18. val reviews: List[String] = json.getArrayInfo("reviews", "review").toList
  19. val titles: List[String] = json.getArrayInfo("reviews", "title").toList
  20. val texts = (comments ::: reviews ::: titles).filter(f => !CleanUtils.isEmpty(f))
  21. (IdBuilder.getSourceKey(inputInfo._1, id), texts)
  22. }
  23. })
  24. }
  25.  
  26. // 广播停用词
  27. val filterWordRdd = sc.broadcast(sc.textFile(s"$baseDir/data/stopwords.txt").map(_.trim).distinct().collect().toList)
  28.  
  29. def formatOutput(infos: List[(Int, String)]): String ={
  30. infos.map(info => {
  31. val json = new JSONObject()
  32. json.put("status", info._1)
  33. try{
  34. json.put("res", info._2)
  35. } catch {
  36. case _ => json.put("res", "[]")
  37. }
  38. json.toString.replaceAll("[\\s]+", "")
  39. }).mkString(" | ")
  40. }
  41.  
  42. def genContArray(words: List[String]): JSONArray ={
  43. val arr = new JSONArray()
  44. words.map(f => {
  45. val json = new JSONObject()
  46. json.put("cont", f)
  47. arr.put(json)
  48. })
  49. arr
  50. }
  51.  
  52. // 基于LTP分词服务
  53. def splitWordByLTP(texts: List[String]): List[(Int, String)] ={
  54. texts.map(f => {
  55. val url = "http://dev.content_ltp.research.com/ltp"
  56. val params = new util.HashMap[String, String]()
  57. params.put("s", f)
  58. params.put("f", "json")
  59. params.put("t", "ner")
  60. // 调用LTP分词服务
  61. val result = HttpPostUtil.httpPostRetry(url, params).replaceAll("[\\s]+", "")
  62. if (CleanUtils.isEmpty(result)) (0, f) else {
  63. val resultArr = new JSONArray()
  64.  
  65. val jsonArr = try { JSONArray.fromString(result) } catch { case _ => null}
  66. if (null != jsonArr && 0 < jsonArr.length()) {
  67. for (i <- 0 until jsonArr.getJSONArray(0).length()) {
  68. val subJsonArr = jsonArr.getJSONArray(0).getJSONArray(i)
  69. for (j <- 0 until subJsonArr.length()) {
  70. val subJson = subJsonArr.getJSONObject(j)
  71. if(!filterWordRdd.value.contains(subJson.getString("cont"))){
  72. resultArr.put(subJson)
  73. }
  74. }
  75. }
  76. }
  77. if(resultArr.length() > 0) (1, resultArr.toString) else (0, f)
  78. }
  79. })
  80. }
  81.  
  82. // 基于AC双数组搭建的分词服务
  83. def splitWordByAcDoubleTreeServer(texts: List[String]): List[(Int, String)] ={
  84. texts.map(f => {
  85. val splitResults = SplitQueryHelper.splitQueryText(f)
  86. .filter(f => !CleanUtils.isEmpty(f) && !filterWordRdd.value.contains(f.toLowerCase)).toList
  87. if (0 == splitResults.size) (0, f) else (1, genContArray(splitResults).toString)
  88. })
  89. }
  90.  
  91. // 内存加载AC双数组
  92. def splitWordByAcDoubleTree(texts: List[String]): List[(Int, String)] ={
  93. texts.map(f => {
  94. val splitResults = HanLPSpliter.getInstance().seg(f)
  95. .filter(f => !CleanUtils.isEmpty(f) && !filterWordRdd.value.contains(f.toLowerCase)).toList
  96. if (0 == splitResults.size) (0, f) else (1, genContArray(splitResults).toString)
  97. })
  98. }
  99.  
  100. // TextRank
  101. def splitWordByTextRank(texts: List[String]): List[(Int, String)] ={
  102. texts.map(f => {
  103. val splitResults = HanLP.extractKeyword(f, 100)
  104. .filter(f => !CleanUtils.isEmpty(f) && !filterWordRdd.value.contains(f.toLowerCase)).toList
  105. if (0 == splitResults.size) (0, f) else {
  106. val arr = genContArray(splitResults)
  107. if(0 == arr.length()) (0, f) else (1, arr.toString)
  108. }
  109. })
  110. }
  111.  
  112. // 互信息
  113. def splitWordByMutualInfo(texts: List[String]): List[(Int, String)] ={
  114. texts.map(f => {
  115. val splitResults = HanLP.extractPhrase(f, 50)
  116. .filter(f => !CleanUtils.isEmpty(f) && !filterWordRdd.value.contains(f.toLowerCase)).toList
  117. if (0 == splitResults.size) (0, f) else {
  118. val arr = genContArray(splitResults)
  119. if(0 == arr.length()) (0, f) else (1, arr.toString)
  120. }
  121. })
  122. }
  123.  
  124. // 提取分词信息
  125. val unionInputRdd = sc.union(
  126. extractComments(sc, SourceType.DB -> "db_review.json"),
  127. extractComments(sc, SourceType.MY -> "my_review.json"),
  128. extractComments(sc, SourceType.MT -> "mt_review.json"))
  129. .filter(_._2.nonEmpty)
  130.  
  131. unionInputRdd.cache()
  132.  
  133. unionInputRdd.map(data => {
  134. val splitResults = algorithm match {
  135. case "ltp" => splitWordByLTP(data._2)
  136. case "acServer" => splitWordByAcDoubleTreeServer(data._2)
  137. case "ac" => splitWordByAcDoubleTree(data._2)
  138. case "textRank" => splitWordByTextRank(data._2)
  139. case "mutualInfo" => splitWordByMutualInfo(data._2)
  140. }
  141.  
  142. val output = formatOutput(splitResults)
  143. s"${data._1}\t$output"
  144. }).saveAsTextFile(HDFSFileUtil.clean(s"$baseDir/result/wordSplit/$algorithm"))
  145.  
  146. val splitRDD = sc.textFile(s"$baseDir/result/wordSplit/$algorithm/part*", 30)
  147. .flatMap(data => {
  148. if(data.split("\\t").length < 2) None
  149. else{
  150. val sourceKey = data.split("\\t")(0)
  151. val words = data.split("\\t")(1).split(" \\| ").flatMap(f => {
  152. val json = JSONObjectEx.fromObject(f.trim)
  153. if (null != json && "".equals(json.getStringByKeys("status"))) {
  154. val jsonArr = try { JSONArray.fromString(json.getStringByKeys("res")) } catch { case _ => null }
  155. var result: List[(String, String)] = List()
  156. if (jsonArr != null) {
  157. for (j <- 0 until jsonArr.length()) {
  158. val json = jsonArr.getJSONObject(j)
  159. val cont = json.getString("cont")
  160. result ::= (cont, cont)
  161. }
  162. }
  163. result.reverse
  164. } else None
  165. }).toList
  166. Some((sourceKey, words))
  167. }
  168. }).filter(_._2.nonEmpty)
  169.  
  170. splitRDD.cache()
  171.  
  172. val totalFilms = splitRDD.count()
  173.  
  174. val idfRdd = splitRDD.flatMap(result => {
  175. result._2.map(_._1).distinct.map((_, 1))
  176. }).groupByKey().filter(f => f._2.size > 1).map(f => (f._1, Math.log(totalFilms * 1.0 / (f._2.sum + 1))))
  177.  
  178. idfRdd.cache()
  179. idfRdd.map(f => s"${f._1}\t${f._2}").saveAsTextFile(HDFSFileUtil.clean(s"$baseDir/result/idf/$algorithm"))
  180.  
  181. val idfMap = sc.broadcast(idfRdd.collectAsMap())
  182. // 计算TF
  183. val tfRdd = splitRDD.map(result => {
  184. val totalWords = result._2.size
  185. val keyWords = result._2.groupBy(_._1)
  186. .map(f => {
  187. val word = f._1
  188. val tf = f._2.size * 1.0 / totalWords
  189. (tf * idfMap.value.getOrElse(word, 0D), word)
  190. }).toList.sortBy(_._1).reverse.filter(_._2.trim.length > 1).take(50)
  191. (result._1, keyWords)
  192. })
  193.  
  194. tfRdd.cache()
  195. tfRdd.map(f => {
  196. val json = new JSONObject()
  197. json.put("_id", f._1)
  198.  
  199. val arr = new JSONArray()
  200. for (keyWord <- f._2) {
  201. val subJson = new JSONObject()
  202. subJson.put("score", keyWord._1)
  203. subJson.put("word", keyWord._2)
  204. arr.put(subJson)
  205. }
  206. json.put("keyWords", arr)
  207. json.toString
  208. }).saveAsTextFile(HDFSFileUtil.clean(s"$baseDir/result/keyword/$algorithm/withScore"))
  209.  
  210. tfRdd.map(f => s"${f._1}\t${f._2.map(_._2).toList.mkString(",")}")
  211. .saveAsTextFile(HDFSFileUtil.clean(s"$baseDir/result/keyword/$algorithm/noScore"))
  212.  
  213. tfRdd.unpersist()
  214.  
  215. splitRDD.unpersist()
  216. idfMap.unpersist()
  217. idfRdd.unpersist()
  218.  
  219. unionInputRdd.unpersist()
  220. filterWordRdd.unpersist()
  221. sc.stop()
  222. }

2) 基于HanLP提供的AC双数组封装

  1. import com.google.common.collect.Lists;
  2. import com.hankcs.hanlp.HanLP;
  3. import com.hankcs.hanlp.seg.Segment;
  4. import com.hankcs.hanlp.seg.common.Term;
  5. import org.slf4j.Logger;
  6. import org.slf4j.LoggerFactory;
  7.  
  8. import java.io.Serializable;
  9. import java.util.List;
  10.  
  11. public class HanLPSpliter implements Serializable{
  12. private static Logger logger = LoggerFactory.getLogger(Act.class);
  13.  
  14. private static HanLPSpliter instance = null;
  15.  
  16. private static Segment segment = null;
  17.  
  18. private static final String PATH = "conf/tencent_word_act.txt";
  19.  
  20. public static HanLPSpliter getInstance() {
  21. if(null == instance){
  22. instance = new HanLPSpliter();
  23. }
  24. return instance;
  25. }
  26.  
  27. public HanLPSpliter(){
  28. this.init();
  29. }
  30.  
  31. public void init(){
  32. initSegment();
  33. }
  34.  
  35. public void initSegment(){
  36. if(null == segment){
  37. addDict();
  38. HanLP.Config.IOAdapter = new HadoopFileIOAdapter();
  39. segment = HanLP.newSegment("dat");
  40. segment.enablePartOfSpeechTagging(true);
  41. segment.enableCustomDictionaryForcing(true);
  42. }
  43. }
  44.  
  45. public List<String> seg(String text){
  46. if(null == segment){
  47. initSegment();
  48. }
  49.  
  50. List<Term> terms = segment.seg(text);
  51. List<String> results = Lists.newArrayList();
  52. for(Term term : terms){
  53. results.add(term.word);
  54. }
  55. return results;
  56. }
  57. }

3) HanLP加载HDFS中的自定义词典

  1. import com.hankcs.hanlp.corpus.io.IIOAdapter;
  2. import org.apache.hadoop.conf.Configuration;
  3. import org.apache.hadoop.fs.FileSystem;
  4. import org.apache.hadoop.fs.Path;
  5.  
  6. import java.io.IOException;
  7. import java.io.InputStream;
  8. import java.io.OutputStream;
  9. import java.net.URI;
  10.  
  11. public class HadoopFileIOAdapter implements IIOAdapter{
  12. @Override
  13. public InputStream open(String path) throws IOException {
  14. Configuration conf = new Configuration();
  15. FileSystem fs = FileSystem.get(URI.create(path), conf);
  16. return fs.open(new Path(path));
  17. }
  18.  
  19. @Override
  20. public OutputStream create(String path) throws IOException {
  21. Configuration conf = new Configuration();
  22. FileSystem fs = FileSystem.get(URI.create(path), conf);
  23. OutputStream out = fs.create(new Path(path));
  24. return out;
  25. }
  26. }

4. 采坑总结

(1) Spark中实现HanLP自定义词典的加载

由于引入腾讯的嵌入词,因此使用HanLP的自定义词典功能,参考的方法如下:

a. 《基于hanLP的中文分词详解-MapReduce实现&自定义词典文件》,该方法适用于自定义词典的数量较少的情况,如果词典量较大,如腾讯嵌入词820W+,理论上jar包较为臃肿

b. 《Spark中使用HanLP分词》,该方法的好处在于无需手工构件词典的bin文件,操作简单

切记:如果想让自定义词典生效,需先将data/dictionary/custom中的bin文件删除。通过HanLP源码得知,如果存在bin文件,则直接加载该bin文件,否则会将custom中用户自定义的词典重新加载,在指定的环境中(如本地或HDFS)中自动生成bin文件。

腾讯820W词典,基于HanLP生成bin文件的时间大概为30分钟。

(2) Spark异常

Spark执行过程中的异常信息:

1) 异常1

a. 异常信息:

Job aborted due to stage failure: Total size of serialized results of 3979 tasks (1024.2 MB) is bigger than spark.driver.maxResultSize (1024.0 MB)

b. 解决:通过设置spark.driver.maxResultSize=4G,参考:《Spark排错与优化

2) 异常2

a. 异常信息:java.lang.OutOfMemoryError: Java heap space

b. 解决:参考https://blog.csdn.net/guohecang/article/details/52088117

如有问题,请留言回复!

数据挖掘:基于Spark+HanLP实现影视评论关键词抽取(1)的更多相关文章

  1. 31页PPT:基于Spark的移动大数据挖掘

    31页PPT:基于Spark的移动大数据挖掘 数盟11.16 Data Science Meetup(DSM北京)分享:基于Spark的移动大数据挖掘分享嘉宾:张夏天(TalkingData首席数据科 ...

  2. 大数据实时处理-基于Spark的大数据实时处理及应用技术培训

    随着互联网.移动互联网和物联网的发展,我们已经切实地迎来了一个大数据 的时代.大数据是指无法在一定时间内用常规软件工具对其内容进行抓取.管理和处理的数据集合,对大数据的分析已经成为一个非常重要且紧迫的 ...

  3. 苏宁基于Spark Streaming的实时日志分析系统实践 Spark Streaming 在数据平台日志解析功能的应用

    https://mp.weixin.qq.com/s/KPTM02-ICt72_7ZdRZIHBA 苏宁基于Spark Streaming的实时日志分析系统实践 原创: AI+落地实践 AI前线 20 ...

  4. 基于 Spark 的文本情感分析

    转载自:https://www.ibm.com/developerworks/cn/cognitive/library/cc-1606-spark-seniment-analysis/index.ht ...

  5. 基于Spark的电影推荐系统(实战简介)

    写在前面 一直不知道这个专栏该如何开始写,思来想去,还是暂时把自己对这个项目的一些想法 和大家分享 的形式来展现.有什么问题,欢迎大家一起留言讨论. 这个项目的源代码是在https://github. ...

  6. 基于Spark的电影推荐系统(推荐系统~1)

    第四部分-推荐系统-项目介绍 行业背景: 快速:Apache Spark以内存计算为核心 通用 :一站式解决各个问题,ADHOC SQL查询,流计算,数据挖掘,图计算 完整的生态圈 只要掌握Spark ...

  7. 京东基于Spark的风控系统架构实践和技术细节

    京东基于Spark的风控系统架构实践和技术细节 时间 2016-06-02 09:36:32  炼数成金 原文  http://www.dataguru.cn/article-9419-1.html ...

  8. 基于Spark ALS构建商品推荐引擎

    基于Spark ALS构建商品推荐引擎   一般来讲,推荐引擎试图对用户与某类物品之间的联系建模,其想法是预测人们可能喜好的物品并通过探索物品之间的联系来辅助这个过程,让用户能更快速.更准确的获得所需 ...

  9. 【基于spark IM 的二次开发笔记】第一天 各种配置

    [基于spark IM 的二次开发笔记]第一天 各种配置 http://juforg.iteye.com/blog/1870487 http://www.igniterealtime.org/down ...

随机推荐

  1. Vue axios发送Http请求

    axios 1.cnpm install axios --save 2.在vue文件中引入,import Axios from 'axios' 3.使用,Axios.get(url).then((re ...

  2. EFCore扩展Update方法(实现 Update User SET Id=Id+1)

    EFCore扩展Update方法(实现 Update User SET Id = Id + 1) 源码地址(github) 前言 EFCore在操作更新的时候往往需要先查询一遍数据,再去更新相应的字段 ...

  3. 第十二篇 os模块

    Python的os模块提供了系统相关,目录,文件操作,执行命令等操作. 1.文件和目录操作相关的方法: 方法 说明 os.mkdir 创建目录 os.rmdir 删除目录 os.rename 重命名 ...

  4. python函数作用域,闭包,装饰器

    第一:函数作用域: L:local 函数内部作用域 E:enclosing       函数内部与内嵌函数之间(闭包) G:global            全局作用域 B:build_in    ...

  5. 【python】使用python smtplib库发邮件添加cc,bcc

    #!/usr/bin/env python# -*- coding: utf-8 -*- '''@author@mail @date 2017/03/16 发送邮件'''import smtplibf ...

  6. Django rest framework框架——APIview源码分析

    一.什么是rest REST其实是一种组织Web服务的架构,而并不是我们想象的那样是实现Web服务的一种新的技术,更没有要求一定要使用HTTP.其目标是为了创建具有良好扩展性的分布式系统. 可用一句话 ...

  7. checkbox的常见问题

    1.在使用checkbox时,最好不要阻止他原有的事件,要利用它原有的事件进行控制 2.尽量使用label for属性,不要对input元素的父元素或者input本身绑定事件,这样不能有效的避免冒泡事 ...

  8. 加快npm包安装的方法

    一直以来都感觉使用npm安装包的速度特别的慢,但是由于npm上面的包比较齐全,所以一直在忍受这种安装依赖包的折磨. 不过这种折磨,到今天为止应该是可以结束了,在知乎看到一个这样的说法: 用npmins ...

  9. MYSQL 面试常见问题汇总

    转自: https://blog.csdn.net/u011010851/article/details/80074550 Q1:主键,外键,超键,候选键:主键    对表中数据进行唯一标识的数据列的 ...

  10. npm 安装 sass=-=-=

    先按照 cnpm  .....因为外网安不上... cnpm install node-sass --save-dev cnpm install sass-loader --save-dev