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

 def extractFilmKeyWords(algorithm: String): Unit ={
// 测试
println(HanLPSpliter.getInstance.seg("如何看待《战狼2》中的爱国情怀?")) val sc = new SparkContext(new SparkConf().setAppName("extractFileKeyWords").set("spark.driver.maxResultSize", "3g")) val baseDir = "/work/ws/video/parse/key_word" import scala.collection.JavaConversions._
def extractComments(sc: SparkContext, inputInfo: (String, String)): RDD[(String, List[String])] = {
sc.textFile(s"$baseDir/data/${inputInfo._2}")
.map(data => {
val json = JSONObjectEx.fromObject(data.trim)
if(null == json) ("", List())
else{
val id = json.getStringByKeys("_id")
val comments: List[String] = json.getArrayInfo("comments", "review").toList
val reviews: List[String] = json.getArrayInfo("reviews", "review").toList
val titles: List[String] = json.getArrayInfo("reviews", "title").toList
val texts = (comments ::: reviews ::: titles).filter(f => !CleanUtils.isEmpty(f))
(IdBuilder.getSourceKey(inputInfo._1, id), texts)
}
})
} // 广播停用词
val filterWordRdd = sc.broadcast(sc.textFile(s"$baseDir/data/stopwords.txt").map(_.trim).distinct().collect().toList) def formatOutput(infos: List[(Int, String)]): String ={
infos.map(info => {
val json = new JSONObject()
json.put("status", info._1)
try{
json.put("res", info._2)
} catch {
case _ => json.put("res", "[]")
}
json.toString.replaceAll("[\\s]+", "")
}).mkString(" | ")
} def genContArray(words: List[String]): JSONArray ={
val arr = new JSONArray()
words.map(f => {
val json = new JSONObject()
json.put("cont", f)
arr.put(json)
})
arr
} // 基于LTP分词服务
def splitWordByLTP(texts: List[String]): List[(Int, String)] ={
texts.map(f => {
val url = "http://dev.content_ltp.research.com/ltp"
val params = new util.HashMap[String, String]()
params.put("s", f)
params.put("f", "json")
params.put("t", "ner")
// 调用LTP分词服务
val result = HttpPostUtil.httpPostRetry(url, params).replaceAll("[\\s]+", "")
if (CleanUtils.isEmpty(result)) (0, f) else {
val resultArr = new JSONArray() val jsonArr = try { JSONArray.fromString(result) } catch { case _ => null}
if (null != jsonArr && 0 < jsonArr.length()) {
for (i <- 0 until jsonArr.getJSONArray(0).length()) {
val subJsonArr = jsonArr.getJSONArray(0).getJSONArray(i)
for (j <- 0 until subJsonArr.length()) {
val subJson = subJsonArr.getJSONObject(j)
if(!filterWordRdd.value.contains(subJson.getString("cont"))){
resultArr.put(subJson)
}
}
}
}
if(resultArr.length() > 0) (1, resultArr.toString) else (0, f)
}
})
} // 基于AC双数组搭建的分词服务
def splitWordByAcDoubleTreeServer(texts: List[String]): List[(Int, String)] ={
texts.map(f => {
val splitResults = SplitQueryHelper.splitQueryText(f)
.filter(f => !CleanUtils.isEmpty(f) && !filterWordRdd.value.contains(f.toLowerCase)).toList
if (0 == splitResults.size) (0, f) else (1, genContArray(splitResults).toString)
})
} // 内存加载AC双数组
def splitWordByAcDoubleTree(texts: List[String]): List[(Int, String)] ={
texts.map(f => {
val splitResults = HanLPSpliter.getInstance().seg(f)
.filter(f => !CleanUtils.isEmpty(f) && !filterWordRdd.value.contains(f.toLowerCase)).toList
if (0 == splitResults.size) (0, f) else (1, genContArray(splitResults).toString)
})
} // TextRank
def splitWordByTextRank(texts: List[String]): List[(Int, String)] ={
texts.map(f => {
val splitResults = HanLP.extractKeyword(f, 100)
.filter(f => !CleanUtils.isEmpty(f) && !filterWordRdd.value.contains(f.toLowerCase)).toList
if (0 == splitResults.size) (0, f) else {
val arr = genContArray(splitResults)
if(0 == arr.length()) (0, f) else (1, arr.toString)
}
})
} // 互信息
def splitWordByMutualInfo(texts: List[String]): List[(Int, String)] ={
texts.map(f => {
val splitResults = HanLP.extractPhrase(f, 50)
.filter(f => !CleanUtils.isEmpty(f) && !filterWordRdd.value.contains(f.toLowerCase)).toList
if (0 == splitResults.size) (0, f) else {
val arr = genContArray(splitResults)
if(0 == arr.length()) (0, f) else (1, arr.toString)
}
})
} // 提取分词信息
val unionInputRdd = sc.union(
extractComments(sc, SourceType.DB -> "db_review.json"),
extractComments(sc, SourceType.MY -> "my_review.json"),
extractComments(sc, SourceType.MT -> "mt_review.json"))
.filter(_._2.nonEmpty) unionInputRdd.cache() unionInputRdd.map(data => {
val splitResults = algorithm match {
case "ltp" => splitWordByLTP(data._2)
case "acServer" => splitWordByAcDoubleTreeServer(data._2)
case "ac" => splitWordByAcDoubleTree(data._2)
case "textRank" => splitWordByTextRank(data._2)
case "mutualInfo" => splitWordByMutualInfo(data._2)
} val output = formatOutput(splitResults)
s"${data._1}\t$output"
}).saveAsTextFile(HDFSFileUtil.clean(s"$baseDir/result/wordSplit/$algorithm")) val splitRDD = sc.textFile(s"$baseDir/result/wordSplit/$algorithm/part*", 30)
.flatMap(data => {
if(data.split("\\t").length < 2) None
else{
val sourceKey = data.split("\\t")(0)
val words = data.split("\\t")(1).split(" \\| ").flatMap(f => {
val json = JSONObjectEx.fromObject(f.trim)
if (null != json && "".equals(json.getStringByKeys("status"))) {
val jsonArr = try { JSONArray.fromString(json.getStringByKeys("res")) } catch { case _ => null }
var result: List[(String, String)] = List()
if (jsonArr != null) {
for (j <- 0 until jsonArr.length()) {
val json = jsonArr.getJSONObject(j)
val cont = json.getString("cont")
result ::= (cont, cont)
}
}
result.reverse
} else None
}).toList
Some((sourceKey, words))
}
}).filter(_._2.nonEmpty) splitRDD.cache() val totalFilms = splitRDD.count() val idfRdd = splitRDD.flatMap(result => {
result._2.map(_._1).distinct.map((_, 1))
}).groupByKey().filter(f => f._2.size > 1).map(f => (f._1, Math.log(totalFilms * 1.0 / (f._2.sum + 1)))) idfRdd.cache()
idfRdd.map(f => s"${f._1}\t${f._2}").saveAsTextFile(HDFSFileUtil.clean(s"$baseDir/result/idf/$algorithm")) val idfMap = sc.broadcast(idfRdd.collectAsMap())
// 计算TF
val tfRdd = splitRDD.map(result => {
val totalWords = result._2.size
val keyWords = result._2.groupBy(_._1)
.map(f => {
val word = f._1
val tf = f._2.size * 1.0 / totalWords
(tf * idfMap.value.getOrElse(word, 0D), word)
}).toList.sortBy(_._1).reverse.filter(_._2.trim.length > 1).take(50)
(result._1, keyWords)
}) tfRdd.cache()
tfRdd.map(f => {
val json = new JSONObject()
json.put("_id", f._1) val arr = new JSONArray()
for (keyWord <- f._2) {
val subJson = new JSONObject()
subJson.put("score", keyWord._1)
subJson.put("word", keyWord._2)
arr.put(subJson)
}
json.put("keyWords", arr)
json.toString
}).saveAsTextFile(HDFSFileUtil.clean(s"$baseDir/result/keyword/$algorithm/withScore")) tfRdd.map(f => s"${f._1}\t${f._2.map(_._2).toList.mkString(",")}")
.saveAsTextFile(HDFSFileUtil.clean(s"$baseDir/result/keyword/$algorithm/noScore")) tfRdd.unpersist() splitRDD.unpersist()
idfMap.unpersist()
idfRdd.unpersist() unionInputRdd.unpersist()
filterWordRdd.unpersist()
sc.stop()
}

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

 import com.google.common.collect.Lists;
