Paradigm Shift Design

ISHITOYA Kentaro's blog.

サッカー記事の一覧サイトを作った(Gensimを利用して類似記事を表示するまでの記録)

はじめに

個人的に、サッカーのニュースや2chまとめ記事を一覧できるサイトを結構な頻度で見る。

おそらくは、単にRSSでデータ引っこ抜いてきて一覧で表示しているだけのサイトなんだれども、ニュースとまとめBlogとかの記事が別れていたり、自分の好きなニュースソースがなかったりするのがずっと不満だった。

昔みたいにRSSリーダーRSSを登録して読めばいいんだけれど、なんだかそういう習慣もなくなってしまっていて、RSSリーダーを探すところから始めるのもなーと。

そんなことを思いつつ1年ぐらい過ごしていたんだけれど、なんだか最近プログラムを書く機会もないし、自然言語界隈で新しいライブラリも出てきてるみたいだし、ということで、勉強がてら自分で作って見た。

使われることはないだろうとは思いつつも、ソースコード使ったら教えてもらえると嬉しいです。

なにかの役に立てばと思うので、勉強の成果を残します。

やりたいこと

  1. サッカーに関連する、ニュースや2chまとめサイト、個人ブログなどの記事を一覧できる
  2. ある記事に関連する記事を自動で抽出して、表示してくれる

やったこと

やりたいことが明確だったので、下調べしてフィージビリティとるとかしないで、

  1. RSSの収集
  2. 記事の取得
  3. 本文およびメイン画像抽出
  4. 本文の形態素解析
  5. 類似文書の計算
  6. クローラサーバの構築
  7. Webサイトの構築

という流れで順にやった。

昔書いたスクリプトがあるとはいえ、全体で10日(40時間程度)でできちゃったのがびびった。 ググればライブラリが出てくるわ出てくるわで、人類の進化をマジで感じた。

RSSの収集

今は、どこのサイトもRSSついてるのね。RSSリーダー使っている人まだいるんだろうか。それとも誰も使わないけどなんとなくあるんだろうか。 収集したものをSpreadsheetにまとめて、面倒くさいからSQLを構築できるようにした

記事の取得

Scrapyで取得。

特筆すべきことはないけれど、昔作ったcrawlerのスクリプトが手元にあったので改変して利用。 Python2時代に描いたやつなのでwarningが色々出るけど動くから無視。

基本はDBにあるcrawler_jobを登録していって、crawlerのプロセスがjobを一個とって処理する感じの実装になっている。 新しいサービスとかあるんでしょうけれど、目的は類似記事の抽出なので、追求せず放置。

RSSからの記事の取得周りとか、昔はRSS0.91/1.0/2.0、Atomに対応するために別々のライブラリを使ったり、なんなら自分で記述してたけど、pip install feedparserして、

rss = feedparser.parse(rawdata)

for entry in rss.entries:
      article = FootballArticleItem()
      article['title'] = entry.title
      article['summary'] = entry.summary if 'summary' in entry else ""
      article['creator'] = entry.author if 'author' in entry else ""

      article['url'] = entry.link
      article['hash'] = article_hash
      article['subject'] = " ".join(map(str, entry.tags)) if 'tags' in entry else ""
      article['feed_id'] = self.crawler_job.feed_id
      if 'updated_parsed' in entry:
          article['published_at'] = datetime.fromtimestamp(mktime(entry.updated_parsed))
      elif 'published_parsed' in entry:
          article['published_at'] = datetime.fromtimestamp(mktime(entry.published_parsed))
      article['scraped_at'] = self.crawler_job.started_at

      if 'published_at' in article and article['published_at'] > datetime.now():
          article['published_at'] = datetime.now()

みたいにサクッとかけちゃうのびびった。世の中進化してる。本当にすごい。

本文およびメイン画像抽出

本文抽出

記事が保存できてもHTMLなので、そこから形態素解析するためには本文抽出しないといけない。 自分で書かないといけないのかと思ってたら、色々とライブラリがあった。すごい。

https://moz.com/devblog/benchmarking-python-content-extraction-algorithms-dragnet-readability-goose-and-eatiht/ https://orangain.hatenablog.com/entry/content-extraction-from-html-in-python に書いてある全てのライブラリを試してみた。

