寻找《红楼梦》十大话唠
定义,匹配如下形式的{话语},认为是会“话”内容,其他认为非会话内容:
[冒号] [左双引号] {话语}[右双引号]
先看看结果:
1.整本书会话内容与非会话内容对比:
2. 按说话的句数(每个配对的双引号算一句)统计Top10:
3. 按说话的总字数统计Top10
基本实现思路如下:
1. 下载纯文本的《红楼梦》文件,作简单的数据清洗
将西文的冒号、双引号替换为中文的冒号、双引号;
2. 提取话语,将符合话语规则的文本提取出到一个List
3.调用NPL工具包,逐句分析包含话语的上下文,找到说话的“主语”,
让主语认领每句话语。
经过比较,选型了FNLP,教程链接附上:https://github.com/xpqiu/fnlp/wiki/QuickTutorial
使用过程中,发现语义分析结果与Demo有出入,第一个标点常常被定性为词语,不明白为啥。
经过人工检验结果,发现很多人名不能正确识别,官方没有较细致的文档。
只查到可以加载自定义字典,我定义了一些人物名到 data/dict_name.txt下,并调用API加载。
贾妃 人名 丰儿 人名 尤氏 人名 贾赦 人名 金荣 人名 吴良 人名 柳氏 人名 贾芹 人名 贾珍 人名 金桂 人名 麝月 人名 晴雯 人名 元春 人名 惜春 人名 迎春 人名 探春 人名 尤二姐 人名 尤三姐 人名 紫鹃 人名 秋纹 人名 袭人 人名 空空道人 人名 士隐 人名 薛蟠 人名 林之孝 人名 女尼 人名 有人 人名 妙玉 人名 何三 人名 鸳鸯 人名 李纹 人名 湘云 人名 长史 人名 薛蝌 人名 北静王 人名 西平王 人名 王爷 人名 赵堂官 人名 衙役 人名 倪二 人名 倪家母女 人名 雨村 人名 道士 人名 巧姐 人名 巧姐儿 人名 凤姐 人名 凤姐儿 人名 秦氏 人名 贾瑞 人名 宝玉 人名 黛玉 人名 李嬷嬷 人名 薛姨妈 人名 雪雁 人名 宝钗 人名 贾蓉 人名 李纨 人名 秦钟 人名 贾母 人名 贾琏 人名 贾兰 人名 贾芸 人名 贾环 人名 平儿 人名 邢夫人 人名 王夫人 人名 众人 人名 贾蔷 人名 丫头 人名 贾政 人名 和尚 人名 程日兴 人名 王仁 人名 婆子 人名 道
4.别名问题
贾妃 元春 贾元春 凤姐 凤姐儿 巧姐 巧姐儿
自己处理了别名映射,计数时将别名下的计数做了累加,定义在 data/map_same.txt中。
5. 工具类
package org.hl; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.fnlp.nlp.cn.CNFactory; import org.fnlp.nlp.parser.dep.DependencyTree; import org.fnlp.util.exception.LoadModelException; public class ChatParser { String fpath; String dict; String dict_same; String charSet; String str_all; int len_total; int len_chat; int len_sentence; List<String> ls_embrace; List<String> ls_sentence; List<String> ls_who; ArrayList<Entry<String,Integer>> who_said_sentence; ArrayList<Entry<String,Integer>> who_said_word; ArrayList<Entry<String,Integer>> who_said_per; CNFactory fm=null; HashMap<String,Integer> m_cout_sentence; HashMap<String,Integer> m_cout_word; HashMap<String,Integer> m_cout_per; HashMap<String,String> m_same=null; ChatParser(String fpath,String charSet,String dict,String fsame) throws Exception{ InputStreamReader insReader = new InputStreamReader( new FileInputStream(fpath), charSet); this.dict = dict; this.charSet=charSet; BufferedReader br = new BufferedReader(insReader); String line = new String(); StringBuffer sbuf = new StringBuffer(); while ((line = br.readLine()) != null) { sbuf.append(line); System.out.println(line); } br.close(); str_all=sbuf.toString(); len_total = str_all.length(); ls_embrace =new ArrayList<String>(); ls_sentence =new ArrayList<String>(); ls_who =new ArrayList<String>(); loadSame(fsame); } public String getSameKey(String key){ if(m_same==null) return key; String mk = m_same.get(key); if(mk==null) return key; else return mk; } public void loadSame(String fpath) throws Exception{ if(fpath==null) return; m_same = new HashMap<String,String>(); InputStreamReader insReader = new InputStreamReader( new FileInputStream(fpath), charSet); BufferedReader br = new BufferedReader(insReader); String line = new String(); while ((line = br.readLine()) != null) { String[] words = line.split("\\s+"); for(int i=0,len=words.length; i<len; i++){ m_same.put(words[i], line); } } br.close(); } public void wash(){ StringBuffer sbuf = new StringBuffer(); StringBuffer bf = null; len_chat=0; len_sentence=0; int pos_start=0; for(int i=0,len=str_all.length(); i<len;i++){ char ch = str_all.charAt(i); if(ch==':'|| ch==':'){ pos_start=i; }else if((ch=='“' || ch=='\"') && bf==null){ //排除非人言的双引号 if(i-pos_start>3) continue; bf = new StringBuffer(); sbuf.append('“'); }else if ((ch=='”' || ch=='\"')&& bf!=null){ String str_bf = bf.toString(); len_chat +=str_bf.length(); ls_sentence.add(str_bf); bf = null; sbuf.append('”'); ls_embrace.add(sbuf.toString()); sbuf = new StringBuffer(); }else{ if(bf!=null) bf.append(ch); else sbuf.append(ch); } } len_sentence=ls_sentence.size(); } public void parse() throws Exception{ long tm_start = new Date().getTime(); CNFactory fm = CNFactory.getInstance("models"); if(this.dict!=null) fm.loadDict(dict); String who=null; HashMap<String,Integer> mc_sentence = new HashMap<String,Integer>(); HashMap<String,Integer> mc_char = new HashMap<String,Integer>(); for(int k=0,klen=ls_embrace.size();k<klen; k++){ String str_input=ls_embrace.get(k); String[][] wc = fm.tag(str_input); DependencyTree dt =fm.parse2T(wc[0], wc[1]); //System.out.println(str_input); //System.out.println(dt); ArrayList<List<String>> ls = dt.toList(); for(int i=0,len=ls.size();i<len; i++ ){ List<String> cur = ls.get(i); if(//cur.get(3).equals("主语") && cur.get(1).equals("人名")){ who = cur.get(0); } } ls_who.add(who); System.out.println(who+":"+ls_sentence.get(k)); } long tm_span = new Date().getTime()-tm_start; System.out.println("-----------------------parse span:"+tm_span+"-----------------------------"); System.out.println(len_chat+"/"+(len_total-len_chat)+"/"+len_total+":"+ls_embrace.size()); } public void count_sort(){ long tm_start = new Date().getTime(); m_cout_sentence = new HashMap<String,Integer>(); m_cout_word = new HashMap<String,Integer>(); m_cout_per = new HashMap<String,Integer>(); for(int k=0,klen=ls_who.size();k<klen; k++){ String who = getSameKey(ls_who.get(k)); String sentence = ls_sentence.get(k); int c1=1; int c2=sentence.length(); if(m_cout_sentence.containsKey(who)){ c1+=m_cout_sentence.get(who); c2+=m_cout_word.get(who); } //System.out.println(who+":"+c1+":"+c2+":"+sentence); m_cout_sentence.put(who, c1); m_cout_word.put(who, c2); } Iterator iter = m_cout_word.entrySet().iterator(); while (iter.hasNext()) { Map.Entry<String,Integer> entry = (Map.Entry) iter.next(); String key = entry.getKey(); int val = entry.getValue()/m_cout_sentence.get(key); m_cout_per.put(key, val); } long tm_span = new Date().getTime()-tm_start; System.out.println("-----------------------count span:"+tm_span+"-----------------------------"); who_said_sentence= new ArrayList<Entry<String,Integer>>(m_cout_sentence.entrySet()); who_said_word= new ArrayList<Entry<String,Integer>>(m_cout_word.entrySet()); who_said_per= new ArrayList<Entry<String,Integer>>(m_cout_per.entrySet()); Collections.sort(who_said_sentence, new Comparator<Map.Entry<String, Integer>>() { public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) { return (o2.getValue() - o1.getValue()); } }); Collections.sort(who_said_word, new Comparator<Map.Entry<String, Integer>>() { public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) { return (o2.getValue() - o1.getValue()); } }); Collections.sort(who_said_per, new Comparator<Map.Entry<String, Integer>>() { public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) { return (o2.getValue() - o1.getValue()); } }); System.out.println("-----------------------top sentence----------------------------"); int TOP =10; int pos=0; for(Entry<String,Integer> e : who_said_sentence) { if(pos<TOP){ System.out.println("{ label:'"+e.getKey() + "',value:" + e.getValue()+"},"); } else{ System.out.println("{ label:'"+e.getKey() + "',value:" + e.getValue()+"}"); break; } pos++; } pos=0; System.out.println("-----------------------top word----------------------------"); for(Entry<String,Integer> e : who_said_word) { if(pos<TOP){ System.out.println("{ label:'"+e.getKey() + "',value:" + e.getValue()+"},"); } else{ System.out.println("{ label:'"+e.getKey() + "',value:" + e.getValue()+"}"); break; } pos++; } System.out.println("-----------------------top per----------------------------"); for(Entry<String,Integer> e : who_said_per) { if(pos<TOP){ System.out.println("{ label:'"+e.getKey() + "',value:" + e.getValue()+"},"); } else{ System.out.println("{ label:'"+e.getKey() + "',value:" + e.getValue()+"}"); break; } pos++; } } public static void main(String[] args) throws Exception { // TODO Auto-generated method stub ChatParser cp = new ChatParser("data/t1.txt", "utf-8","data/dict_name.txt","data/map_same.txt"); cp.wash(); cp.parse(); cp.count_sort(); } }
6. 分析结果的饼状图展示, 选用了d3.js的一个pie组件,非常好用
只需要给出json格式的数据,布局完全委托给该组件了。
<script> var pie = new d3pie("pie", { header: { title: { text: "对话/非对话", fontSize: 30 } }, data: { content: [ { label: "对话", value: 404217 }, { label: "非对话", value: 448962} ] } }); </script>
7. 性能
在IMac下,耗时毫秒数-----------------------parse span:84332-----------------------------
NLP的组件还不够理想,为了减轻其负担,双引号内的内容替换为空,再交给它处理的。
虽然如此,看着成群的对话风卷残云一半滚屏,有趣!
贾蓉:这样的大排场,我打量拿着妖怪给我们瞧瞧到底是些什么东西,那里知道是这样收罗,究竟妖怪拿去了没有? 贾珍:糊涂东西,妖怪原是聚则成形,散则成气,如今多少神将在这里,还敢现形吗!无非把这妖气收了,便不作祟,就是法力了。 贾珍:头里那些响动我也不知道,就是跟着大老爷进园这一日,明明是个大公野鸡飞过去了,拴儿吓离了眼,说得活象.我们都替他圆了个谎,大老爷就认真起来.倒瞧了个很爇闹的坛场。 贾赦:只怕是谣言罢.前儿你二叔带书子来说,探春于某日到了任所,择了某日吉时送了你妹子到了海疆,路上风恬浪静,合家不必挂念.还说节度认亲,倒设席贺喜,那里有做了亲戚倒提参起来的.且不必言语,快到吏部打听明白就来回我。 贾琏:才到吏部打听,果然二叔被参.题本上去,亏得皇上的恩典,没有交部,便下旨意,说是失察属员,重征粮米,苛虐百姓,本应革职,姑念初膺外任,不谙吏治,被属员蒙蔽,着降三级,加恩仍以工部员外上行走,并令即日回京.这信是准的.正在吏部说话的时候,来了一个江西引见知县,说起我们二叔,是很感激的,但说是个好上司,只是用人不当,那些家人在外招摇撞骗,欺凌属员,已经把好名声都弄坏了.节度大人早已知道,也说我们二叔是个好人.不知怎么样这回又参了.想是忒闹得不好,恐将来弄出大祸,所以借了一件失察的事情参的,倒是避重就轻的意思也未可知。 贾琏:先去告诉你婶子知道,且不必告诉老太太就是了。 王夫人:打听准了么?果然这样,老爷也愿意,合家也放心.那外任是何尝做得的!若不是那样的参回来,只怕叫那些混帐东西把老爷的性命都坑了呢!
源码参见附件,FNLP的m文件比较大,自己参考教程的下载链接下。
推荐阅读