#概要
前回までに対話型LINE BOTの原型を作成し、テキストメッセージおよび画像メッセージの送信ができることを確認した。
今回は実用編として株価情報の取得ならびにチャート描画を行いユーザに提供する機能を実装する。
##システム構成
LINEアプリからユーザが起動コードとティッカーシンボルを含む所定の書式のメッセージを投下(①)すると、LINE BOTが株価情報サービスサイトのAPIにアクセスしスクレイピングを行う(②)。取得したデータを元に株価チャートを作図し画像データとして保存する(③)とともに、LINE BOTは画像メッセージにそのURLを記述してLINEアプリに返信する。LINEアプリが所定の画像URLへアクセスしダウンロード(④)するという流れ。
上の図例ではアマゾンの株価を取得している。
起動コードは下記の4パターンを定義した。(〇〇〇にティッカーシンボルを入力する)
-
株価
〇〇〇 -
チャート
〇〇〇 -
stock price
〇〇〇 -
chart
〇〇〇
いずれを指定しても動作に違いはない。なお指定された銘柄が存在しない場合はエラーコードをテキストメッセージで返すようにする。
##採用したもの一覧
種類 | 名前 | 備考 |
---|---|---|
DB取得ライブラリ | pandas-datareader | 0.8.1 |
作図ライブラリ | matplotlib | 3.1.1 |
株価情報サービス | IEX Cloud | アメリカの新興取引所であるInvestors Exchange Groupが提供するAPI。(利用にはAPI_KEYの取得が必要) |
〃 | Yahoo Finance | 2019年11月現在、生存確認。(ただしスクレイピングは原則禁止されているらしいのであくまで動作確認用として用いるのみ) |
〃 | Google Finance | 未確認 |
DBサーバいろいろあるみたいだが、株価情報が安定して取得できるは多くない模様。
一番確実なのでいうとIEX Cloudなのかな?
#1.(IEX Cloudの場合)API_KEYの取得
IEX Cloudにてユーザ登録しログインすると、
ポータルから[API Tokens]⇒[My Tokens]⇒[PUBLISHABLE]にて
API_KEYであるpk_xxxxxxxxxxx
の文字列を確認することができ
#2.Python3.6環境への移行
いきなり話が脱線したところから始まるが
前回までに構築してきた現LINE BOTが動作する仮想環境(botenv)はpython3.4
をベースにしていたが、
今回のメインとなるpandas-datareader
はどうやらpython3.4
に対応しておらず、
インストールしようとしたらpythonのversion変更を迫られてしまった。
そこで本稿では備忘録としてpython3.6
の仮想環境構築と、現環境の移設手順を記録しておく。
誰かの役に立つことがあれば幸いである。
まず事前準備として現行環境で利用しているパッケージリストを作成しておく。
(botenv) [botenv]$ pip freeze > requrements.txt
(botenv) [borenv]$ deactivate
[borenv]$ cd ..
$
OS本体にpython3.6が入っていなければインストール
$ sudo yum install -y https://centos7.iuscommunity.org/ius-release.rpm
$ sudo yum install -y python36u-devel python36u-pip
$ python3.6 -V
python 3.6.7
#(参考)デフォルトバージョン
$ python -V
python 2.6.6
python3.6がデフォルトバージョンとして動作する仮想環境を新規作成する
$ virtualenv -p python3.6 botenv2
$ cd botenv2
[borenv2]$ source bin/activate
#結果確認
(botenv2) [borenv2]$ python -V
Python 3.6.7
先ほど抜き出したパッケージリストを元に一括インストール
(botenv2) [borenv2]$ pip install -r ../botenv/requrements.txt
#結果確認
(botenv2) [borenv2]$ pip list
以上で環境構築は完了。
従来と同じ要領でDjangoプロジェクトとアプリの作成を行い、
ソースコードを移設してくれば移設完了。
#Djangoプロジェクト(プロジェクト名:line_bot)作成
(botenv2) [botenv2]$ django-admin startproject line_bot
(botenv2) [botenv2]$ cd line_bot
#botアプリケーション(アプリ名:bot)作成
(botenv2) [line_bot]$ python manage.py startapp bot
#各種ソースコードを移設
(省略)
もっと要領良い手順があればご教示くださいorz
(参考) Python, pipでrequirements.txtを使ってパッケージ一括インストール
(参考) virtualenvの仮想環境構築でPythonのバージョンを指定する
#3.株価情報データの取得
必要パッケージの準備を行う。
#pipアップデート
(botenv2) [line_bot]$ pip install --upgrade pip
#pandasインストール
(botenv2) [line_bot]$ pip install pandas
(botenv2) [line_bot]$ pip install pandas-datareader
動作確認としてAT&Tの株価を取得してみる。
(botenv2) [line_bot]$ python
Python 3.6.7 (default, Dec 5 2018, 15:02:16)
[GCC 4.4.7 20120313 (Red Hat 4.4.7-23)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> import pandas_datareader.data as web
>>> from datetime import datetime
>>>
>>> os.environ['IEX_API_KEY'] = 'pk_xxxxxxxxxxxx' #API_KEYを設定
>>>
>>> start = datetime(2019,10,1)
>>> end = datetime(2019,10,10)
>>> t = web.DataReader('T', 'iex', start, end)
>>> print(t.head())
open high low close volume
date
2019-10-01 37.95 37.96 37.37 37.41 24334716
2019-10-02 37.35 37.35 36.92 37.08 26612141
2019-10-03 37.00 37.20 36.66 37.19 21001457
2019-10-04 37.22 37.52 37.13 37.51 22598045
2019-10-07 37.58 37.87 37.52 37.66 21245336
>>>
ちなみにyahooだともう少し細かい情報が得られる。
>>> t2 = web.DataReader('T', 'yahoo', start, end)
>>> print(t2.head())
High Low Open Close Volume Adj Close
Date
2019-09-30 37.919998 37.529999 37.590000 37.840000 28341900.0 37.325100
2019-10-01 37.959999 37.369999 37.950001 37.410000 23038500.0 36.900951
2019-10-02 37.349998 36.919998 37.349998 37.080002 26257000.0 36.575443
2019-10-03 37.200001 36.660000 37.000000 37.189999 20372600.0 36.683945
2019-10-04 37.520000 37.130001 37.220001 37.509998 21914100.0 36.999588
>>>
#4.株価チャートの描画
必要なパッケージのインストールを行う。
(botenv2) [line_bot]$ pip install matplotlib
(botenv2) [line_bot]$ pip install https://github.com/matplotlib/mpl_finance/archive/master.zip
(botenv2) [line_bot]$ pip install seaborn
(botenv2) [line_bot]$ pip install numpy
試しにAlphabetの2年チャートを描画してみる。
import os
import datetime
from dateutil.relativedelta import relativedelta
import pandas_datareader.data as web
import matplotlib.pyplot as plt
#グラフを滑らかに
import seaborn as sns
sns.set_style('whitegrid')
source = 'iex'
os.environ['IEX_API_KEY'] = 'pk_xxxxxxxxxxxxxx'
#銘柄と期間指定
symbol = 'GOOG'
end = datetime.datetime.now()
start = end - relativedelta(years=2)
#データ取得
df = web.DataReader(symbol, source, start, end)
#初期化
plt.figure()
#チャート描画
df['close'].plot(figsize=(10, 5), color='blue', label=symbol)
#凡例の表示
plt.legend()
上記コードを手元のPC上のJupyter Notebookで実行すると下記のグラフが出力される。
今回はこれをデフォルトフォーマットとしてBOTに実装していくこととしたい。
さてこのpyplot
であるが、BOTアプリはサーバサイドで動作するため標準出力先がない。よってこのままではグラフを表示することはできない。そこで描画プロセスの出力先にバックエンドを指定して画像ファイルとして書き出すという処理が用いられる。
バックエンドを設定するためにはpyplot
をimportする直前に下記のように記述する必要がある。
import matplotlib as mpl
mpl.use('Agg')
import matplotlib.pyplot as plt
また画像ファイルへの出力にはsavefig
メソッドを使用することで実現する。
plt.savefig(xxx.png)
#5.BOTアプリに実装
それでは本格的にBOTのコードを書いていく。
今回は新たに外部ファイルgetStockdata.py
を用意して一連の処理を外部関数get_chart
としてまとめることとした。
一度取得した生データはCSVファイルとして保存しておき、同日に同じ銘柄への要求が繰り返し発生した場合は、APIアクセスの代わりに既存CSVファイルを読み込むようにすることにより、APIサーバへの負荷を軽減させている。
株価の最新値と前日比を計算する簡単な数式も組み込む。
import os
import shutil
import datetime
import logging
logger = logging.getLogger('getStockdata')
#date
import datetime
from dateutil.relativedelta import relativedelta
#dandas
import pandas as pd
import pandas_datareader.data as web
#plotをbackendに出力処理
import matplotlib as mpl
mpl.use('Agg')
import matplotlib.pyplot as plt
#図形処理
import seaborn as sns
sns.set_style('whitegrid')
#計算処理
import numpy as np
import mpl_finance as mpf
#png保存ディレクトリ
STOCK_DIR = '/usr/share/nginx/html/static/stock'
#データソース選択
##source = 'iex'
##os.environ['IEX_API_KEY'] = 'pk_xxxxxxxx' #API_KEYを入力する
source = 'yahoo'
#チャート取得関数 (引数が空の場合はT(AT&T)のデータを参照する)
def get_chart(symbol='T'):
#初期化
plt.figure()
symbol = symbol.upper()
name = 'chart-' + symbol + '-' + str(datetime.date.today())
namePNG = name + '.png'
nameCSV = name + '.csv'
pathPNG = STOCK_DIR + '/' + namePNG
pathCSV = STOCK_DIR + '/' + nameCSV
#既に当日分のデータが取得済の場合はCSVから読み込み
if os.path.exists(pathCSV):
logger.debug('read CSV') #logging
df = pd.read_csv(pathCSV)
df['Date'] = pd.to_datetime(df['Date']) #ObjectからDatetimeへ型変換
logger.debug(df['Date'].dtype) #logging
df.set_index('Date',inplace=True) #DatetimeIndexとしてIndex設定
logger.debug(type(df.index)) #logging
logger.debug('read CSV ---> Done') #logging
#データが存在しない場合は新規取得
else:
try:
#期間指定
end = datetime.datetime.now()
start = end - relativedelta(years=2)
#データ取得
logger.debug('read web data') #logging
df = web.DataReader(symbol, source, start, end)
logger.debug('read web data ---> Done') #logging
except:
return None,'Exception error:\nsymbol [' + symbol + '] is not found.'
#グラフ描画
df['Adj Close'].plot(figsize=(10, 5), color='blue', label=symbol)
plt.title('symbol:' + symbol + ' 2-year chart ')
plt.legend()
plt.savefig(namePNG)
logger.debug('read web data ---> make png -> Done') #logging
#CSV出力
df.to_csv(nameCSV)
logger.debug('read web data ---> make csv -> Done') #logging
#WEBサーバへ配置
shutil.move(namePNG,pathPNG)
shutil.move(nameCSV,pathCSV)
#応答文
logger.debug('calc') #logging
idx = df.tail(1).index
s_date = str(idx[0].date())
prc = np.array(df.tail(2))
s_price = str(round(prc[1,5],2))
s_diff = '{:+.2%}'.format((prc[1,5] - prc[0,5]) / prc[0,5])
text = '[' + symbol + '] ' + s_date + '\n終値 $' + s_price + ' (前日比' + s_diff + ')'
logger.debug('calc ---> Done') #logging
return namePNG,text
if __name__ == '__main__':
print(get_chart())
BOT本体コードの方には起動コードによる条件分岐とティッカーシンボルの抽出処理のみを追加する。
# -*- Coding: utf-8 -*-
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
from django.shortcuts import render
from datetime import datetime
from time import sleep
import requests
import json
import base64
import logging
import os
import random
import log.logconfig
from utils import tools
import re #★追記
from .getStockdata import get_chart #★追記
logger = logging.getLogger('commonLogging')
LINE_ENDPOINT = 'https://api.line.me/v2/bot/message/reply'
LINE_ACCESS_TOKEN = '' #Line developer管理画面で確認
LINE_ALLOW_USER='' #Line developer管理画面で確認
ULOCAL_ENDPOINT = 'https://chatbot-api.userlocal.jp/api/chat'
ULOCAL_API_KEY = '' #Userlocal登録時に確認
IMAGE_DIR = '/usr/share/nginx/html/media'
IMAGE_URL = 'https://mydomain.com/media'
CATVIEW_KEY = ['カメラ','室内','部屋','猫']
CATVIEW_RES = ['カメラの画像を送るよ',
'猫ちゃんの様子です',
'自宅警備中です',
'写真で確認よろしく']
STOCK_URL = 'https://mydomain.com/static/stock' #★追記
STOCKVIEW_KEY = ['株価','stock price','チャート','chart'] #★追記
@csrf_exempt
def line_handler(request):
#exception
if not request.method == 'POST':
return HttpResponse(status=200)
logger.debug('line_handler message incoming') #logging
out_log = tools.outputLog_line_request(request) #logging
request_json = json.loads(request.body.decode('utf-8'))
for event in request_json['events']:
reply_token = event['replyToken']
message_type = event['message']['type']
user_id = event['source']['userId']
#whitelist
if not user_id == LINE_ALLOW_USER:
logger.warning('invalid userID:' + user_id) #logging
return HttpResponse(status=200)
#action
if message_type == 'text':
if any(s in event['message']['text'] for s in OUTVIEW_KEY):
action_res(reply_token,'outview')
elif any(s in event['message']['text'] for s in STOCKVIEW_KEY): #★追記
action_data(reply_token,'stockview',event['message']['text']) #★追記
else:
#ulocal chat
response_text(reply_token,ulocal_chatting(event))
return HttpResponse(status=200)
def action_res(reply_token,command,):
if command == 'outview':
#監視カメラ画像
files = os.listdir(IMAGE_DIR)
#logger.info(files) #logging
orgFiles = [ s for s in files if '_view' in s ]
orgPATH = IMAGE_DIR + '/' + orgFiles[0]
orgUrl = IMAGE_URL + '/' + orgFiles[0]
subtext = random.choice(OUTVIEW_RES)
sleep(1)
response_image(reply_token,orgUrl,orgUrl,subtext)
##########################################★追記 ここから
def action_data(reply_token,command,value):
if command == 'stockview':
#株価チャート
logger.debug('get_chart on') #logging
symbols = re.search('[a-zA-Z]+$', value)
logger.debug('get_chart symbol = ' + symbols.group()) #logging
result = get_chart(symbols.group())
logger.debug('get_chart finish') #logging
if result[0] is not None:
orgUrl = STOCK_URL + '/' + result[0]
response_image(reply_token,orgUrl,orgUrl,result[1])
else:
response_text(reply_token,result[1])
##########################################★追記 ここまで
def response_image(reply_token,orgUrl,preUrl,text):
payload = {
"replyToken": reply_token,
"messages":[
{
"type": 'text',
"text": text
},
{
"type": 'image',
"originalContentUrl": orgUrl,
"previewImageUrl": preUrl
}
]
}
line_post(payload)
def response_text(reply_token,text):
payload = {
"replyToken": reply_token,
"messages":[
{
"type": 'text',
"text": text
}
]
}
line_post(payload)
def line_post(payload):
url = LINE_ENDPOINT
header = {
"Content-Type": "application/json",
"Authorization": "Bearer " + LINE_ACCESS_TOKEN
}
requests.post(url, headers=header, data=json.dumps(payload))
out_log = tools.outputLog_line_response(payload) #logging
logger.debug('line_handler message -->reply') #logging
def ulocal_chatting(event):
url = ULOCAL_ENDPOINT
payload={
'key' : ULOCAL_API_KEY,
'message': event['message']['text']
}
out_log = tools.outputLog_ulocal_request(payload) #logging
logger.debug('ulocal_chatting send request') #logging
ulocal_res = requests.get(url,payload)
logger.debug('ulocal_chatting -->recv response') #logging
out_log = tools.outputLog_ulocal_response(ulocal_res) #logging
data = ulocal_res.json()
response = data['result']
return response
それでは新環境でLINE BOTを起動してみる。
(botenv2) [line_bot]$ gunicorn --bind 127.0.0.1:8000 line_bot.wsgi:application
LINEアプリから書式に沿ったメッセージを投下すると結果が得られるだろう。
応答が機械的になってしまうのでもう少し人間味を加えたほうがいいかな。
今回はここまで。