あるわあるわ。すげーね。で、2chまとめサイトにかけてみたら...どれもダメ。全然ダメ。 英語圏の本文抽出エンジンだから日本語には適用できないんだなー、どうしようもないなーとおもってたら、日本人が書いてるのがあった。 http://labs.cybozu.co.jp/blog/nakatani/2007/09/web_1.html https://github.com/yono/python-extractcontent でも、日付をみたら2007...orz

でも、2015にpython3用に書き直している人がいた。いや、いらっしゃった。

https://github.com/kanjirz50/python-extractcontent3

人類は確実に進化している。動かしたら、他のライブラリよりもずっといい結果(目視です)だったので、こちらを採用。dragnetは英語の記事の形態素解析に利用することにした。ありがたやありがたや。

if feed.language == 'ja':
    extractor.analyse(content)
    text, title = extractor.as_text()
else:
    text = extract_content(content)

書いたコード、これだけですよ。びびるわそんなん。俺のためにライブラリ書いてくれてたのかとか錯覚するわ。

用途がはっきりしてるなら、最近だと

とか使えばいいのかもしれないけどね。

メイン画像抽出

メイン画像の抽出はいいライブラリが見つけられなかった(ググり力)ので、適当に書いた。

http://effbot.org/zone/pil-image-size.htm でネット上の画像のヘッダ部分だけをDLしてPillowでサイズ取得みたいなのをやっている人がいたので、それを利用。同じ画像を処理するときにはWebにアクセスしなくていいようにdiskcacheでキャッシュするようにした。

メイン画像の取得部分は以下の通り。アルゴリズム的には

  1. 画像のURLを取得
  2. ゴミ除去
  3. サイズ取得してきて、大きさとアスペクト比を比較。
  4. 大きくて四角に近い画像をメイン画像とする

みたいなシンプルなもの。なんかちゃんととれないけど、Webサイトにした時の見た目的に必要なだけで、本質とは関係ないのでいいやー、って感じ。本当は、記事上の位置とか調べたほうがいいんだけど、HTMLから取得しようとするの無理があるだろって思うので、もういい。

primary_image_url = None

for image_url in bs.find_all('img'):
    src = image_url.get('src')

    if not src or re.match('data:image', src):
        continue

    if not re.match('http', src):
        src = urljoin(article.url, src)

    if re.search('common|share|button|footer|header|head|logo|menu|banner|parts|thumbnail|ranking|icon|copyright|feedly|ico|seesaablog.gif|fan_read.gif|fan_received.gif|captcha|/n.gif|/u.gif|chart.apis.google.com|images-amazon.com|facebook.com|powered_by|rss.rssad.jp|blank|navi|custom.search.yahoo.co.jp|pixel|xrea.com|w=64|i2i|microad.jp|resize.blogsys.jp|b.hatena.ne.jp|accesstrade.net|poweredby|scorecardresearch.com|ssc.api.bbc.com|sa.bbc.co.uk|amazon-adsystem.com|zero-tools.com|clicktrack2.ziyu.net|nakanohito.jp|pv.geki.jp|arrow_left|arrow_right|spacer.gif|spike.png|wp-content/themes', src):
        continue
    print("  " + src)
    width, height = getsizes(src, dc)
    if not width:
        continue
    square = width * height
    aspect_ratio = width / height
    if square > max and aspect_ratio > 0.5 and aspect_ratio < 1.8:
        primary_image_url = src
        max = square

本文の形態素解析

本文が抽出できたので次は形態素解析だ!とおもって"python 形態素解析"で検索したら、

Python初心者が1時間以内にjanomeで形態素解析できた方法とかいうQiitaの記事が出てきた。janome...mecabから進化したのかーと思い、とりあえずインストールしてつかってみたら、Python janomeのanalyzerが便利とかにあるように、とても便利だった。

自分で書かなきゃいけない分量がかなり減っていて、これまた人類の進化を感じるわけであります。

class FootballCompoundNounFilter(TokenFilter):
    def apply(self, tokens):
        _ret = None
        re_katakana = re.compile(r'[\u30A1-\u30F4]+')
        for token in tokens:
            parts = token.part_of_speech.split(',')
            if _ret:
                ret_parts = _ret.part_of_speech.split(',')
                if parts[0] == u'名詞' and not parts[1] == 'u固有名詞' and ret_parts[0] == u'名詞' and not ret_parts[1] == u'接尾':
                    _ret.surface += token.surface
                    _ret.part_of_speech = u'名詞,複合,*,*'
                    _ret.base_form += token.base_form
                    _ret.reading += token.reading
                    _ret.phonetic += token.phonetic
                else:
                    ret = _ret
                    if parts[0] == u'名詞' and parts[1] == u'固有名詞':
                        yield token
                    else:
                        _ret = token
                    yield ret
            else:
                _ret = token
        if _ret:
            yield _ret

