TensorFlowによる精度計算の流れを追う

accuracy = tf.reduce_mean(tf.cast(tf.equal(tf.argmax(Y, 1), tf.argmax(Y_, 1)), tf.float32))

Yをニューラルネットワークの出力層、Y_をその正解ラベルとした場合、学習モデルの精度を計算するとき、 TensorFlowでは上記ようにAPIを組合せて書くことがある。 式が長いので、各APIの処理の流れを、1つずつ出力しながら追ってみる。 分かれば非常にシンプルな処理の流れである。

計算の流れ

NumpyとTensorFlowをimportしておく。

>>> import tensorflow as tf
>>> import numpy as np

YとY_に[3, 4]のサンプルデータをセットする。 1行目を不正解としている。

>>> Y = np.array([
...                 [0.1, 0.2, 0.3, 0.4],
...                 [0.0, 0.8, 0.2, 0.0],
...                 [0.0, 0.4, 0.5, 0.1]
...             ])
>>> print Y
[[ 0.1  0.2  0.3  0.4]
 [ 0.   0.8  0.2  0. ]
 [ 0.   0.4  0.5  0.1]]

>>> Y_ = np.array([
...                 [0.0, 0.0, 1.0, 0.0],
...                 [0.0, 1.0, 0.0, 0.0],
...                 [0.0, 0.0, 1.0, 0.0]
...             ])
>>> print Y_
[[ 0.  0.  1.  0.]
 [ 0.  1.  0.  0.]
 [ 0.  0.  1.  0.]]

TensorFlowのSessionを開始。 Sessionと言えば、昨日、映画のSessionを見ましたが非常に良い映画でした。

>>> sess = tf.Session()

ここから精度計算におけるTensorFlowのAPIの説明。 まずはtf.argmax()から。 第2パラメーターに1をセットすると、行ごとに最大となる列を返す。 Yの場合、1行目が4列目の0.4、2行目が2列目の0.8、3行目が3列目の0.5が最大となる。 1行目は0からカウントされるので、以下のようになる。 Y_についても同様。 ちなみに、第2パラメーターに0をセットすると、列ごとに最大となる行を返す。

>>> print sess.run(tf.argmax(Y, 1))
[3 1 2]
>>> print sess.run(tf.argmax(Y_, 1))
[2 1 2]

続いてtf.equal()。 渡された2つのベクトルが一致しているか否かを見る。 今回は[3 1 2]と[2 1 2]を比較しているので次のようになる。

>>> eq = tf.equal(tf.argmax(Y, 1), tf.argmax(Y_, 1))
>>> print sess.run(eq)
[False  True  True]

tf.cast()では第1パラメーターを第2パラメーターのデータ・タイプに変換する。 [False True True]もfloat32に変換すると次の通り。

>>> print sess.run(tf.cast(eq, tf.float32))
[ 0.  1.  1.]

最後はtf.reduce_mean()。 np.mean()と同じで平均を計算する。 [ 0. 1. 1.]の平均なので2/3となる。

>>> print sess.run(tf.reduce_mean(tf.cast(eq, tf.float32)))
0.666667

最後に全部繋げれば、一気に計算できる。

>>> accuracy = tf.reduce_mean(tf.cast(tf.equal(tf.argmax(Y, 1), tf.argmax(Y_, 1)), tf.float32))
>>> print sess.run(accuracy)
0.666667

jsmでYahooファイナンスのデータを片っ端から取得してMongoDBに保存する

はじめに

jsm(Japanese Stock Market)という Yahooファイナンスをクロールして株関連データを取得できるライブラリーがある。 Brand、Finance、Priceデータが取得できるので、東証一部に絞ってデータを片っ端から取得するコードを書いた。

インストール

pipを更新してからjsmをインストールする。 スクレイピングはBeautifulSoup4で行っているので(再)インストール

$ sudo pip install --upgrade pip
$ sudo pip install jsm
$ sudo pip install beautifulsoup4 -U

MongoDBのラッパー