import com.hankcs.hanlp.HanLP;
import com.hankcs.hanlp.seg.Segment;
import com.hankcs.hanlp.seg.common.Term;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import java.io.Serializable;
import java.util.List; public class HanLPSpliter implements Serializable{
private static Logger logger = LoggerFactory.getLogger(Act.class); private static HanLPSpliter instance = null; private static Segment segment = null; private static final String PATH = "conf/tencent_word_act.txt"; public static HanLPSpliter getInstance() {
if(null == instance){
instance = new HanLPSpliter();
}
return instance;
} public HanLPSpliter(){
this.init();
} public void init(){
initSegment();
} public void initSegment(){
if(null == segment){
addDict();
HanLP.Config.IOAdapter = new HadoopFileIOAdapter();
segment = HanLP.newSegment("dat");
segment.enablePartOfSpeechTagging(true);
segment.enableCustomDictionaryForcing(true);
}
} public List<String> seg(String text){
if(null == segment){
initSegment();
} List<Term> terms = segment.seg(text);
List<String> results = Lists.newArrayList();
for(Term term : terms){
results.add(term.word);
}
return results;
}
}

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

 import com.hankcs.hanlp.corpus.io.IIOAdapter;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI; public class HadoopFileIOAdapter implements IIOAdapter{
@Override
public InputStream open(String path) throws IOException {
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(URI.create(path), conf);
return fs.open(new Path(path));
} @Override
public OutputStream create(String path) throws IOException {
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(URI.create(path), conf);
OutputStream out = fs.create(new Path(path));
return out;
}
}

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. python-循环(while循环、for循环)

    循环:循环会重复执行循环体里面的代码,python中循环可分为while循环和for循环. break 不管循环有没有完成,立即结束循环 continue 结束本次循环,继续进行下一次循环 一.whi ...

  2. 【arc074e】RGB Sequence dp

    Description ​ 丰泽爷今天也在愉快地玩Minecraft! ​ 现在丰泽爷有一块1∗N1∗N的空地,每个格子按照顺序标记为11到NN.丰泽爷想要在这块空地上铺上红石块.绿宝石块和钻石块作为 ...

  3. 解决双击dwg文件ARX自定义实体提示代理的问题

    双击dwg文件的时候,如果没有通过注册表设置会提示代理实体. 注册表自动加载arx 注册表参考路径 R18.1 是cad版本 ACAD-9001:409 是cad的地区语言,409是英文 ,804是中 ...

  4. 宽带、ADSL、以太网、PPPoE

    作者:北极链接:https://www.zhihu.com/question/25847423/answer/31563282来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出 ...

  5. 【RMAN备份】数据库备份

    转载请注明地址. 备份片文件名通配符: %c 备份片的拷贝数 %D 位于该月中的第几天 (DD) %M 位于该年中的第几月 (MM) %F 一个基于DBID 唯一的名称,这个格式的形式为c-IIIII ...

  6. 停止memcached服务

    telnet 127.0.0.1 11211 进入memcache stats 查看pid号 退出memcache kill -9 pid号

  7. JS 为任意元素添加任意事件的兼容代码

    为元素绑定事件(DOM):有两种 addEventListener 和 attachEvent:   相同点: 都可以为元素绑定事件 不同点: 1.方法名不一样 2.参数个数不一样addEventLi ...

  8. Ubuntu Server 中实际内存与物理内存不相等的问题

    记录 来源 v2ex,提到了一个平时不是很起眼的问题,Ubuntu Server 中系统默认会占用 128M 内存,用于 CVM 内部的 kdump 服务. 科普 查看 CVM 所拥有的物理内存 通过 ...

  9. Java 简单的RPC 实现

    借用了网上某大神的例子.... 目录结构是这样的... RpcFramework 主要是两个方法.一个是暴露服务,一个为引用服务.暴露服务的主要作用是声明一个接口的实现类.可以通过socket 远程调 ...

  10. 【算法笔记】B1042 字符统计

    1042 字符统计 (20 分) 请编写程序,找出一段给定文字中出现最频繁的那个英文字母. 输入格式: 输入在一行中给出一个长度不超过 1000 的字符串.字符串由 ASCII 码表中任意可见字符及空 ...