React + Flask + Python + MongoDBで作るRSSリーダー
これまで2回に渡ってReactについて学んできた。
testpy.hatenablog.com testpy.hatenablog.com
僕は普段、Pythonを使って機械学習やデータ解析のコードを実装してるのだが、 Webアプリ化したいな、できればReactで実現できたらいいな、と思うことが度々あった。 そこでPythonistaのために、Reactを使った簡単なWebアプリ作成記事があればと、 今(2016/10/31現在)は亡きReactチュートリアル日本語版 をベースに、MongoDBに格納したRSSをPythonで読み込み、Flask経由でクライアントに送り、 Reactで描画してみたので、コードを載せておく。 ただし説明はほとんどないので、バリバリのReact初心者の方は、上の記事でベースを固めてから読んでみて下さい。 ちなみに、Reactチュートリアルのソースはまだあります。
見た目
こんな感じにする。
ファイル構造
react-tutorial ├── gigazine_rss.py // GIGAZINEのRSSをMongoDBに格納 ├── node_modules ├── package.json ├── public │ ├── css │ │ └── base.css // RSSを見やすいように加工 │ ├── index.html │ └── scripts │ └── example.js // シンプルなDOMをReactで作成 └── server.py // Flaskを用いてクライアントとサーバーを連携
データフロー
- index.htmlにアクセスする
- index.htmlがexample.jsを呼び出す
- example.jsがserver.jsを呼び出す
- server.jsがgigazine_rss.pyを呼び出す
- gigazine_rss.pyがGIGAZINE RSSを取得してMongoDBに格納する
- server.jsがgigazine_rss.py経由でMongoDBに格納したRSSを読み込む
- server.jsがexample.jsにRSSを渡す
- example.jsがRSSのDOMを作成する
- DOMがindex.htmlに描画される
index.html | example.js | server.py | gigazine_rss.py <-> GIGAZINE RSS | MongoDB
index.html
Reactはここではなく、exmaple.jsに記述する。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>React Tutorial</title> <!-- Not present in the tutorial. Just for basic styling. --> <link rel="stylesheet" href="css/base.css" /> <script src="https://unpkg.com/react@15.3.0/dist/react.js"></script> <script src="https://unpkg.com/react-dom@15.3.0/dist/react-dom.js"></script> <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script> <script src="https://unpkg.com/jquery@3.1.0/dist/jquery.min.js"></script> <script src="https://unpkg.com/remarkable@1.7.1/dist/remarkable.min.js"></script> </head> <body> <div id="content"></div> <script type="text/babel" src="scripts/example.js"></script> </body> </html>
example.js
チュートリアルでは、CommentBox、CommentList、Commetがあったが、 ここではCommentをRssに置き換えている。 またBoxとListで十分表現可能で、公式のDocs にも、そのように書けと書いてあるので、RssBoxとRssListのみとした。
var RssBox = React.createClass({ loadRssFromServer: function() { $.ajax({ url: this.props.url, dataType: 'json', cache: false, success: function(data) { this.setState({data: data}); }.bind(this), error: function(xhr, status, err) { console.error(this.props.url, status, err.toString()); }.bind(this) }); }, getInitialState: function() { return {data: []}; }, componentDidMount: function() { this.loadRssFromServer(); setInterval(this.loadRssFromServer, this.props.pollInterval); }, render: function() { return ( <div className="rssBox"> <h1 className="siteTitle">GIGAZINE RSS</h1> <RssList data={this.state.data} /> </div> ); } }); var RssList = React.createClass({ render: function() { var rssNodes = this.props.data.map(function (rss) { return ( <div className="rss" key={rss.id}> <h3 className="title"> <a href={rss.link}> {rss.title} </a> </h3> <p className="updated">{rss.updated}</p> <p className="summary">{rss.summary}</p> </div> ); }); return ( <div className="rssList"> {rssNodes} </div> ); } }); ReactDOM.render( <RssBox url="/api/rss" pollInterval={2000000} />, document.getElementById('content') );
server.py
gigazine_rss.pyを呼び出して、GIAZINE RSSの保存と読み込みを行っている。
import json import os import time from flask import Flask, Response, request from gigazine_rss import Gigazine_RSS app = Flask(__name__, static_url_path='', static_folder='public') app.add_url_rule('/', 'root', lambda: app.send_static_file('index.html')) # client and server side with MongoDB @app.route('/api/rss', methods=['GET', 'POST']) def comments_handler(): Gigazine_RSS().save() rss = Gigazine_RSS().read() return Response( json.dumps(rss), mimetype='application/json', headers={ 'Cache-Control': 'no-cache', 'Access-Control-Allow-Origin': '*' } ) if __name__ == '__main__': app.run(port=int(os.environ.get("PORT", 3000)), debug=True)
gigazine_rss.py
この記事を1ファイルで実行できるようにした。
# -*- coding: utf-8 -*- import sys import json import nltk import numpy import feedparser import urllib2 from bs4 import BeautifulSoup import re import pymongo reload(sys) sys.setdefaultencoding('utf-8') DATABASE_NAME = 'gigazine' COLLECTION_NAME = 'rss' class Gigazine_RSS: def __init__(self): pass def save(self): rss = self.__get_rss() self.__save_rss(rss) return rss def __get_rss(self): rss_url = 'http://feed.rssad.jp/rss/gigazine/rss_2.0' articles = feedparser.parse(rss_url) rss = [] for e in articles.entries: dic = { 'id': e.id, 'updated': e.updated, 'title': e.title, 'link': e.link, 'summary': self.__getTextOnly(BeautifulSoup(e.summary)) } rss.append(dic) return rss # Extract the text from an HTML page (no tags) def __getTextOnly(self, soup): v = soup.string # Split by tags and check whether nested tags if v == None: # If tags are nested c = soup.contents # Eliminate outmost tags resulttext = '' for t in c: subtext = self.__getTextOnly(t) # If the subtext is null(u''), don't append it if len(subtext) > 0: resulttext += subtext + '\n' return resulttext else: return v.strip() # Eliminate '\n' def __save_rss(self, data): client = pymongo.MongoClient('localhost', 27017) db = client[DATABASE_NAME] co = db[COLLECTION_NAME] co.drop() co.insert(data) def read(self): client = pymongo.MongoClient('localhost', 27017) db = client[DATABASE_NAME] co = db[COLLECTION_NAME] data = [d for d in co.find()] rss = [] for c in co.find(): c.pop('_id', None) rss.append(c) return rss if __name__ == '__main__': Gigazine_RSS().save() #Gigazine_RSS().read()
base.css
見やすいようにセンタリングやコントラストなどの調整をした。
body { background: #fff; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 15px; line-height: 1.7; margin: 0; padding: 30px; } a { color: #4183c4; text-decoration: none; } a:hover { text-decoration: underline; } code { background-color: #f8f8f8; border: 1px solid #ddd; border-radius: 3px; font-family: "Bitstream Vera Sans Mono", Consolas, Courier, monospace; font-size: 12px; margin: 0 2px; padding: 0 5px; } h1, h2, h3, h4 { font-weight: bold; margin: 0 0 15px; padding: 0; } h1 { font-size: 2.5em; } h2 { border-bottom: 1px solid #eee; font-size: 2em; } h3 { font-size: 1.5em; } h4 { font-size: 1.2em; } p, ul { margin: 15px 0; } ul { padding-left: 30px; } .rssBox { width: 600px; margin: auto; } .updated { color: #999; } .siteTitle { text-align: center; margin: 20px 0 40px; } .summary { margin: 0 0 40px; }