class FootballNounFilter(TokenFilter):
    def apply(self, tokens):
        for token in tokens:
            parts = token.part_of_speech.split(',')
            if re.match('[0-9]+', token.surface):
                continue
            if parts[0] == u'名詞' and parts[1] == u'非自立':
                continue
            if parts[0] == u'名詞' and parts[1] == u'接尾':
                continue
            if re.search('[0-9]+([年月日分]|ゴール)$', token.surface):
                continue
            if re.search('[0-9]+$', token.surface):
                continue
            if token.surface in [u'次ページ']:
                continue
            yield token

こんな感じのフィルタを書いて、

char_filters = [UnicodeNormalizeCharFilter(), RegexReplaceCharFilter('&[^&]+;', '')]
tokenizer = Tokenizer(mmap=True)
token_filters = [FootballCompoundNounFilter(), FootballNounFilter(), POSKeepFilter('名詞')]
analyzer = Analyzer(char_filters, tokenizer, token_filters)

みたいに初期化したら、

tokens = list(analyzer.analyze(content))

みたいなコードで形態素解析できちゃうんですよ...すごい。なおフィルタの中身は、なんか適当です。

ちなみに、英語はNLTKで、

from nltk.tokenize import word_tokenize
words = word_tokenize(content)

ですよ。なんなんだ、この時代は。

類似文書の計算

形態素解析できたら、Doc2Vecに食わせて類似度計算です。

ここまで正味2日(16時間)くらいできたので、マジでやばい感じに脳内麻薬が出てしまっていました。 びびる。

Python と gensim で doc2vec を使うとか読むと、なにやら、TaggedDocumentの配列を用意して、Doc2Vecインスタンス作って、trainingして、類似する文書を取得するだけみたいで、マジかよ感が。

results = session.query(ArticleContents, Articles, Feeds).filter(Articles.hash == ArticleContents.article_hash, Articles.feed_id == Feeds.id, ArticleContents.extracted_content != None).order_by(ArticleContents.id).all()

for result in results:
    try:
        article_content, article, feed = result
        if article.hash in trainings:
            continue
        if not article_content.extracted_content:
            continue
        print('  ' + article.url)
        content = article_content.extracted_content
        words = []
        if feed.language == "ja":
            tokens = list(analyzer.analyze(content))
            for token in tokens:
                words.append(token.surface)
                #print(token)

        elif feed.language == "en":
            words = word_tokenize(content)
        trainings[article.hash] = TaggedDocument(words, tags=[article.hash])

みたいな感じでTaggedDocumentの配列(ここではキャッシュのためにDictionary)作って、

m = Doc2Vec(documents=trainings.values(), dm=1, vector_size=500, window=5, min_count=2, sample=8, alpha=0.1, epochs=55)
m.save("doc2vec.model")

ってして、

m = Doc2Vec.load('doc2vec.model')
similar_articles = m.docvecs.most_similar(article.hash, topn=20)

これだけで、類似する記事が出てくる。topnは出力する記事数ね。デフォルトだと10出力されます。

Doc2Vecのオプションに関しては、min_countとwindow/sampleを元となる文書群の規模に応じて変える必要がありそう。今の所4000記事とかしかないので、このくらいの数字じゃないといい感じに出てこない。 形態素解析の段階でゴミをどれだけ除去できるかも影響が大きい。

そんなことよりも!!!!! 4000記事のtrainingに10秒とかしかかかんないのなんなの!!!!

人類やばい。 類似度計算まで3日...24時間で終わっちゃうとかなんなんだ。

クローラサーバの構築

ここは、昔使ってたスクリプトを流用して構築した。 AWSを使ってます。

  • クローラサーバ2台 (micro)
  • 解析用サーバ1台 (small)
  • Web用サーバ1台 (micro)
  • RDS (small)

で、Route53でドメイン取って、ALB置いて、構築しました。 terraformとか使おうかなって思ったけど、そこ本質じゃないしやめた。

Webサーバはphp71-fpmとnginxで構築した。

Webサイトの構築