save, read, deleteを用意したMongoDBのラッパーファイル(db.py)を作成する。 DB名とCollection名を初期化で指定する。 saveはdictのlistをdataとして渡す。

# -*- coding: utf-8 -*-
import sys
import pymongo

reload(sys)
sys.setdefaultencoding('utf-8')


class DB:
    def __init__(self, db_name, coll_name):
        self.db_name = db_name
        self.coll_name = coll_name


    def save(self, data):
        client = pymongo.MongoClient('localhost', 27017)
        db = client[self.db_name]
        coll = db[self.coll_name]

        coll.insert(data)


    def read(self):
        client = pymongo.MongoClient('localhost', 27017)
        db = client[self.db_name]
        coll = db[self.coll_name]

        data = [d for d in coll.find()]

        return data


    def delete(self):
        client = pymongo.MongoClient('localhost', 27017)
        db = client[self.db_name]
        coll = db[self.coll_name]

        coll.drop()

前準備

クローラーファイル(clawer.py)を作成し、必要なライブラリーと初期設定を行う。 上記のdb.pyもインポートしておく。 stockデータベースに、Brand、Finance、Priceコレクションを作成して取得したデータを保存する。

# -*- coding: utf-8 -*-
import sys
import jsm
from progressbar import ProgressBar
import pandas as pd
import datetime

from db import DB

reload(sys)
sys.setdefaultencoding('utf-8')

DB_STOCK = 'stock'  # Stock DB
COLL_BRAND = 'brand'  # Brand Collection
COLL_FINANCE = 'finance'  # Finace Collection
COLL_PRICE = 'price'  # Price Collection
START_DATE = datetime.date(2014, 1, 1)  # 株価の取得開始日

Brandデータを取得

get_brand()で全銘柄が取得できるが、取得状況の進捗を見たいのでカテゴリーごとに取得する。

def get_brands(self):
    db = DB(DB_STOCK, COLL_BRAND)
    #db.delete()

    categories = [
        '0050',  # 農林・水産業
        '1050',  # 鉱業
        '2050',  # 建設業
        '3050',  # 食料品
        '3100',  # 繊維製品
        '3150',  # パルプ・紙
        '3200',  # 化学
        '3250',  # 医薬品
        '3300',  # 石油・石炭製品
        '3350',  # ゴム製品
        '3400',  # ガラス・土石製品
        '3450',  # 鉄鋼
        '3500',  # 非鉄金属
        '3550',  # 金属製品
        '3600',  # 機械
        '3650',  # 電気機器
        '3700',  # 輸送機器
        '3750',  # 精密機器
        '3800',  # その他製品
        '4050',  # 電気・ガス業
        '5050',  # 陸運業
        '5100',  # 海運業
        '5150',  # 空運業
        '5200',  # 倉庫・運輸関連業
        '5250',  # 情報・通信
        '6050',  # 卸売業
        '6100',  # 小売業
        '7050',  # 銀行業
        '7100',  # 証券業
        '7150',  # 保険業
        '7200',  # その他金融業
        '8050',  # 不動産業
        '9050'   # サービス業
    ]

    q = jsm.Quotes()
    pb = ProgressBar(maxval=len(categories)).start()
    for i in range(len(categories)):
        lis = []
        try:
            brands = q.get_brand(categories[i])
        except:
            pass
        for b in brands:
            dic = {
                    'category': categories[i],
                    'ccode': b.ccode,
                    'market': b.market,
                    'name': b.name,
                    'info': b.info
            }
            lis.append(dic)
        db.save(lis)
        pb.update(i+1)

Finaceデータを取得

Brandデータから東証一部の証券コード(ccode)のみを取得する。

def get_target_ccodes(self):
    data = DB(DB_STOCK, COLL_BRAND).read()
    df_brand = pd.DataFrame(data)
    df_brand = df_brand[df_brand['market']=='東証1部']
    ccodes = df_brand['ccode'].tolist()

    return ccodes

取得した東証一部の証券コードを引数にしてFianceデータを取得する。

