Feedy(Python)でRSSフィードをいい感じに処理する

最近、RSSフィードをfetchしてゴニョゴニョ処理したいと思うことが多かったのですが、特に気にいるライブラリが無かった *1 のでFeedyというライブラリを作ってみました。 個人的には結構気に入っていて、便利に使えているので紹介します。

もともと欲しかった機能・特徴としては、

  • デコレータベースでシンプルに記述できる
  • 当然、前回fetchした時間からの更新分のみの取得も可
  • RSSフィードのリンク先のhtmlも自動で取得して、好きなHTMLパーサ(個人的にはBeautifulSoup4)でいい感じに処理したい

具体的には↓のように記述します

from feedy import Feedy

feedy = Feedy('./feedy.dat')  # 前回フェッチした時間とかを格納(Redisとかに自分で置き換えることも可能)

@feedy.add('https://www.djangopackages.com/feeds/packages/latest/rss/')
def djangopackages(info, body): 
    # django packagesのRSSに載っている、パッケージ名と記事へのリンクを出力する例
    print("- [%s](%s)" % info['article_title'], info['article_url'])

if __name__ == '__main__':
    feedy.run()

他にも、全部は紹介しませんがプラスαの機能として

  • デバッグ時とかは柔軟に実行時のオプションが指定できるCommand Line Interfaceも欲しい
  • ページごとにFacebookとかはてブ数も気軽に取得できる仕組みがほしい
  • ↑のような便利な機能を簡単に追加できる、プラグイン機構
  • 上記のことを同期的に処理するとそれなりに時間がかかるので、裏側ではasyncioで高速に処理しておきたい(リクエストが飛び過ぎないようにsemaphoreも指定できる)
  • HTMLをパースして文章をjanome形態素解析・各単語の出現頻度を数えた結果もほしい

Feedyを使ってみる

README頑張って書いたので、Feedyの基本的な使い方やCLIのオプションなどはGithubを見てください。 ここではとりあえずみなさんにも便利そうな使い方を3つぐらい紹介します。

  1. 記事のfacebookのいいね数、pocketの保存数、はてブ数を取得してみる
  2. 画像のURLを集めてみる
  3. 単語の出現頻度をカウントする

記事のfacebookのいいね数、pocketの保存数、はてブ数を取得してみる

プラグインは自分で書くことも出来ますが、とりあえず僕の方で作った social_share_plugin を使ってみます。 SNSでのシェア数等が簡単に習得できます。

from feedy import Feedy
from feedy_plugins import social_share_plugin

feedy = Feedy('feedy.dat')
feedy.install(social_share_plugin)

@feedy.add('http://nwpct1.hatenablog.com/rss')
def c_bata_web(info, body, social_count):
    print('=============================')
    print('Title:', info['title'])
    print('HatenaBookmark: ', social_count.get('hatebu_count'))
    print('Pocket:', social_count.get('pocket_count'))
    print('Facebook:', social_count.get('facebook_count'))

最新の記事3つ分ぐらい表示してみましょう

$ feedy example.py feedy -t c_bata_web -m 3
=============================
Title:  Pythonを使ったデータ分析に関する内容をJupyter Notebookにまとめ始めました
HatenaBookmark:  67
Pocket:  79
Facebook:  20
=============================
Title:  Golangでつくる検索エンジン(Webクローラ、MongoDB、Kagome、gin)
HatenaBookmark:  67
Pocket:  94
Facebook:  5
=============================
Title:  Python製WebフレームワークのURL DispatcherとType Hintsの活用について
HatenaBookmark:  41
Pocket:  66
Facebook:  1

成功 🎉

このブログの画像のURLを集めてみる

試しに↓のようにimgタグを全て表示してみます。

from feedy import Feedy
from bs4 import BeautifulSoup

feedy = Feedy('feedy.dat')

@feedy.add('http://nwpct1.hatenablog.com/rss')
def c_bata_web(info, body):
    soup = BeautifulSoup(body, "html.parser")
    for x in soup.find_all('img'):
        print(x)

実行すると↓の通り。

$ feedy example.py feedy -t c_bata_web -m 3 --ignore-fetched
<img alt="この記事をはてなブックマークに追加" height="20" src="https://b.st-hatena.com/images/entry-button/button-only.gif" style="border: none;" width="20"/>
<img alt="実践 機械学習システム" class="hatena-asin-detail-image" src="http://ecx.images-amazon.com/images/I/51%2BfZJOKEKL._SL160_.jpg" title="実践 機械学習システム"/>
:
<img alt="f:id:nwpct1:20160409180830p:plain" class="hatena-fotolife" itemprop="image" src="http://cdn-ak.f.st-hatena.com/images/fotolife/n/nwpct1/20160409/20160409180830.png" title="f:id:nwpct1:20160409180830p:plain"/>
:
:

はてなブックマークボタンなどノイズも混じっていますが、どうやら class="hatena-fotolife" は私がアップロードした画像のようです。 class="hatena-fotolife" で絞ってみます。

    for x in soup.find_all('img', {'class': 'hatena-fotolife'}):
        print(x['src'])

実行してみましょう

$ feedy example.py feedy -t c_bata_web --ignore-fetched
http://cdn-ak.f.st-hatena.com/images/fotolife/n/nwpct1/20160409/20160409180830.png
http://cdn-ak.f.st-hatena.com/images/fotolife/n/nwpct1/20160107/20160107173222.png
http://cdn-ak.f.st-hatena.com/images/fotolife/n/nwpct1/20160107/20160107173406.jpg
:

成功 🎉

単語の出現頻度をカウントする

はてなブックマークのITカテゴリのホットエントリーやHacker Newsの一覧から自分の興味のある記事だけ抽出したいと考えています。 そのためには、Bag-of-Wordsした結果に対してTF-IDFの計算やクラスタリングやトピックモデルなどの機械学習手法を当てはめるとよさそうです。 全部説明するのは長いので、ここではBag-of-Wordsをするところまで紹介。

from feedy import Feedy
from feedy_utils import word_counter

feedy = Feedy('feedy.dat')

@feedy.add('http://b.hatena.ne.jp/hotentry/it.rss')
def hatena_it(info, body):
    print(word_counter.count_words(body).most_common(20))  # 出現回数の多い単語を20個取得して表示

実行してみます

$ feedy example.py feedy -t hatena_it -m 3 --ignore-fetched
[('エンジニア', 583), ('paiza', 203), ('スケジュール', 129), ('人', 124), ('コミュニケーション', 90), ('ユーザー', 77), ('仕様', 75), ('jp', 73), ('tag', 72), ('B', 66), ('learning', 64), ('項目', 64), ('img', 64), ('jmp', 64), 62), ('業務', 61), ('http', 59), ('IT', 58)]
[('月', 1059), ('ユーザー', 140), ('document', 138), ('ハリス', 75), ('企業', 69), ('Google', 65), ('IT', 65), ('write', 60), ('amp', 42), ('GIGAZINE', 40), ('Facebook', 37), ('心理', 35), ('手法', 30), ('ムービー', 30), ('if','都合', 27), ('社会', 26), ('自分', 25), ('店', 25)]
[('var', 334), ('id', 327), ('ITmedia', 319), ('i', 300), ('name', 253), ('if', 217), ('position', 192), ('adRequest', 192), ('著者', 170), ('document', 167), ('d', 150), ('b', 150), ('getElementById', 149), ('div', 144), ('s',42), ('return', 140), ('ISP', 138), ('span', 136), ('js', 120), ('IT', 120)]

成功 🎉 実際には、printするのではなくscikit-learnのCountVectorizerに入れてしまったり、MongoDB等に保存しておくという使い方になるかと思います。

終わりに

プラグイン等を除いた、メインの実装は200行ぐらいで済みました feedy.py 。 今回はasyncioやaiohttpで高速化を頑張ってみたのですが、それについてはまた後日記事にまとめようとおもいます。

PythonによるWebスクレイピング

PythonによるWebスクレイピング

*1:Scrapyも検討したのですがRSSをフェッチして処理するだけにしては仕組みが少し複雑すぎるかなという印象でした

Pythonを使ったデータ分析に関する内容をJupyter Notebookにまとめ始めました

研究をかれこれ2年半ぐらい続けてきたので、研究をする中で必要になった機械学習の手法について調べたりコードを書いたりしてきたのですが、まだまだ触ったことのない機械学習の手法も多く、研究で必要になる手法以外の知識も付けたくなってきたので、勉強し始めました。

Sphinxにまとめるか悩んだのですが、「ひとまず簡単にスライドにできること」・「手元でもすぐにコードを実行できる」という理由でJupyter Notebookを使用しています。 もし誤りやタイポ等があれば、IssueやPRお待ちしております。

github.com

今のところ↓の2つについてまとめました。

ノートブックの内容一覧