Webサイトは、こだわりポイントもあまりないので、PHPで記述した。

なんか適当なルーター(Klein)と適当なテンプレートエンジン(Twig)をつかって、見た目はBootstrapで。

記事ごとに、メイン画像を抽出しておいたので、それを表示したいなとおもったけれど、自前で画像を取得してきてリサイズしてS3に置くとか面倒臭かったので、そういうproxyがないか調べたら、あった。

で、上にもあるようにせっかくAWSSSL使えるのに、画像がhttpだとワーニングがたくさん出るので、Images.weserv.nlを使うことにした。

Faviconを表示したいなーと思ってググったらFaviconを取得するというのを見つけた。

http://www.google.com/s2/favicons?domain=www.yahoo.co.jp

だけとかびびるわ。

所感

昔、hatenaの記事を形態素解析して、トピック抽出しようとしていたことがありました。

もう10年前なのか...このとき、形態素解析してnumpy使って次元縮約して、自分でトピック抽出しようとしていました。あるユーザーがよく見るWebサイトの情報を収集してトピック抽出して、新しいWeb記事をサジェストしてくれるサービスにすれば儲かるんじゃないかっておもって。そのあと、GunosyとかSmartNewsとかでてきたんだけど、あの頃は、類似文書の出力とか、一部のスーパープログラマー達にしか実現することができなかったし、それこそ自然言語についての論文を広く読んでなかったらできなかった。

それが、類似度の計算までたったの24時間で到達できて、適当にWebシステムにして公開するまでに正味40時間ですよ。

もう、なんだか、今のプログラマ達は別の世界に生きているんだなって、痛感した。

とりあえず目的のものはできたので、あとはまた気が向いたら色々やってみようかなーって感じです。

まぁ、何かの役に立てば幸いです。

使ったもの

python

PHP

フロントは適当なので、てなりで書けるPHP

その他

php56-httpのインストール

I've run into problem installing php56-http with command

brew install php56-http
==> Installing php56-http
==> Downloading http://pecl.php.net/get/pecl_http-2.1.2.tgz
Already downloaded: /Library/Caches/Homebrew/php56-http-2.1.2.tgz
==> PHP_AUTOCONF="/usr/local/opt/autoconf/bin/autoconf" PHP_AUTOHEADER="/usr/loc
==> mkdir -p ext/raphf
==> mkdir -p ext/propro
==> ./configure --prefix=/usr/local/Cellar/php56-http/2.1.2 --with-php-config=/u
==> make
                        case CURLSSLBACKEND_QSOSSL:
                             ^
2 errors generated.
make: *** [php_http_client_curl.lo] Error 1
make: *** Waiting for unfinished jobs....

After googling, I found Cannot install php56-http · Issue #1429 · Homebrew/homebrew-php · GitHub

Open formula with command,

brew edit php56-http

Edit url and sha1.

url 'http://pecl.php.net/get/pecl_http-2.1.4.tgz'
sha1 'bcd2b925207ba06aa31608bd0b20008093caa61f'

And then run install command again.

brew install php56-http

クロスブラウザな画像マスク手法(FadeIn/FadeOut可能)

最近、クロスブラウザで画像と動画にマスクをかけるという案件がありまして、IEに呪いの言葉をぶつけながら解決したので、そのメモです。

ちなみに私は、フロントエンドエンジニアではなく、バックエンドエンジニアでもなく、ただのプログラマなのでCSSまわりとかおかしいところあるかもしれませんが。

なお、当記事は、The Nitty GrittyというサイトのChristian Schaeferさんが書いたThe Nitty Gritty: CSS Masks – How To Use Masking In CSS Nowという記事をものすごく参考にしています。

はじめに

なにをしたいかは、マスクテスト (Masking Test Script)を見ていただければ分かります。
(サンプルの画像がなぜネコ科のアレかというと、Schaeferさんの記事があのネズミだからです)

つまり
「画像を2枚重ねて、上側の画像を任意の形にマスキングして透過し、下側の画像が見えるようにする」
ということで、あらかじめ画像を用意できればいいんですが、そうでない場合いろいろごにょごにょせにゃならんということで。
github:masking-testにコードがおいてあるので、サンプルのマスクテスト (Masking Test Script)と見比べながら読んでみるとよくわかると思います...

画像のマスキング

画像のマスキングそのものは、さっきのSchaeferさんの記事にあるようにやればOKです。
が、いくつかバグがあります…ブラウザの。

