Python
JavaScript
GoogleAppEngine

はてブのホッテントリのタイトルを要約してWebの今を見つめる

More than 5 years have passed since last update.

GoogleReaderが終焉し、SmartNews,Gunosyなど寝ててもおすすめコンテンツが降ってくるサービスが注目を集めている今、ここでひとつ自分もなんか気の利いたものを作ってみたい。

というわけで、はてブの人気エントリーから記事を引っ張ってきて、

ズバリ一行に要約するプログラムを書いてみた。

はいこれ。


要約くん

http://xiidec.appspot.com/markov.html


これを使うと・・・


けだるい猫ライオンがこの国でエリートコースに乗れると日本の真相。


こんな感じで


なぜ高学歴の差別発言が求められるのかって生産性を欲しがった話をどうするか。


今話題のニュースがごちゃ混ぜになって一行に要約される。


浜崎あゆみの件、原子炉に十分届かず 炉心溶融の差別発言が続出。


Webの今が1行で分かる!


仕組み


  1. サーバサイド(Python)で、はてなブックマーク人気エントリーのRSSを取ってくる。

  2. Javascriptで動く形態素解析tiny_segmenterで単語を分解。

  3. マルコフ連鎖というBotによく使われるアルゴリズムを用いて再構成。

だいたいこんな感じ。

Google App Engine(無料)のサーバを借りて動かしてる。

Feedを取ってくる仕組みはfeedparserで自動的にねこ画像を拾ってくるでやった時とほとんど同じ。それをクライアントに渡す。

そんでクライアントでは、受け取った文字列をTinySegmenterという魔法のようなJavascriptライブラリで単語を分解する。


今日は良い天気ですね。



今日|は|良い|天気|です|ね|。


こんなイメージ。

そしてそれをマルコフ連鎖というアルゴリズムを使って再構成する。

詳しくはWikipediaのマルコフ連鎖の記事を読んで貰えればとてもよく分からないと思うんだけど、大雑把に解説すると、


今日→は→良い→天気→です→ね→。

吾輩→は→猫→で→ある→。→名前→は→まだ→無い→。

親譲り→の→無鉄砲→で→子供→の→頃→から→損→ばかり→している。


という複数の文章があるとする。

まず最初にランダムに先頭の1語を取ってくる。

→「今日」

「今日」の次に続く単語は「は」しかない。

→「今日は」

「は」に続く単語は、「良い」と「猫」がある。ランダムに選ぶ。

→「今日は猫」

猫に続く単語は「で」しかない。

→「今日は猫で」

「で」に続く単語は「ある」と「子供」。またランダムに選ぶ。

→「今日は猫で子供」

「子供」の次はまた一択。

→「今日は猫で子供の」

「の」の次は「無鉄砲」と「頃」。

→「今日は猫で子供の無鉄砲」

「無鉄砲」の次はまた「で」一択。

→「今日は猫で子供の無鉄砲で」

そろそろ終わりにしよう。

→「今日は猫で子供の無鉄砲である。」

なんかそれっぽい文章になった。

本当はマルコフ連鎖はもうちょっと奥深い。難しい数式が出てくる。

理屈はどうであれ結果的にそれっぽくなれば良しとしよう。


ソース

サーバ側のソースがこれ。


markov.py

#!/usr/bin/env python

# -*- coding: utf-8 -*-
import webapp2
import os
from google.appengine.ext.webapp import template
from xml.etree.ElementTree import *
import re

import urllib

class Markov(webapp2.RequestHandler):
def get(self):
mes=""
if self.request.get('mode')=="2ch":
mes=self.get_2ch()
else:
mes=self.get_hotentry_title()

template_values={
'mes':mes
}
path = os.path.join(os.path.dirname(__file__), 'html/markov.html')
self.response.out.write(template.render(path, template_values))

def get_hotentry_title(self):
titles = ""
tree = parse(urllib.urlopen('http://feeds.feedburner.com/hatena/b/hotentry'))
for i in tree.findall('./{http://purl.org/rss/1.0/}item'):
titles+= re.sub("[-:|/|:].{1,30}$","",i.find('{http://purl.org/rss/1.0/}title').text) + "\n"
return titles

def get_2ch(self):
titles = ""
response = urllib.urlopen('http://engawa.2ch.net/poverty/subject.txt')
html = unicode(response.read(), "cp932", 'ignore').encode("utf-8")
for line in html.split("\n"):
if line != "":
titles+=re.sub("\(.*?\)$","",line.split("<>", 2)[1])+ "\n"
return titles

app = webapp2.WSGIApplication([
('/markov.html', Markov)
], debug=True)


Markovクラスのgetメソッドが、ユーザーがアクセスしてきた時に仕事するメソッド。