内容については今後何度も変更をすると思いますが、とりあえず今の予定としては下記の内容について書く予定です。 次は「Seabornの使い方」と「クラスタリング」について書く予定。

データ加工(Data Wrangling)・可視化(Visualization)

統計(Statistics)と機械学習(Machine Learning)

Pythonのデータ分析環境の構築 with Docker

このままだと内容紹介だけになってしまうので、Dockerを使った環境構築についてメモ。

JupyterはDockerイメージを公開してくれているので、基本的にはそちらを使えばいいと思います。この時、notebooks ディレクトリを作成しておいて、そこを下記のコマンドのように共有しておくと終了した際にもNotebookが手元のマシンに残すことができます。

$ docker pull jupyter/datascience-notebook
$ docker run -p 8888:8888 -v $PWD/notebooks:/home/jovyan/work c-bata/datascience

もし自作ライブラリなどJupyterが公開しているDockerイメージの中に含まれてないライブラリ・パッケージを使用する際には下記のように jupyterのdockerfileをベースとしたコンテナを作成しましょう。

# https://github.com/c-bata/datascience-notebook/blob/master/Dockerfile
From jupyter/datascience-notebook 
MAINTAINER Masashi Shibata <contact@c-bata.link>

USER root
RUN apt-get update && \
    apt-get install -y graphviz-dev graphviz && \
    rm -rf /var/lib/apt/lists/*

USER jovyan
RUN pip install pandas-validator outlier-utils

docker buildや実行は下記のようにすればOKです。

$ docker build -t c-bata/datascience .
$ docker run -p 8888:8888 -v $PWD/notebooks:/home/jovyan/work c-bata/datascience

Jupyter NotebookはWebブラウザからアクセスするので、Dockerを使う方法は相性が良いなと感じています。

VPS上でNginxも使ってJupyter Notebookを使用する

VPSの環境がある場合、わざわざ手元で実行しなくてもVPS上で実行していれば好きなときにURLにアクセスして確認することが出来ます。 Nginxの設定は↓を参考にしてください。 /api/kernels のところを下記のように記述しないと「カーネルにつながらない」と言われ動かすことが出来ない点に注意してください。

upstream jupyter {
    server 127.0.0.1:8001;
}

server {
    server_name jupyter.hoge.com;

    location / {
        auth_basic "Restricted";
        auth_basic_user_file /etc/nginx/.htpasswd;

        proxy_set_header Host $host;
        proxy_pass       http://jupyter;
    }

    location /api/kernels {
        proxy_set_header Host $host;
        proxy_set_header Upgrade "websocket";
        proxy_set_header Connection "Upgrade";
        proxy_pass       http://jupyter;
    }
}

研究データの解析とかもここでやっておくと、教授への共有も簡単ですし、移動中でもiPad等から操作出来て良さそうです。 また運用しながら知見等が貯まれば記事にしますね。

Pythonによるデータ分析入門 第2版 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 第2版 ―NumPy、pandasを使ったデータ処理

Golangでつくる検索エンジン(Webクローラ、MongoDB、Kagome、gin)

最近、Golangを書き始めたので勉強として、1年半ほど前にPythonで作っていたWebクローラと検索エンジンGolangで実装してみた。WebフレームワークはFlaskの代わりにgin、Mecabの代わりにKagomeを使用、Datastoreは前回と同じくMognoDBを使ってます。

f:id:nwpct1:20160409180830p:plain

今回はHTML書くの面倒だったので、フロントはginでJSON返すだけにしました。

以下使ったライブラリやGoに関するメモ

可変長引数の挙動

スライスを展開して渡すときは、他の引数もスライスにappendしてから展開して渡さないとToo many argumentsで落ちるみたいです。

gore> hoge := func(args ...string) {
.....         for _, a := range args {
.....                 fmt.Println(a)
.....         }
..... }
(func(...string))0x2610
gore> hoge("foo", "bar")
foo
bar
gore> hoge([]string{"foo", "bar"}...)
foo
bar
foo
bar
gore> hoge("foo", []string{"bar"}...)
# command-line-arguments
/var/folders/nc/jkpxzpsd6459zf5qb2stk1cwnk6_vq/T/364524589/gore_session.go:31: too many arguments in call to hoge
error: exit status 2
exit status 2

仕様だとは思いますが、少し面倒な気もします。

gore(GolangのREPL)の挙動

gore> :import fmt
gore> fmt.Println("foo")
foo
(int)4
(interface {})<nil>
gore> fmt.Println("bar")
foo
bar
(int)4
(interface {})<nil>
gore> fmt.Println("baz")
foo
bar
baz
(int)4
(interface {})<nil>

Goの勉強中はgoreというREPLをヨック使ってました。 標準出力に過去のprint内容が混ざっているので、最初バグかと思ったのですが、goreではそれまでに入力されてたものをすべて連結して毎回コンパイルするので実装的に仕方ないみたいです。

スライスがあるアイテムを持つかどうかの判定

pythonで言うところの 'baz' in ['foo', 'bar', 'baz'] のようなチェックをしたい場合、Golangではそういう演算子や構文が特に用意されていないみたい。少し泥臭いですが、以下のページのように対応するよりなさそうです。

stackoverflow.com

analyzerについて

前回Pythonで実装したものもそうなのですが、N-gramは使わずにanalyzerは形態素解析エンジンのみです。 以前はMecabを使用していましたが、今回はPure Goな形態素解析エンジンであるKagomeを使用しました。 こちらも特にトラブルなく使えました。

GitHub - ikawaha/kagome: Self-contained Japanese Morphological Analyzer written in pure golang

Godep

GitHub - tools/godep: dependency tool for go

Goの依存関係を記述するツールとしてGodepを使いました。まだ godep save しただけなので特に問題は起きてないです。

envの扱い

MongoDBのDataBase URIとかは環境によって変更したいので、↓のようなファイルを用意して環境変数に応じて変えました。

github.com

デバッガ(delve)

delveをとりあえず入れて何度か動かしてみたんですが、まだ慣れなくてスムーズには扱えてないです。 結構assertionのレポートとかを頼りにデバッグしてます。 アサートが失敗したタイミングでカジュアルにdelveを起動できるといいのかも(ちゃんと調べてないだけです。ありそうな気もします)。

テスト

Goでは1テストメソッド複数アサーションが普通に行われるみたいです。 アサーションライブラリはtestifyを使用しました。GoConveyも良さそうだったのですが、個人的に書き方に慣れているので、今回はこれを使用しました。

GitHub - stretchr/testify: A sacred extension to the standard go testing package

// https://github.com/c-bata/gosearch/blob/master/crawler/crawl_test.go#L68
func TestRemoveTags(t *testing.T) {
    assert := assert.New(t)

    input := `<ul><li>item</li></ul>`
    actual := RemoveTags(input)
    expected := "item"
    assert.Equal(expected, actual)
}

Webクローラについて

Webクローラ完成時のコードは↓です。これ実行すると結構スピード出るので使う際は気をつけてください。

GitHub - c-bata/gosearch at 57aa4107e247254e9de5e95590aa6b3ecd38d67d

mgo

MongoDBのドライバでは一番人気そうだったので使用。 Sessionの取得とかは この辺 参考にしてください。

labix.org

GetCollection

c := Session.DB("database_name").C("collction_name")

Insert

type Index struct {
    ID      string        `bson:"_id"`
    Keyword string        `bson:"keyword"`
    Url     []string      `bson:"url"`
}

c := Session.DB("test").C("index")
index := &Index{"hoge", []string{"http://example.com"}}
c.Insert(index)

Update

err = c.Update(bson.M{"keyword": "keyword1"}, bson.M{"$push": bson.M{"url": "http://1.example.com"}})
err = c.UpdateId(result.ID, bson.M{"$push": bson.M{"url": "http://2.example.com"}})

UpdateAll

err = c.Update(bson.M{"keyword": "keyword1"}, bson.M{"$pushAll": bson.M{"url": []string{"http://hoge.example.com"}}})
err = c.Update(bson.M{"_id": result.ID}, bson.M{"$pushAll": bson.M{"url": []string{"http://6.example.com"}}})

Find

result := &Index{}
c.Find(bson.M{"keyword": "hoge"}).One(&result)

FindAll

var results []Index
c.Find(nil).All(&results)
results

Goの所感など

  • 並行処理周り以外ではそれなりにすぐに理解できた
    • 並行処理まわりは気づいたらブロックして出力が何もないことが多かったです。
    • こちら Go の並行処理 - Block Rockin’ Codes を読んで理解していきました。ありがとうございます。
  • デバッガはまだ慣れなくて、ついついテストとPrintデバッグに頼っちゃってます
  • 個人的に一番期待しているのはクロスコンパイルとシングルバイナリなのですが、その辺の良さが一番出るのはコマンドラインツールな気がするので、次はその辺で何か作ってみます。github.com

Go言語によるWebアプリケーション開発

Go言語によるWebアプリケーション開発