WebKit

WebKit様は、一行でかつ完璧です。完璧!素晴らしい!素晴らしい!

-webkit-mask-image: url('../images/mask.png');

InternetExplorer 7/8

なんとIE7/IE8ではこのまま問題なく動くんですねぇ。素晴らしい。
あ、素晴らしいのはSchaeferさんであって、IEではないです。

OSX FireFox

マスクテスト (Masking Test Script)をOSXのFireFoxで開いて、Ex1の「Click to Fade In/Out 」ボタンを押してもらえれば分かるのですが、フェードというかネコ科のロボットが消えていなくなると思います。ブラウザのサイズ変えたり、上にウィンドウをかぶせてもらえれば分かるんですが、再描画のバグですかね。ちなみに、Ex1はSchaeferさんのやり方でマスキングした奴です。

clip-pathとかいうCSSプロパティがつかえるというので…

clip-path: url('../images/mask.svg#mask');

とか書いたったんですけどね…
FireFoxはIE系と同じForeignObjectを用いたマスク方法が使えるので、それを適用したのがEx2です。 四次元ポケットネコがちゃんとFadeIn/FadeOutすると思います。

InternetExplorer 9

IE9だけ挙動違うとか...爆発すればいい!けどなぁ… IE9のマスキングは、

<div style="width:400px; height:400px; filter: progid:DXImageTransform.Microsoft.Chroma(color='#00FFFF'); zoom: 1;">
  <img src="images/image.png"/>
  <!--[if lte IE 9]><img src="images/mask_mozie.png" style="display: block; margin-top: -400px;"><![endif]-->
</div>

のようにするみたいです。

親にChromaフィルタを指定して、color='#00FFFF'で指定したシアンの部分を抜いた画像が出力されます。 iemask
mask_mozie.pngはこんな感じなんで、image.pngからシアン部分を抜いたものが表示されるわけで。

で、IE9からopacityが使えるようになったようですが、それが中途半端なようでEx1/Ex2をIE9で動かしてもらえれば分かりますが、opacityが変更されると、mask_mozie.pngの色も変わってしまい色が抜けなくなってしまうみたいです… opacityをimage.pngにかけてみても変わらないため、

filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=20);

でAlphaを調整しています。この方法だとopacityと違って色が抜けるのね。
ただ、IE9で動かしてみれば分かりますが、残念ながら今のところOpacity=0になったら消えるんじゃなくて黒くなるのね…CSS設定しても何も変わらないし、仕事ではバックグラウンドが黒くてセフセフでしたので、追求してません。
原因が分かる方いらっしゃいましたら教えてください…

おわりに

IE6…? 爆発しろ!なお、Operaとか試してません。

追記

MojoliciousでPocketIOを使いつつセッションを共有する

さて念願の81忘年会、2次会まではよかったのですが、3次会は飲み過ぎてダウンしてました。 日曜日は久々に二日酔い。グロッキーで記事を書くつもりが…orz

というわけで、Goomerの技術的な話を、備忘録的にまとめて書いておきたいと思います。元記事はイベント内Likeツール、Goomer作りましたです。

Goomerは、HTML5WebSocketを使ったリアルタイムWebアプリケーションです。 WAFはMojoliciousを使っています。Mojoliciousには標準でWebSocket実装が乗っているのですが、今回は、socket.ioperl実装であるPocketIOを使っています。

あまりよく理解していなかったのですが、実際に実装をしてみるまで、WebSocket = socket.ioだと思っている僕がいました。 なので最初はMojoliciousに載っていたWebSocketを使って色々やってました。ただ、socket.ioはWebSocketが利用できないクライアントのときのフォールバック実装が豊富にあり、IE5.5からサポートしているという変態さを発揮しています。ヤバ過ぎる。Mojoliciousの方はそんなのなくて、WebSocket実装なくんばブラウザにあらず的な高慢さです。

いやまぁ、正直IE5.5とか…

とにかく、まぁ、そういうわけでMojoliciousのWebSocket実装から、PocketIOへと移行しました。そして、Mojolicious側とPocketIO側で同じセッション情報を利用したかったので、色々工夫したわけですよ。

yusukebeさんの「PocketIOのイカ娘語echoサンプル」がMojolicious + PocketIOなので参考になりそうです。が、正直perlはじめて1ヶ月経たない僕にはいろいろと理解ができず…。

