Google App Engine でRSSリーダーを作った
昔のホームページには大抵リンク集があったが、
今はあまり見なくなってしまった。
無断リンク禁止とか、相互リンクお断りとか面白かったのに。
まぁ、やろうとすれば
【2ch】ニュー速クオリティ http://news4vip.livedoor.biz/
とか簡単にできるけど、
みんなで追加できるリンク集のようなものがあればいいのにと思い、
Google App Engine の チュートリアルビデオ
を少しいじれば作れそうだったので、作ってみることにした。
・・・・最終的にはリンク集じゃなくて、RSSリーダーができた。
feedparser.py というモジュールに感動したからだと思う。
feedparser.py
ダウンロード
http://code.google.com/p/feedparser/downloads/list
参考サイト
http://d.hatena.ne.jp/souta-bot/20090204/1233764053
http://itpro.nikkeibp.co.jp/search/index.html?q=feedparser.py(1週間前にあったリンクが切れた・・)
目標管理的にどうかと思うし、
できのいいモジュールを使っているとバカになってしまいそうな気がするが・・
これも Joel Spolsky がいうところの Fire and Motion の一種なのかもしれない。
Fire and Motion
http://www.joelonsoftware.com/articles/fog0000000339.html
できたRSSリーダー
PublicRSS
http://ippaipub.appspot.com/publicrss
以下詳細
開発前に目標とした点
1.2つの画面を作る
2.複数のレコードの1つを指定して消せるようにする
チュートリアルビデオと同じ事をするのはさすがに気が引けたので。
関係ないけど、このビデオはいい。
なんか見ていて楽しくなってくる。
このビデオの人がコンピューターサイエンスの博士号を持っている宇宙飛行士であれ、
そこら辺を歩いていた暇人であれ、とても尊敬できる。
で、目標とした点=苦労した点だった。
でも、やる前からそこでひっかかることは予想できたので問題ではない・・はず。
かかった時間もほぼ予想通りだった。全部で2,30時間ぐらい。
目標を満たすまで10時間ぐらいかかって、あとは何回か作り直した。
1.2つの画面を作る
Web アプリでは画面と画面の間は別の世界で、簡単には値を受け渡せない。そこで
(1)別ページに移動する前に、cookie に値を格納する。
publicrss.py
cookie = 'publicrss_siteurl=%s;' % self.request.get('get_item') self.response.headers.add_header('Set-Cookie', cookie )
(2)別ページで、cookie から値を取得する。
publicrss.py
feedsiteurl= self.request.cookies.get('publicrss_siteurl', '')
2.複数のレコードの1つを指定して消せるようにする
レコードを識別する必要がある。そこで、
(1)checkbox にレコードID をセットする(name=レコードID)
publicrss.html
<input type="checkbox" name={{feed.siteurl}}>
(2)チェックしたレコードID のレコードを削除する。
publicrss.py
if self.request.get(str(feedsite.siteurl)) !='': feedsite.delete( )
作っている間は、熱狂、狂乱的な何かがあった。
ゲームにはまっているときと同じで、早く家に帰ってすぐいじりたい感じ。
・・ほんとは簿記2級の勉強をしなくてはいけないのだけど・・
そういう時ほど集中できる。
人間は適当なダミー目標を持っているといいのかもしれない。
MBD (Management By Dummy objectives)
ただ、波に乗っているかと聞かれれば、波に巻かれている気がする。
上のJoel の記事から。
It doesn't matter if your code is lame and buggy and nobody wants it.
If you are moving forward, writing code and fixing bugs constantly, time is on your side.
・・だってさ。
ソースコード全部は以下の通り
app.yaml
application: ippaipub version: 1 runtime: python api_version: 1 handlers: - url: /favicon\.ico static_files: favicon.ico upload: favicon\.ico - url: /css static_dir: css - url: /publicrss script: publicrss.py - url: /publicrss/item script: publicrss.py - url: .* script: main.py
publicrss.html
<!DOCTYPE HTML> <html lang="ja"> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="../css/publicrss.css" type="text/css"> <title>PublicRSS</title> </head> <body> <p> PublicRSS </p> <form action="#" method="post"> <div><input type="text" name="t_sitename" placeholder="Slashdot" required> <input type="text" name="t_siteurl" size="50" placeholder="http://slashdot.jp/slashdotjp.rss" required></div> <div><input type="submit" name="add_site" value="Add RSS Site" id="addbutton"></div> </form> <p> </p> <form action="#" method="post"> <input type="submit" name="del_site" value="Delete checked RSS site" id="delbutton"> {% for feed in tp_feed %} <div> <input type="checkbox" name={{feed.siteurl}}> <a href="{{feed.siteurl}}" target="_blank">{{feed.sitename}}</a> <input type="submit" name="get_item" value={{feed.siteurl}} class="urlbutton"> </div> {% endfor %} </form> </body> </html>
publicrssitem.html
<!DOCTYPE HTML> <html lang="ja"> <head> <meta charset="UTF-8"> <title>PublicRSS Item</title> </head> <body> <p> <A href="../publicrss">PublicRSS</a> </p> <p> {{tp_feed.publisher}} <br> {{tp_feed.title}} </p> {% for feeditem in tp_feeditem %} <div> <a href="{{feeditem.link}}" target="_blank">{{feeditem.title}}</a> </div> {% endfor %} </body> </html>
publicrss.css
#addbutton { -webkit-border-radius: 3px; -moz-border-radius: 3px; background: #ccccff; color: #000000; } #delbutton { -webkit-border-radius: 3px; -moz-border-radius: 3px; background: #ffcccc; color: #000000; } .urlbutton { width: 100px; color: #666666; }
publicrss.py
#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright 2007 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # import os import datetime import logging from google.appengine.dist import use_library use_library('django', '1.2') from google.appengine.ext.webapp import template from google.appengine.ext import db from google.appengine.ext import webapp from google.appengine.ext.webapp import util class Feedsite(db.Model): sitename = db.StringProperty(required=True) siteurl = db.StringProperty(required=True) sitetime = db.DateTimeProperty(auto_now_add=True) def parse_feed(feed_url): import feedparser from google.appengine.api import urlfetch """ call feedpaser module """ result = urlfetch.fetch(feed_url) if result.status_code == 200: d = feedparser.parse(result.content) else: raise Exception('Can not retrieve giben URL.') if d.bozo == 1: raise Exception('Can not parse giben URL.') return d class HeadHandler(webapp.RequestHandler): def get(self): # display feedsite information feedsite = db.GqlQuery( 'SELECT * FROM Feedsite ' 'ORDER BY sitetime') template_values = { 'tp_feed': feedsite } path = os.path.join(os.path.dirname(__file__), 'template/publicrss.html') self.response.out.write(template.render(path, template_values)) def post(self): # add feedsite information if self.request.get('add_site') !='': if (self.request.get('t_sitename') !='' and self.request.get('t_siteurl') !='' ): feedsite = Feedsite( sitename = self.request.get('t_sitename'), siteurl = self.request.get('t_siteurl')) feedsite.put( ) self.redirect('/publicrss') # delete feedsite information if self.request.get('del_site') !='': for feedsite in Feedsite.all( ): if self.request.get(str(feedsite.siteurl)) !='': feedsite.delete( ) self.redirect('/publicrss') # set feedsite URL to Cookie if self.request.get('get_item') !='': cookie = 'publicrss_siteurl=%s;' % self.request.get('get_item') self.response.headers.add_header('Set-Cookie', cookie ) self.redirect('/publicrss/item') class ItemHandler(webapp.RequestHandler): def get(self): # get feedsite URL from Cookie feedsiteurl= self.request.cookies.get('publicrss_siteurl', '') # get feed try: d = parse_feed(feedsiteurl) except Exception, e: logging.info(Exception) return self.redirect('/publicrss?error=Invalid%20URL') # display feed template_values = { 'tp_feed':d.feed , 'tp_feeditem':d.entries } path = os.path.join(os.path.dirname(__file__), 'template/publicrssitem.html') self.response.out.write(template.render(path, template_values)) def main(): application = webapp.WSGIApplication( [('/publicrss', HeadHandler), ('/publicrss/item', ItemHandler)], debug=True) util.run_wsgi_app(application) if __name__ == '__main__': main()