GoogleAppEngine再入門(4) -RSS Readerを作る-

前回:GoogleAppEngine再入門(3) -Datastore API 2- - souta-bot log
日経ソフトウェア200901号のPart3に習い簡易RSS Readerを作る!

ディレクトリ構成はこんな感じにする

-hoge-reader/
    |-main.py(プログラム本体)
    |-app.yaml
    |-feedparser.py
    |-templates.py(テンプレートファイル)
    |-index.yaml(データストアのインデックス情報)
    |-static/
        |-rss-big.png
        |-rss-small.png
        |-style.css
  • テンプレートファイルとCSSファイルはここからダウンロード
  • index.yamlは必須だが実行時に勝手に作られる

簡易テンプレート

top_html ="""<html>
  <head>
    <title>Read it!</title>
    <link rel="stylesheet" type="text/css" href="/static/style.css">
  </head>
  <body>
    <span class="greeting">%(greeting)s</span>
    <img src="/static/rss-big.png" width="28" height="28" alt="Readit">
    <span class="app-title">Read it!</span><br/>
    <span class="error">%(error)s</span>
    <br clear="all"/>
    <form method="POST" action="/">
    Type the feed URL and Read it!
    <input type="text" name="feed_url" size="40">
    <input type="submit" name="readit" value="Read it!">
    </form>
    %(feed_list)s
  </body>
</html>
"""

read_html ="""<html>
  <head>
    <title>Read it!</title>
    <link rel="stylesheet" type="text/css" href="/static/style.css">
  </head>
  <body>
    <span class="greeting"><a href="/?">Go to Top.</a></span>
    <img src="/static/rss-big.png" width="28" height="28" alt="Read it!">
    <span class="app-title">Read it!</span><br/>
    <span>Now reading %(feed_url)s.</span><br/>
    <br clear="all"/>
    %(contents)s
  </body>
</html>
"""

feed_list = """
<div class="item">
  <a href="%(feed_url)s">%(feed_name)s</a>&nbsp;
  <a href="/view_feed?key=%(key)s">Read it!</a>
</div>
"""

contents = """
<div class="item">
  <a href="%(link)s">%(title)s</a>
</div>
"""

CSSファイル

span.greeting {
    float: right;
}

span.app-title {
    font-size: 14pt;
    font-weight: bold;
}

.error {
    color: red;
}

div.item {
    background-image:URL("/static/rss-small.png");
    background-repeat: no-repeat;
    padding-left: 20pt;
}

scriptを変えるだけだったapp.yamlもほんのちょっと複雑に

pplication: hoge-reader
version: 1
runtime: python
api_version: 1

handlers:
- url: /static
  static_dir: static
- url: /.*
  script: main.py

プログラム本体

# -*- coding: utf-8 -*-
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.ext import db, webapp
#ユーザ認証を提供する「Users API」
from google.appengine.api import users
#取得データをメモリにキャッシュする「Memcache API」
from google.appengine.api import memcache
import logging, cgi
import templates

class Feed(db.Model):
    name = db.StringProperty()
    url = db.URLProperty()
    feed_url = db.URLProperty()
    #認証時に取得したユーザ名を格納
    owner = db.UserProperty()
    created = db.DateTimeProperty(auto_now_add=True)

def parse_feed(feed_url):
    import feedparser
    from google.appengine.api import urlfetch
    #Responseオブジェクトを取得
    result = urlfetch.fetch(feed_url)
    if result.status_code == 200:
        d = feedparser.parse(result.content)
    else:
        raise Exception("Can not retrieve given URL.")
    #RSSの形式が規格外の場合(bozo=まぬけ)
    if d.bozo == 1:
        raise Exception("Can not parse given URL.")
    return d

class Readit(webapp.RequestHandler):
    def get(self):
        #ログイン中のユーザ取得
        user = users.get_current_user()
        error = self.request.get("error")
        #ログイン中なら
        if user:
            greeting = ("Welcome, %s! (<a href=\"%s\">sign out</a>)" %
                        (user.nickname(), users.create_logout_url("/")))
            query = Feed.all().filter("owner = ", user).order("-created")
            if query.count() > 0:
                feed_list = "You have read these items before<br />"
            else:
                feed_list = "You have not read anything yet."
            #フィードを列挙
            for feed in query:
                feed_list += templates.feed_list % dict(
                    feed_url=cgi.escape(feed.url),
                    feed_name=cgi.escape(feed.name),
                    key=feed.key())
        else:
            greeting = ("<a href=\"%s\">Sign in or register</a>." %
                        users.create_login_url("/"))
            feed_list = ""
        self.response.out.write(templates.top_html %
                                dict(greeting=greeting,
                                     feed_list=feed_list,
                                     error=error))

    def post(self):
        feed_url = self.request.get("feed_url")
        user = users.get_current_user()
        try:
            d = parse_feed(feed_url)
        except Exception, e:
            logging.error(e)
            return self.redirect("/?error=Invalid%20URL")
        feed = Feed(name=d.feed.title, url=d.feed.link, feed_url=feed_url, owner=user)
        feed.put()
        #60秒間メモリにキャッシュ→データストアにアクセスする必要なし
        #しかしキャッシュできるのは1Mバイトまでだからよく考えて
        #ここではキーにentityのキー(データストア登録時に作成される)を使用した
        memcache.add(str(feed.key()), d, time=60)
        self.redirect("/view_feed?key=%s" % feed.key())

class ViewFeed(webapp.RequestHandler):
    def get(self):
        key = self.request.get("key")
        feed = Feed.get(key)
        d = memcache.get(key)
        if d is None:
            try:
                d = parse_feed(feed.feed_url)
            except Exception, e:
                logging.error(e)
                return self.redirect("/?error=Invalid%20URL")
            memcache.add(key, d, time=60)
        contents = ""
        for entry in d.entries:
            contents += templates.contents % dict(link=cgi.escape(entry.link),
                                                  title=cgi.escape(entry.title))
        self.response.out.write(
            templates.read_html % dict(feed_url=cgi.escape(feed.feed_url),
                                       contents=contents))

application = webapp.WSGIApplication([('/', Readit),
                                      ('/view_feed', ViewFeed)], debug=True)

def main():
    run_wsgi_app(application)

if __name__ == "__main__":
    main()

実行結果

開発環境用のログインページ?

ログイン完了!

FeedのURLを登録すると

TOPページには登録したFeedが

どんどん追加

デプロイしてもちゃんと使えるし(http://hoge-reader.appspot.com/)、もうLDRいらないね(嘘)

これで特集は終了。ページ数が少ないせいか少し説明足らずな部分もありましたが良記事でした。