特に、MojoliciousとPocketIOでどうやったらセッションを共有できるのか、また、よく色々なところに「Mojoliciousのセッション機能は貧弱だからPlackのSession使いましょう」とか書いてあるんだけど、そのPlackのセッションはどうやって使うのかと!

諦めムードですよ。まじで。

そんなわけで、だらだら前置き長いですが、 「MojoliciousとPocketIOを同じアプリケーションで作成しつつ、MojoX::Sessionを使ってセッションを共有する」 サンプルをgithubのリポジトリにおきました。

perl始めて2ヶ月だし、ほぼやっつけなのでソースコードの汚さはどうか!と先に言っておきます。 以下解説です。

Mojoliciousアプリを作る

まずは、普通に

mojo generate app [app_name]

としてアプリを生成します。次に自動生成のscript/app_nameとは別に、PSGIファイルを作ります。

PSGIファイルの書き方

mojolicio.usでPocketIOを使うためにはPSGIファイルを書き換えないといけないのですが、やりかたが分からず…。
CookBookよんでも、正直分からない…
こういうのみなさんどうやって身に付けてるんだろう。

とりあえず、色々なサイトぐぐって片っ端から色々やるに、script/[app_name].psgiファイルは次のようになりました。

script/mojolicious_pocket_io.psgi

MojoX::Sessionを使う

use utf8;
use warnings;
use strict;

use Mojo::Server::PSGI;
use Plack::Builder;
use Plack::App::File;
use PocketIO;

use FindBin;
use lib "$FindBin::Bin/../lib";

use MojoliciousPocketIO;
use MojoliciousPocketIO::WebSocket;

#MojoliciousをPSGIで
my $psgi = Mojo::Server::PSGI->new( app => MojoliciousPocketIO->new );
my $app = sub { $psgi->run(@_) };

#socket.ioのスクリプトとファイルのルートディレクトリ
my $siroot = "$FindBin::Bin/../public/js/";

builder {
  #socket.ioのスクリプトとファイルをmountする
  mount '/socket.io/socket.io.js' =>
    Plack::App::File->new(file => "$siroot/socket.io.js");
  mount '/socket.io/static/flashsocket/WebSocketMain.swf' =>
    Plack::App::File->new(file => "$siroot/WebSocketMain.swf");
  mount '/socket.io/static/flashsocket/WebSocketMainInsecure.swf' =>
    Plack::App::File->new(file => "$siroot/WebSocketMainInsecure.swf");

  #PocketIOをマウント
  mount '/socket.io' =>
    PocketIO->new( class => 'MojoliciousPocketIO::WebSocket', method => 'run' )\
;
  #Mojoliciousをマウント
  mount '/' => $app;
};

のような感じになりました。
知っている人にはなんでもないんでしょうが、mountっていうのでアプリケーションをパスを振り分けることができるんですね。
あと、PocketIOのサイトをいくら探してもsocket.io.jsとswfファイルを見つけることができなかったのですが、githubのsocket.io-clientにあるんですね。ものすごい探した…orz。

まぁ、これでMojolicio.usを使った普通のアプリケーションと、PocketIOを使ったWebSocketアプリケーションが両立できます。

で、問題はここから。
MojoliciousのセッションとPocketIOのセッションを透過的に扱いたい!
というわけで、MojoX::Sessionを使いました。Plack::MiddleWare::Sessionを使ってみたかったのですが、なんかよくわからなかったのでMojoX::Sessionです。一応Mojoliciousのプラグインもあるようなので。

Mojolicious側のセッションは次のように初期化しています。lib/MojoliciousPocketIO.pm

sub setup_app{
  my $self = shift;
  my $config =
    $self->plugin('Config',
                  {file => $self->app->home->rel_file('conf/app.conf')});
  $self->secret($config->{secret});
  $self->controller_class('MojoliciousPocketIO::Controller');
  
  my $handler = DBIx::Handler->new(
    $config->{db_dsn},
    $config->{db_username},
    $config->{db_password},
    +{
      mysql_auto_reconnect => 1,
      mysql_enable_utf8 => 1,
      RaiseError => 1,
      PrintError => 0,
      AutoCommit => 1,
      on_connect_do => [
        "SET NAMES 'utf8'",
        "SET CHARACTER SET 'utf8'",
      ],
    },
  );

  $self->plugin(
    session => {
      stash_key => 'mojox-session',
      store     => [dbi => {dbh => $handler->dbh}],
      transport => 'cookie',
      expires_delta => 1209600, #2 weeks.
      init      => sub{
        my ($self, $session) = @_;
        $session->load;
        if(!$session->sid){
          $session->create;
        }
      },
    }
  );
}