def get_finances(self, ccodes):
    db = DB(DB_STOCK, COLL_FINANCE)
    #db.delete()

    q = jsm.Quotes()
    pb = ProgressBar(maxval=len(ccodes)).start()
    lis = []
    for i in range(len(ccodes)):
        try:
            f = q.get_finance(ccodes[i])
        except:
            pass
        dic = {
                'ccode': ccodes[i],
                'market_cap': f.market_cap,
                'shares_issued': f.shares_issued,
                'dividend_yield': f.dividend_yield,
                'dividend_one': f.dividend_one,
                'per': f.per,
                'pbr': f.pbr,
                'eps': f.eps,
                'bps': f.bps,
                'price_min': f.price_min,
                'round_lot': f.round_lot,
                'years_high': f.years_high,
                'years_low': f.years_low
        }
        lis.append(dic)
        pb.update(i+1)

    db.save(lis)

Priceデータを取得

Financeデータと同様、東証一部の証券コードを引数にしてPriceデータを取得する。

def get_prices(self, ccodes):
    start_date = START_DATE
    end_date = datetime.date.today()

    db = DB(DB_STOCK, COLL_PRICE)
    #db.delete()

    q = jsm.Quotes()
    pb = ProgressBar(maxval=len(ccodes)).start()
    for i in range(len(ccodes)):
        lis = []
        try:
            prices = q.get_historical_prices(ccodes[i], jsm.DAILY, start_date, end_date)
        except:
            pass
        for p in prices:
            dic = {
                    'ccode': ccodes[i],
                    'date': p.date,
                    'open': p.open,
                    'high': p.high,
                    'low': p.low,
                    'close': p.close,
                    'volume': p.volume
            }
            lis.append(dic)
        db.save(lis)
        pb.update(i+1)

crawler.pyの全ソース

db.pyと同じ階層にファイルを置いて$ python crawler.pyを実行すればクロールを開始する。

# -*- coding: utf-8 -*-
import sys
import jsm
from progressbar import ProgressBar
import pandas as pd
import datetime

from db import DB

reload(sys)
sys.setdefaultencoding('utf-8')

DB_STOCK = 'stock'
COLL_BRAND = 'brand'
COLL_PRICE = 'price'
COLL_FINANCE = 'finance'
START_DATE = datetime.date(2014, 1, 1)