get_hotentry_title()ではてブの人気エントリの一覧を取ってきて、markov.htmlに渡す。

RSSの取得にはElementTreeを使ってる。なんかGAE上でfeedparser使うの面倒そうだったから。

get_2ch()はおまけ機能。はてブのエントリの代わりに2chのスレを拾ってくる。URLの末尾に「?mode=2ch」って加えると2chモード。こんな風にパラメータによって取ってくる情報を変える機能を充実させれば夢が広がりそう。


re.sub("[-:|/|:].{1,30}$”,””,~~~)


このre.subという謎の記述。

これによって余計なノイズを省いてる。


◯◯を△△するたったひとつの冴えたやりかた100選 - ××ブログ


こんなタイトルの「- ××ブログ」を削除してシンプルにする。

続いてはクライアントサイド。


markov.html


<html>
<head>
</head>
<body style="">
<p>&nbsp;</p>
<p>
<meta charset="UTF-8">
<title>要約くん</title>
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.css" />
<script type="text/javascript" src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
<script type="text/javascript" src="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.js"></script>
<script type="text/javascript" src="jscss/tiny_segmenter-0.1.js" charset="UTF-8">
</script> <script type="text/javascript">
var segmenter
$(function(){
segmenter = new TinySegmenter();// インスタンス生成
})
//実行
function doAction(){
var wkIn=$("#txtIN").val()//インプット
var segs = segmenter.segment(wkIn); // 単語の配列が返る
var dict=makeDic(wkIn)
var wkbest=doShuffle(dict);
for(var i=0;i<=10;i++){
wkOut=doShuffle(dict).replace(/\n/g,"");
if(Math.abs(40-wkOut.length)<Math.abs(40-wkbest.length)){
wkbest=wkOut
}
}

$("#txtOUT").val(wkbest);//アウトプット

}
//文章をシャッフル
function doShuffle(wkDic){
var wkNowWord=""
var wkStr=""
wkNowWord=wkDic["_BOS_"][Math.floor( Math.random() * wkDic["_BOS_"].length )];
wkStr+=wkNowWord;
while(wkNowWord != "_EOS_"){
wkNowWord=wkDic[wkNowWord][Math.floor( Math.random() * wkDic[wkNowWord].length )];
wkStr+=wkNowWord;
}
wkStr=wkStr.replace(/_EOS_$/,"。")
return wkStr;
}
//辞書に追加
function makeDic(wkStr){
wkStr=nonoise(wkStr);
var wkLines= wkStr.split("。");
var wkDict=new Object();
for(var i =0;i<=wkLines.length-1;i++){
var wkWords=segmenter.segment(wkLines[i]);
if(! wkDict["_BOS_"] ){wkDict["_BOS_"]=new Array();}
if(wkWords[0]){wkDict["_BOS_"].push(wkWords[0])};//文頭

for(var w=0;w<=wkWords.length-1;w++){
var wkNowWord=wkWords[w];//今の単語
var wkNextWord=wkWords[w+1];//次の単語
if(wkNextWord==undefined){//文末
wkNextWord="_EOS_"
}
if(! wkDict[wkNowWord] ){
wkDict[wkNowWord]=new Array();
}
wkDict[wkNowWord].push(wkNextWord);
if(wkNowWord=="、"){//「、」は文頭として使える。
wkDict["_BOS_"].push(wkNextWord);
}
}

}
return wkDict;
}

//ノイズ除去
function nonoise(wkStr){
wkStr=wkStr.replace(/\n/g,"。");
wkStr=wkStr.replace(/[\?\!?!]/g,"。");
wkStr=wkStr.replace(/[-||::・]/g,"。");
wkStr=wkStr.replace(/[「」()\(\)\[\]【】]/g," ");
return wkStr;
}
</script> </meta>
<div data-role="page" id="first">
<div data-role="content">

<p>今ネット上で話題の記事を一行で要約すると・・・</p>
<p><textarea cols="60" rows="8" name="txtIN" id="txtIN" style="max-height:200px;">{{ mes }}</textarea></p>
<input type="button" name="" value="生成" onClick=" doAction()"></br>
<textarea cols="60" rows="8" name="txtIN" id="txtOUT"></textarea>
<p></p>

</div>
</div>
</body>
</html>


うわーゴチャゴチャだ。

まずはdoAction()、これがメイン関数。

segmenter.segment(wkIN)で受け取った文字列をバラバラに分解。

それを元にmakeDic()で文の繋がりの辞書を作る。

あとはdoShuffle()で10回混ぜ混ぜして、40文字に一番近い文字列を採用。

完成。

お好みに合わせて、Webから取ってくる情報を変えたり、混ぜた文章の評価基準を変えることで色々改良できそう。


まとめ

あんまり実用性がない。