みたくなってます。Handlerの初期化とかは、なんかもっといい方法あると思います。で、WebSocket側は、

WebSocketController.pmソースコードがあります。Mojolicious側と大体同じ方法でMojoX::Sessionを初期化しています。
その際、session_idが必要なのですが、

WebSocket.pmでPocketIOのENVからHTTP_COOKIEを取り出し、SIDを取得しています。

sub session_id{
  my $self = shift;
  my $socket = shift;
  my $cookie = $socket->{conn}->{on_connect_args}[0]->{HTTP_COOKIE};
  if($cookie =~ /sid=([0-9a-f]+)/){
    return $1;
  }
  return undef;
}

SIDさえ取得できれば、

$session->load($self->session_id);

として、Mojolicious側のsessionを取得することができます。
いやしかし、$socket->{conn}->{on_connect_args}[0]->{HTTP_COOKIE};というのを発見するのに1日かかった感じです…orz

ここまでくれば、Mojolicious側のMain.pm

sub set_name{
  my $self = shift;
  my $name = $self->param('name');
  $self->session(name => $name);
  $self->render(json => $name);
}

みたいな感じで、sessionにセットしたnameを、

sub onEcho{
  my ($self, $socket, $message) = @_;
  my $room = $self->session('room_id');
  my $name = $self->session('name');
  $socket->sockets->in($room)->emit('echo', $name . ' says ' . $message);
}

のようにして、使うことができます。

分かっちゃえば楽だし、もう考えなくてすむのでいいんですがね…長かった。

サンプルを動かす

サンプルは

git clone https://github.com/kent013/MojoliciousPoketIO.git
cd MojoliciousPocketIO

リポジトリを取り出して、

cpanm --installdeps .

依存ライブラリをインストールし

twiggy script/mojolicious_pocket_io.psgi -l :3000

として、サーバを立ち上げた後、

http://localhost:3000

にアクセスすれば動作確認ができます。

しょかん

ショージキ、辛かった。
っていうか、この方法があっているのかも、分かりません!
もっとスマートな方法があったら教えてください!

イベント内Likeツール、Goomer作りました

もうね、ジョーの心境ですよ。

81忘年会というイベントが毎年開催されています。
1981年生まれの僕は、初開催の時から行きたくて行きたくて仕方なかったのです。
だけども、20歳からの10年間、北海道・名古屋とすんでいた僕は参加できずにいました。

東京帰ってきたから行くぞと。楽しみは楽しみなんだけど、呑んだら死にそうな状態であります!
がんばれ!

というわけで、

で、イベントで使えるWebアプリケーションを作りました。

「Goomer」

といいます。http://g.cfe.jpで公開しています。
もっと気軽に簡単にイベント内でコミュニケーションをする方法はないか!
というuzullaさんの思いを62%くらい表現できているのではないかと思います。

f:id:kent013:20121208163724p:plain

インタフェースはこんな感じで、Goom!ボタンを連打したり、テキストでメッセージを送ったりできます。
メッセージはtwitterにも流れます。
ボタンを押すと、リアルタイムに相手に通知されます。WebSocketを使っています。

名前の横にある#20とかの数字は、id:yusukebeさんの81忘年会用自己紹介ツールの番号です。
結構、急ごしらえなので後の方の番号の人は入ってません…あしからず。

f:id:kent013:20121208163701p:plain

イベントのタイムラインはこんな感じで、イベント参加者のテキストを一覧できます。
81の紹介ツール、Twitterへ移動できるので、自分にメッセージを送ってくれた人や、番号は知っているけど名前は知らない人をフォローしたりしなかったりしましょう!

とりあえず、もう移動しないと遅刻なので、行きます。
実装的には言語はperl、Mojolicious + PocketIOで実装しています。
WebSocketに色々苦労したので、その辺の技術エントリはまた今度!

逝って参ります!

追記:MojoliciousでPocketIOを使いつつセッションを共有する

MarkDownとSyntax Highlightの相性

http://blog.ishitoya.info/entry/2012/10/17/102904

で、MarkDownをつかって書いているのですが、