class Crawler:
    """
    Refer to https://pypi.python.org/pypi/jsm/0.19
    """

    def __init__(self):
        pass


    def main(self):
        print 'getting brands...'
        self.get_brands()

        print 'getting finances...'
        ccodes = self.get_target_ccodes()
        self.get_finances(ccodes)

        print 'getting prices...'
        self.get_prices(ccodes)


    def get_brands(self):
        db = DB(DB_STOCK, COLL_BRAND)
        #db.delete()

        categories = [
            '0050',  # 農林・水産業
            '1050',  # 鉱業
            '2050',  # 建設業
            '3050',  # 食料品
            '3100',  # 繊維製品
            '3150',  # パルプ・紙
            '3200',  # 化学
            '3250',  # 医薬品
            '3300',  # 石油・石炭製品
            '3350',  # ゴム製品
            '3400',  # ガラス・土石製品
            '3450',  # 鉄鋼
            '3500',  # 非鉄金属
            '3550',  # 金属製品
            '3600',  # 機械
            '3650',  # 電気機器
            '3700',  # 輸送機器
            '3750',  # 精密機器
            '3800',  # その他製品
            '4050',  # 電気・ガス業
            '5050',  # 陸運業
            '5100',  # 海運業
            '5150',  # 空運業
            '5200',  # 倉庫・運輸関連業
            '5250',  # 情報・通信
            '6050',  # 卸売業
            '6100',  # 小売業
            '7050',  # 銀行業
            '7100',  # 証券業
            '7150',  # 保険業
            '7200',  # その他金融業
            '8050',  # 不動産業
            '9050'   # サービス業
        ]

        q = jsm.Quotes()
        pb = ProgressBar(maxval=len(categories)).start()
        for i in range(len(categories)):
            lis = []
            try:
                brands = q.get_brand(categories[i])
            except:
                pass
            for b in brands:
                dic = {
                        'category': categories[i],
                        'ccode': b.ccode, 
                        'market': b.market, 
                        'name': b.name, 
                        'info': b.info
                }
                lis.append(dic)
            db.save(lis)
            pb.update(i+1)


    def get_finances(self, ccodes):
        db = DB(DB_STOCK, COLL_FINANCE)
        #db.delete()

        q = jsm.Quotes()
        pb = ProgressBar(maxval=len(ccodes)).start()
        lis = []
        for i in range(len(ccodes)):
            try:
                f = q.get_finance(ccodes[i])
            except:
                pass
            dic = {
                    'ccode': ccodes[i],
                    'market_cap': f.market_cap,
                    'shares_issued': f.shares_issued,
                    'dividend_yield': f.dividend_yield,
                    'dividend_one': f.dividend_one,
                    'per': f.per,
                    'pbr': f.pbr,
                    'eps': f.eps,
                    'bps': f.bps,
                    'price_min': f.price_min,
                    'round_lot': f.round_lot,
                    'years_high': f.years_high,
                    'years_low': f.years_low
            }
            lis.append(dic)
            pb.update(i+1)
        
        db.save(lis)
        

    def get_prices(self, ccodes):
        start_date = START_DATE
        end_date = datetime.date.today()

        db = DB(DB_STOCK, COLL_PRICE)
        #db.delete()

        q = jsm.Quotes()
        pb = ProgressBar(maxval=len(ccodes)).start()
        for i in range(len(ccodes)):
            lis = []
            try:
                prices = q.get_historical_prices(ccodes[i], jsm.DAILY, start_date, end_date)
            except:
                pass
            for p in prices:
                dic = {
                        'ccode': ccodes[i],
                        'date': p.date, 
                        'open': p.open, 
                        'high': p.high, 
                        'low': p.low,
                        'close': p.close,
                        'volume': p.volume
                }
                lis.append(dic)
            db.save(lis)
            pb.update(i+1)


    def get_target_ccodes(self):
        data = DB(DB_STOCK, COLL_BRAND).read()
        df_brand = pd.DataFrame(data)
        df_brand = df_brand[df_brand['market']=='東証1部']
        ccodes = df_brand['ccode'].tolist()

        return ccodes


if __name__ == '__main__':
    Crawler().main()

所感

Priceデータを取得するのに15時間ぐらいかかったので、マルチスレッドにした方がいい。 上記のコードで継続的に最新のデータを収集し続けるには、もう少し改良が必要だけど、 とりあえず、サクッとデータを収集して分析したい人は使えると思う。 jsm自体は、 更新が2015年で止まっていて、GitHubからは削除されているが、ソース自体はPyPIに上がってるので、 これまで自作してた人は動かなくなっても改修できると思う。

ReactのListとKey

ReactでList(ObjectのArray)を描画する際、mapを利用して各Objectの項目をセットする。

var data = [
        {id: 1, author: "Pete Hunt", text: "This is one comment"},
        {id: 2, author: "Jordan Walke", text: "This is *another* comment"}
];

var CommentList = React.createClass({
  render: function() {
    var commentNodes = this.props.data.map(function (comment) {
      return (
        <div author={comment.author}>
          {comment.text}
        </div>
      );
    });
    return (
      <div className="commentList">
        {commentNodes}
      </div>
    );
  }
});

 

もし描画する際に、各ObjectにユニークなKeyを持たせていない場合は警告が出る。

react.js:20541 Warning: Each child in an array or iterator should have a unique "key" prop.

 

以下のように、keyに何かしらのユニークな値を持たせると、以下の警告は消える。

<div author={comment.author} key={comment.id}>

参考文献

React + Flask + Python + MongoDBで作るRSSリーダー

これまで2回に渡ってReactについて学んできた。

testpy.hatenablog.com testpy.hatenablog.com

