EagleLand

PythonとMongoDBとPolymerでRSSリーダーを作った

Published at 2014-03-12

Pythonで何かアプリ書きたいなと思ってはいたので、RSSリーダーを作った。フレームワークは薄いやつが良かったのでFlaskを、データはMongoDBに突っ込んでいる。

Flask + MongoDBのところまで実装して暫く放置していたけど、最近思いつきでPolymerをねじ込んだので記事にしてみる。

やってること

Opml(XML)のパース→記事の取得 に非常に時間が掛かるので、データストアに入れておいてそこから取得しないとRSSリーダーとして非実用的。この定期的な取得処理をHeroku Schedulerで実行している。あと遊び半分でNew Relicも入れてある。ここまで色々遊べて無料。Heroku良い。

最初は格納してあるデータを全件取得して表示していたが、50件ずつ表示にしたことで使い心地はやや下がった。Polymerを使いたかっただけ。

PythonとFlask

Flaskは、軽量なWebアプリケーションフレームワーク。マイクロフレームワークの意味するところはシンプルなコアな機能と拡張性を持たせている点であり、それ自体にはデータベースレイヤへのアクセスモジュールなどはないが、様々な拡張モジュールが存在する。例えば、これについてはSQLAlchemyというORマッパーがある。日本語のハンズオンドキュメントも充実している。

Pythonは、プロパティアクセスとかリテラル(配列・オブジェクト)がJavaScriptとほぼ同じだし、JavaScriptやってる人ならあまり違和感なくやれるのではと思ったり。

// JavaScriptでのArrayとObject
var array = [0, 1, 2];
var object = {
  key: 'value'
};
// 出力
console.log(object.key);
# PythonでのArrayとObject
array = [0, 1, 2]
object = {
  key: 'value'
}
# 出力
print(object.key);

言語レベルでインデントを強制されるので、実装がばらつかない。

MongoDBへのアクセス

MongoDBへのアクセスはpymongoで実装。MongoDBを使ったことはなかったが、ここに関してもあまり悩まず実装できた。ローカルでデバッグするときはHomebrewでインストールしたMongoDBをデーモン実行しておくのと、Herokuのときは環境変数からURLを得るようにする。

# install mongodb
$ brew install mongo

# run mongo
$ mongod

# run application
$ python run.py
class EntryDAO:

    def __init__(self):
        # get url string from env
        url = os.environ.get('MONGOHQ_URL')

        if url:
            client = pymongo.MongoClient(url)
            self.db = client[urlparse(url).path[1:]]
        else:
            client = pymongo.MongoClient('localhost', 27017)
            self.db = client.db

Herokuの時はGunicornで起動。

Flask RESTFul

最初はMongoDBに入れておいたデータを全て返して表示しておくだけだったが、RESTなAPIも用意してみた。普通にHTMLテンプレートにデータ渡して描画させてるときは気にかけてなかったけど、Pythonのlistオブジェクトを素直にjsonify()出来なくて、暫く中断。

結果的にはMongoDBから取得しているために、ObjectId()があったのが要因だった。ので、素直にdelしてる。JavaScriptでいうところのPluckとかMap的なことをしたい。

flask.jsonify()はFlaskが提供するユーティリティで、組み込みのjson.dumps()と何が違うのかと思っていたら、引数周りを上手いことラップしたりmimetypeの指定をしてるらしい。

URLのマッピングは/api/get/<int:skip>/<int:limit>とし、/api/get/10/10とも出来たけど、個人的にクエリの方がなんかしっくりくるのでreqparse.RequestParser()でパースしてる。

Polymerのおまけ

繰り返しだが、これは完全に使いたかっただけ。とりあえずレベルで<polymer-ajax>を試したあとにいくつかの課題を残しつつ、記事の一覧(画面左)を<x-entry>というカスタムエレメント化。

<polymer-ajax>

リクエストURLに"/api/get?skip={{skip}}&limit={{limit}}"という形で変数を埋め、Ajaxで取得する毎にskipを書き換えている。ささやかながら2way-bindingの恩恵を受けている。以下課題とメモ。

テンプレートオブジェクトが残る

Ajaxで取得したデータを以下に渡して描画。

<polymer-element name="x-entry" attributes="skip limit filter">
  <template>
    <div class="list-group">
      <template id="entries" repeat="{{ entry in entries }}" index="i">
        <a href="{{entry.link}}" target="_blank" class="list-group-item" data-feed="{{entry.feedTitle}}">
          <h4 class="list-group-item-heading">{{entry.title}}</h4>
          <p class="list-group-item-text">
            <date>{{entry.date}}</date> - {{entry.feedTitle}}
          </p>
        </a>
      </template>
    </div>
    ...
</polymer-element>

repeat="{{ entry in entries }}"で記事の数だけループさせたはまではよかったのだが、<template>がデータとして残るので、CSSが意図しない適用になった(例えば:first-childのところとか)。

readyでフィルタシンタックスが参照出来ない?

画面右のUIで表示する記事のフィルタリングをしているが、その絞り込みがお粗末になってしまった。上で述べたループ定義をrepeat="{{ entry in entries | filterEntry }}"とし、filterEntry関数を用意すれば良いような気がしたけど、どうもreadyのタイミングだとエラーが出て参照できない模様。

プロパティの監視

attributeChangedでも取れるけど、propertyNameChangedでもいける。つまりfilterというattributeがある場合に、

attributeChanged: function attributeChangedCallback(changedAttr) {
  switch (changedAttr) {
    case 'filter':
      console.log('filter property is changed.');
      break;
  }
},

と書いていたのは、以下のように書き換え可能。ダイナミックだ。

filterChanged: function filterChangedCallback(oldValue, newValue) {
  console.log('filter property is changed.');
},

オブジェクトや配列の初期化はcreatedでやったほうがベター

Polymer()の第二引数直下で初期化するとprototypeで紐付いてしまうそう。Polymer側で色々やっていそうだけど、document.registerElement()の第二引数のような感じなのか。

Polymer('x-foo', {
  // ここで初期化するとprototypeに紐付いて予期しない動作を起こす
  //array: [],
  //object: {},
  created: function() {
    // ここでやったほうが良い
    this.array = [];
    this.object = {};
  }
});

その他感想

Polymer要素が多めな割に、あまりスコープの恩恵は受けていない作りだが、普段触らない技術を試してみるのは良いもの。

タイトルと URL をコピーしました