perlのコード部分を
[abc][1]
```perl
my $some = 'code';
```
[abc][1]
[1]: http://abc.com "mogemoge"
とするとコード部分より上にあるリンクが効きません。

perlのコード部分を [abc][1]

my $some = 'code';

abc とするとコード部分より上にあるリンクが効きません。

バグかな。

mojoliciousでOAuth2

Hi, perl apprenticeなkent013です。

Mojolicious::Liteは何となく1ファイルに全て押し込む的な発想(単に慣れてないだけ)が嫌いなので、Mojoliciousを単体で使ってみようとか思ってます。
だけど、その辺に落ちているコードはLite用で苦労してます。
ま、勉強っつーコトで。

掲題のOAuth2ですが、mojolicious素敵だからさくっとできるんだろうなー。凄いんだろうなー!
って期待してやってみましたが、Railsomniauthほどではないけど、簡単。

1, Mojolicious::Plugin::OAuth2をインストール

Mojolicious::Plugin::OAuth2をインストールするのに、

cpanm Mojolicious::Plugin::OAuth2

ってしたら、

FAIL Installing Mojolicious::Plugin::OAuth2 failed.

とか言われちゃって、Issue上げそうになりましたが、

mojo version

したら、マイナーバージョンが違ったのでアップデートしたら直りました。あぶねぇあぶねぇw

2, startupでpluginの読み込み

libの[AppName].pmのstartupに、

$self->plugin('o_auth2',
            facebook => {
              key => '[APP Key]',
              secret => '[APP Secret'
            });  

と書いておく。Liteだと、

plugin 'o_auth2',
   facebook => {
      key => '[APP Key]',
      secret => '[APP Secret]' 
   };

って、書くみたい。大分違うので戸惑う…本当はconfファイル書くべきなんだろうけど、まだよく分からないので、ペンディング。

3, ルーティング

bridgeするなら、

my $r = $self->routes;
$r->get('/auth')->to('auth#auth');
$r->get('/login')->to('auth#login');
$r->get('/logout')->to('auth#logout');

$r = $r->bridge('/')->to(cb => sub {
  my $self = shift;

  if ($self->session('login_name')) {
    return 1;
  }
  else {
    $self->redirect_to('/login');
  }
});
$r->get('/')->to('main#top');

のように、bridgeの前にlogin/logout/authを書く。authはfacebook authしてくれる奴。
名前は適当。Liteだとunderとか使うのがいいみたい。
本当は、bridgeの中でsessionがinvalidだったらreturn 1するみたいな処理も書かないといけないとおもう。

4, 認証周り

sub auth {
  my $self = shift;
  $self->get_token('facebook', callback => sub {
    my $token = shift;
    my $ua = Mojo::UserAgent->new;
    my $me = $ua->get(
      'https://graph.facebook.com/me?access_token=' . $token)->res->json;
    $self->session('login_name' => $me->{name});
    $self->session('token' => $token);
    $self->redirect_to('/');
  });
}

sub login {
  my $self = shift;
  $self->session(expires => 1);
}

sub logout {
  my $self = shift;
  $self->session(expires => 1);
  $self->redirect_to('/login');
}

logoutはセッション消してからloginに転送するだけ。 loginはログインっぽい表示するページ authはfacebookにログインしてsessionにデータ書き込んで/へ転送する。

OAuth2プラグインのgithubページによると、

get '/auth' => sub {
  my $self=shift;
  $self->get_token('facebook',callback=>sub {
    ...
  });
};

とあって、作者のMarcusさんのEasily integrating your Mojolicious app with Facebook.という記事には、

get 'hello' => sub {
  My $self=shift;
  #redirects the gets the token asynchronous
  $self->get_token('facebook', callback => sub {
    my $token=shift;
    my $me=$self->client->get(
      'https://graph.facebook.com/me?access_token='.$token)->res->json;
    $self->render( text =>
        "Hello ".$me->{name} );
  });
}

とあるんだけど、この$self->clientがそんなメソッドないぜ!っつって怒られちゃう。
むがー!!!ってなってたら、最近のmojoliciousでは、UserAgentを使うように変更されたそうで。
Mojo::Client has been deprecateでRiedelさんがいってはりました。

5, しょかん

というわけであとは、facebookAPIキーとれば動きます。
かなり簡単。イロイロ外してるところはあるかもしれませぬが。
でもサポートしてるサービスが少ないのが玉に瑕かな。まー、追々ということで。