僕は普段、Pythonを使って機械学習やデータ解析のコードを実装してるのだが、 Webアプリ化したいな、できればReactで実現できたらいいな、と思うことが度々あった。 そこでPythonistaのために、Reactを使った簡単なWebアプリ作成記事があればと、 今(2016/10/31現在)は亡きReactチュートリアル日本語版 をベースに、MongoDBに格納したRSSPythonで読み込み、Flask経由でクライアントに送り、 Reactで描画してみたので、コードを載せておく。 ただし説明はほとんどないので、バリバリのReact初心者の方は、上の記事でベースを固めてから読んでみて下さい。 ちなみに、Reactチュートリアルのソースはまだあります

見た目

こんな感じにする。

f:id:Shoto:20161030013827p:plain

ファイル構造

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を用いてクライアントとサーバーを連携

データフロー

  1. index.htmlにアクセスする
  2. index.htmlがexample.jsを呼び出す
  3. example.jsがserver.jsを呼び出す
  4. server.jsがgigazine_rss.pyを呼び出す
  5. gigazine_rss.pyがGIGAZINE RSSを取得してMongoDBに格納する
  6. server.jsがgigazine_rss.py経由でMongoDBに格納したRSSを読み込む
  7. server.jsがexample.jsにRSSを渡す
  8. example.jsがRSSのDOMを作成する
  9. 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

testpy.hatenablog.com

この記事を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;
}

Reactチュートリアル入門: Pythonサーバーサイド連携編

チュートリアル | React がなくなったようで。。良いチュートリアルだったのに。。
まあとりあえず、前回は クライアントサイドのみでWebサイトを表示したので、 今回はサーバーサイドで取得したデータをクライアントサイドに渡して表示させる。 とは言っても、JSONファイルを表示するだけなので、非常に簡単。 ただし、サーバーサイドはNode.jsではなく、Pythonを使う。 その前にまず、前回のReactコードを少しカスタマイズする。

クライアントサイドのカスタマイズ

CommentBox

前回、シンプルだったCommentBoxに色々追記する。

  loadCommentsFromServer: 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)
    });
  },

Ajax通信を行う。urlは、後ほどReactDOM.renderでCommentBoxに渡される。 これがサーバーサイドへのパスになる。dataはサーバーサイドから返ってきたデータ。  

  getInitialState: function() {
    return {data: []};
  },

クライアントサイドのdataの初期化。dataはObjectのArrayなので[]となる。  

  componentDidMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },

DOMに関わる初期化。 loadCommentsFromServerで設定したAjaxリクエストや、リフレッシュ頻度を指定するsetIntervalの登録など、 server-side rendering時には必要ない初期化処理についてはこの中で行う。

以上より、今回のCommentBoxは次のようになる。  

var CommentBox = React.createClass({
  loadCommentsFromServer: 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.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList comments={this.state.data} />
      </div>
    );
  }
});

ReactDOM.render

設定したCommentBoxに、ReactDOM.renderでパラメーターを渡す。 サーバーサイドのパスとなるurlと、リフレッシュ頻度となるpollIntervalを指定する。 前回はurlキーではなく、dataキーにクライアントサイドで定義したdataを渡していた。 pollIntervalは公式では2000(2秒)だが、短すぎるので2000秒としておく。

ReactDOM.render(
  <CommentBox url="/api/comments" pollInterval={2000000} />,
  document.getElementById('content')
);

Pythonサーバーサイド

Pythonのサーバーサイドは、Flaskを使っている。 Flaskは僕も知らないので、ググって下さい。 でもFlaskはまったく悩むことなく簡単に使えるようで、 クライアントサイドで指定したurl='/api/comments' [check1!] のすぐ下のcomments_handler() の中でデータを取得して返している。 今回はGETのみなので、単純にserver.pyと同じディレクトリーに置かれたcommnet.jsonを読み込んで [check2!] 、 json形式にPythonで適切に変形して、クライアントサイドに返している [check3!] ことが分かる。

import json
import os
import time
from flask import Flask, Response, request

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 json file

@app.route('/api/comments', methods=['GET', 'POST'])  # Check1!
def comments_handler():
    with open('comments.json', 'r') as f:
        comments = json.loads(f.read())  # Check2!

    if request.method == 'POST':
        new_comment = request.form.to_dict()
        new_comment['id'] = int(time.time() * 1000)
        comments.append(new_comment)

        with open('comments.json', 'w') as f:
            f.write(json.dumps(comments, indent=4, separators=(',', ': ')))

    return Response(
        json.dumps(comments),  # Check3!
        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)

実行

$ python server.py

上記のコマンドを実行すると、以下のように表示されるはず。

f:id:Shoto:20161030002637p:plain

Reactチュートリアル入門:クライアントサイド編

チュートリアル | React(何故か2016-10-23時点ではNot Found) を理解しながら写経すると、Reactで簡単なWebアプリが作れるようになる。 しかし、僕自身もそうだが、テンプレートを使ったWebアプリしか作ったことしかなく、 Reactを理解したいという人向けに、今回はクライアントサイドだけをいじってWebアプリを作成する。 そもそもReactとは何か、Reactを使うと何が便利なのかを知りたい人は Reactを使うとなぜjQueryが要らなくなるのか が分りやすい。

Reactチュートリアルの始め方

Reactチュートリアルのプロジェクトをクローンして、パッケージをインストールする。

$ git clone https://github.com/reactjs/react-tutorial.git
$ cd react-tutorial
$ npm install

初期設定

プロジェクトの中身を見ると、色々ファイルが入っているので、必要なものだけに絞って残りは削除する。

react-tutorial
├── LICENSE
├── README.md
├── app.json
├── comments.json
├── db.py
├── db.pyc
├── node_modules
│   ├── body-parser
│   └── express
├── npm-debug.log
├── package.json
├── public
│   ├── css
│   ├── index.html
│   └── scripts
├── requirements.txt
├── server.go
├── server.js
├── server.php
├── server.pl
├── server.py
└── server.rb

 
以下が削除した結果。

react-tutorial
├── comments.json     // 今回は使わない
├── node_modules      // npm installで生成されたフォルダー
│   ├── body-parser
│   └── express
├── package.json      // メタ情報ファイル
├── public
│   ├── css
│   ├── index.html    // 今回編集する唯一のファイル
│   └── scripts
├── requirements.txt  // 後で使う
├── server.js         // 後で実行する
└── server.py         // 後で実行する
  • comments.json
    • コメントが書かれたJSONファイル。今回は使わない。
  • node_modules
    • "npm install"を実行した時に生成されたフォルダー。Reactなど今回使うnode.js関連のパッケージが入っている。基本触らない。
  • package.json
    • ライセンスや著者、バージョンなど記述されたメタ情報ファイル。"npm install"の後でパッケージの依存関係の記述も自動的に追加されている。
  • public
    • HTML・CSSJavaScript・画像など、クライアントサイドのファイルを置く場所。
  • index.html
    • 今回は編集する唯一のファイル。
  • server.jsとserver.py
    • アプリケーション・サーバーを起動するNode.jsとPythonのコード。他の言語を使いたい人は、それらのファイルを残す。
  • requirements.txt
    • Pythonのサーバーサイドを実行する際に必要なライブラリーをインストールするための設定ファイル。

index.htmlを開くと以下のようになるが、今回は下の方のscriptダグで囲まれたエリアにReactコードを追記していく。 そのため、"script/example.js"をインポートしているscriptタグをコメントアウトしておく。

<!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>
    -->
    <script type="text/babel">
    // To get started with this tutorial running your own code, simply remove
    // the script tag loading scripts/example.js and start writing code here.
    </script>
  </body>
</html>

Reactの実装

最終的なWebアプリの見た目はこんな感じ。

f:id:Shoto:20161023190302p:plain

 
まずは、Webアプリで表示するためのJSONを記述する。

      var data = [
        {id: 1, author: "Pete Hunt", text: "This is one comment"},
        {id: 2, author: "Jordan Walke", text: "This is *another* comment"}
      ];

 
ここからReact。 上記ので記述したdataを、CommentBoxに入れた結果を、index.htmlの<div id="content"></div>内にレンダリングする、 という意味のコードを記述する。 レンダリングするDOMの構造を、このCommentBoxから始まるReactによって作成することが、今回の主な内容であり、Reactのコアな部分になる。

      ReactDOM.render(
        <CommentBox data={data} />,
        document.getElementById('content')
      );

 
上記のdataキーは、CommentBoxでは{this.props.data}として表される。 逆に言えば、親のデータは{this.props.xxx}で取得できる。 また{this.props.data}は、子であるCommentListcommentsキーにそのまま渡される(オリジナルはdataキー)。 ここより下は、コメントのリストが表示されるので、h1タグでCommentsと記述しておく。

      var CommentBox = React.createClass({
        render: function() {
          return (
            <div className="commentBox">
              <h1>Comments</h1>
              <CommentList comments={this.props.data} />
            </div>
          );
        }
      });

 
上記のcommentsキーは、CommentListでは{this.props.comments}となる。 これはJSONのobjectのarrayなので、個々のobjectを子であるCommentに渡すために、 map関数を使っている。またCommentへはobjectの値をセットしている。

      var CommentList = React.createClass({
        render: function() {
          var commentNodes = this.props.comments.map(function (comment) {
            return (
              <Comment author={comment.author} key={comment.id}>
                {comment.text}
              </Comment>
            );
          });
          return (
            <div className="commentList">
              {commentNodes}
            </div>
          );
        }
      });

 
上記のauthorキーは{this.props.author}になる。 また上記の{comment.text}{this.props.children}になる。

      var Comment = React.createClass({
        render: function() {
          return (
            <div className="comment">
              <h2>
                {this.props.author}
              </h2>
              {this.props.children}
            </div>
          );
        }
      });

 
このようにReactによって、CommetBox以下でDOMの構造が構築された後、最終的にレンダリングされる。 index.htmlに追記したコードをまとめると以下のようになる。

<!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>
    -->
    <script type="text/babel">
      // To get started with this tutorial running your own code, simply remove
      // the script tag loading scripts/example.js and start writing code here.
      var data = [
        {id: 1, author: "Pete Hunt", text: "This is one comment"},
        {id: 2, author: "Jordan Walke", text: "This is *another* comment"}
      ];

      var CommentBox = React.createClass({
        render: function() {
          return (
            <div className="commentBox">
              <h1>Comments</h1>
              <CommentList comments={this.props.data} />
            </div>
          );
        }
      });

      var CommentList = React.createClass({
        render: function() {
          var commentNodes = this.comments.data.map(function (comment) {
            return (
              <Comment author={comment.author} key={comment.id}>
                {comment.text}
              </Comment>
            );
          });
          return (
            <div className="commentList">
              {commentNodes}
            </div>
          );
        }
      });

      var Comment = React.createClass({
        render: function() {
          return (
            <div className="comment">
              <h2>
                {this.props.author}
              </h2>
              {this.props.children}
            </div>
          );
        }
      });

      ReactDOM.render(
        <CommentBox data={data} />,
        document.getElementById('content')
      );
    </script>
  </body>
</html>

アプリケーション・サーバーの実行

以下のいずれかを実行し、http://localhost:3000/にアクセスすれば、 2つのコメントがレンダリングされた画面が表示されるはず。

  • Node.js
node server.js
pip install -r requirements.txt
python server.py

pipの実行は初回だけでよい。

参考文献

Atomがフリーズしたときの対処法

macOS Sierraにアップデートしたら、"Editor is not responding"となったので、その対処法をメモに残しておく。

手順

  1. Finderのメニューバーから、[移動] > [フォルダーへ移動...]と選択
  2. テキストボックスに<~/.atom/storage>を入力して[移動]ボタンをクリック
  3. フォルダー内のファイルを削除

なお、Atom関連の設定が色々と飛ぶようなので、自己責任で。 僕の場合、ファイルは"application.json"しかなかった。 削除したら、"Editor is not responding"というメッセージが表示されることもなくなったし、 インストールしたパッケージやテーマも飛ばなかった。

参考文献