ストックドッグ

KatoTakahiro。金融系の会社で働くSEが株やPython、その他諸々について書いています。サービスも運営してます→http://fmbrain.work

herokuでのスケジュール管理にAPSchedulerを使ってみる

目次

herokuで定期的にファイルを実行したいとき何を使っていますか?

cron?

heroku scheduler?

APScheduler?

cron...?

DBの更新、データの取得、バックアップの保存などのスケジュールの管理はモダンなWebアプリには必須機能です。

ubuntuのデフォルトでインストールされているcronはあらゆるスケジューリングが可能で、上記のほとんどのことはcronによって達成することが出来ます。

ですが、cronはサーバー上で動作することもあり、Webアプリのテストをローカル環境で行うことを考えると、もっと高いレイヤーのアプリケーションのインスタンスで動作するほうが勝手がいいです。

heroku scheduler...?

herokuにはスケジュール管理のためのファンタスティックなアドオンが用意されています。

このheroku schedulerはシンプルなタスクを10分、1時間、一日、複数の間隔での実行が可能です。

しかし、5分や37分など中途半端な時間での実行は難しいです。

cronよりもheroku schedulerよりAPSchedulerが良い

アプリケーションのインスタンスで動作し、かつ、中途半端な時間でも簡単にスケジュールの管理が可能なのが、APSchedulerです。

これはPython製のライブラリなので、私のようにwebアプリを全てpythonで完結させたい人たちにはもってこいです。

heroku上でAPSchedulerを使ってみる

ということで、1分おきに現在時刻をDBにインサートするプログラムを書いてみます。

最終的な出力はこうなります。

herokuのログを見ると、1分おきにDBを更新していることがわかります。

f:id:doz13189:20170410001021p:plain

用意するもの

herokuでアプリを運用するために必要なファイルは以下です。

#適当に用意してください
runtime.txt
requirements.txt

#以下は今から作成します
Procfile
gettime.py
time.db
timeclock.py

スタート。

#screamtimeという名前のアプリを作ります
heroku create screamtime

次に、gettime.pyを作成します。

import sqlite3
import pandas as pd
import datetime


#現在時刻を取得する
now = datetime.datetime.today()
hour = now.hour
minute = now.minute

#DataFrame形式にぶち込む
#普通はSQL文書いてインサートすると思いますが、
#私がDataFrame形式に慣れているので、こちらを使用していますが、各自なれた方法でお願いします
time_df = pd.DataFrame({"Hour" : [hour], "Minute" : [minute]})


#db作成
dbname = "time.db"

#dbコネクト
conn = sqlite3.connect(dbname)
c = conn.cursor()

#dbのnameをtime_dfとし、読み込んだcsvファイルをsqlに書き込む
#if_existsでもしすでにtime_dfが存在していても、置き換えるように指示
time_df.to_sql("times", conn, if_exists="replace")

#作成したdbを見てみる
select_sql = 'select * from times'
for row in c.execute(select_sql):
    print(row)

conn.close()

これをgettime.pyという名前で保存します。

次は、今作成したgettime.pyをAPSchedulerで定期的に実行させるコードを書きます。

from apscheduler.schedulers.blocking import BlockingScheduler
import os

sched = BlockingScheduler()

#間隔は1分ごとにしています
#minutesではなくてhourに変更したら、時間での指定も可能です
@sched.scheduled_job('interval', minutes=1)
def timed_job():
	print("Let's start")
	os.system("python gettime.py")


sched.start()

これをtimeclock.pyという名前で保存します。


最後にProcfileを作成します。

clock: python timeclock.py

これで、herokuにclockのファイルであるということを宣言します。

ファイルが全て揃ったので、あとはherokuに上げるだけです。

念の為、herokuに上げるまでのgitでの手順を載せておきます。

git init
git add .
git commit -m "first commit"
git push heroku master

エラーなく行けば、これでherokuには「1分おきに現在時刻をDBにインサートするプログラム」がアップロードされました。

以下でherokuにclockをスケールします。

heroku ps:scale clock=1

上手くスケールされたことを確認して。

heroku ps
|<

数分待ってからherokuのログを見てみます。

>||
heroku logs

f:id:doz13189:20170410001021p:plain

以上で「1分おきに現在時刻をDBにインサートするプログラム」が完成しました。

まとめ

APSchedulerはとても簡単にスケジュールの管理が行うことができます。

コードもpythonで書くことができるので、使い勝手がとても良いです。

cronよりもheroku schedulerよりもAPSchedulerが良いんじゃあないでしょうか!?

herokuのworker dynoとweb dynoの違いって何?

目次

dyno?

herokuでは、プロセスの処理はdynoによって行われます。

プロセスの処理とはHTTPのリクエストやレスポンス、バックグラウンド処理などです。

dynoは3種類あり、処理をするプロセスの種類によって使い分けられます。

  • web dyno
  • worker dyno
  • one-off dyno


herokuを勉強していて、dynoはheroku内の独特の単位で一番はじめにつまづいたのでまとめておきます。

web dyno

HTTPのリクエスト・レスポンスを処理します。

ユーザーからのリクエストはweb dynoが処理するので、webアプリが行う処理の大半はweb dynoによって行われています。

worker dyno

バックグラウンド処理を行います。(ほぼweb dyno以外の処理)

バックグラウンド処理とは、例えばAPIsからのデータ取得やRSSフィードの読み込み、画像のリサイズ、S3へのデータ送信などが当てはまります。

one-off dyno

一時的な処理を行うためのdyno。

heroku run

などのherokuコマンドを実行したときに使用されるdynoです。

ログ

これらdynoの動作状況はログを見ることによって確認できます。

#ログの確認
heroku logs -t

#見たい行数を指定する(最大1500行まで)
heroku logs -n 200

Control+Cでログの出力を中止できます。

ログは直近の1500行のみしか表示されず、ログの保存を行う場合はアドオンを追加します。

使用中のdynoを確認する

heroku ps

このコマンドで現在、使用しているdynoを確認することができます。

まとめ

worker dynoとweb dynoの違いは、まとめると処理するプロセスの違い、ということになります。

Webアプリによって必要な処理は違ってくるので、アクセス数がひたすら多いWebアプリはweb dynoを増やしたり(=スケーリング)、バックグラウンドでの処理が多いならworker dynoを増やしたりと適宜変更しながら運用していきたいですね。

cronで定期的にフォルダを作成してみる(Ubuntu 16.04)

色々とcronについて勉強したので、今回はcronで一分毎にTestという名のフォルダを作成するまでやってみようと思います。

cronとは

設定したファイルを定期的に実行させるためのツール

crontabに登録されているcronを確認

crontab -e

編集等はvimで行うことになります。
もし、まだcronがひとつも登録されていなければ、no crontab for <ユーザー名> が表示されます。

crontabは /etc の中にあるのですが、私はvimの使い方がわからなかったので、sublimetext(テキストエディタ)に移してから編集していましたが、これではうまくいきませんでした。(編集しても内容が反映されない)

おそらく、編集・実行の権限が与えられていなかったからだと思います。

諦めてvimの使い方をドットインストールで学んできて、ようやく編集することができました。

vimは癖が強すぎるが、慣れれば最高に使いやすいツールになることの片鱗をつかめた!)

そもそもcronが動作しているのかをチェック

sudo service cron status

active(running)が表示されていれば正常に動作しています。

また、cronの動作ログもこれで確認可能です。

基本的にcronの動作ログは /var/log のsyslogファイルに書かれています。

なので。

cd /var/log
cat syslog

で、確認可能ですが、いちいちsyslogを見に行くのも面倒なので、sudo service cron status で確認したほうが楽です。


定期的に実行させるファイルを作成

/home にtest.shというファイルを作成します。

#!/bin/shシェルスクリプトであるということを明示するためのものです。

mkdir Test にしているので、cronが上手く実行されれば/home にTestという名のフォルダが作成されるはずです。

(mkdirでもなくても大丈夫です、今回はcronが動作しているかどうかを確認したいため、適当にmkdirにしました)

#!/bin/sh
mkdir Test

crontabからcronを設定

crontab -e
* * * * * /bin/sh /home/ubuntu/test.sh > /dev/null 2>&1

時間は1毎分に設定しています。

エラーが発生していた場合、すぐに確認できるので。

また、cronによって実行するファイルがシェルスクリプトであることを明示するために /bin/sh を記述しています。

cronでの時間設定の書式は以下のサイトがわかりやすかったです。

qiita.com


これでcronの設定は終わったので、ログを見てみます。

3月 30 23:09:01 tk CRON[4148]: (ubuntu) CMD (/bin/sh /home/test.sh)

CMDとはcronが実行したコードを示しています。

ログを見る限り、cronは正常に実行されています。

ディレクトリを見てみると...

これが...

テンプレート
ピクチャ
test.sh
デスクトップ
ミュージック
ドキュメント
公開
ダウンロード
ビデオ

1分後...

Testフォルダが作成されたので、無事にcronが動作したようです。

Test
ダウンロード
ビデオ
テンプレート
ピクチャ
test.sh
デスクトップ
ミュージック
ドキュメント
公開

MTAというエラーが出た場合

cronはエラーが出た際、メールで通知しようとするのですが、メールの設定を行っていない場合にMTAというエラーが出ます。

postfixなどでメールの設定を行うことでこのエラーは解決できます。

以下のサイトがわかりやすかったです。

ryoichi0102.hatenablog.com

まとめ

今回はcronを動かすまでを最短距離で行ったので、至らない点が多数あります。

cronにはまだまだ工夫の余地があり、以下のサイトがとても参考になります。

dqn.sakusakutto.jp

bootの容量がいっぱいなのに、古いカーネルが消せない問題(ubuntu 16.04)

bootの容量がいっぱいすぎて、新しいカーネルに更新できず、まわりまわって他のソフトウェアもアップデートできないという問題が発生しました。

試しにbootの容量を確認してみると...

df /boot
Filesystem     1K-blocks   Used Available Use% Mounted on
/dev/sda7         236876 224404         0 100% /boot

見事に100%!!
100%って何か気持ち良いですね!元気がでました。

だがしかしだけれども、何とかbootの容量を空けないと身動きができないので、古いカーネルを消そうとしました。

とりあえず、現在使用中のカーネルは...

uname -r
4.4.0-63-generic


そして、boot内にあるカーネルは...

dpkg --get-selections | grep linux-
linux-base					install
linux-firmware					install
linux-generic					install
linux-headers-4.4.0-57				install
linux-headers-4.4.0-57-generic			install
linux-headers-4.4.0-59				install
linux-headers-4.4.0-59-generic			install
linux-headers-4.4.0-62				install
linux-headers-4.4.0-62-generic			install
linux-headers-4.4.0-63				install
linux-headers-4.4.0-63-generic			install
linux-headers-4.4.0-64				install
linux-headers-4.4.0-64-generic			install
linux-headers-4.4.0-66				install
linux-headers-4.4.0-66-generic			install
linux-headers-4.4.0-70				install
linux-headers-4.4.0-70-generic			install
linux-headers-generic				install
linux-image-4.4.0-21-generic			deinstall
linux-image-4.4.0-53-generic			deinstall
linux-image-4.4.0-57-generic			install
linux-image-4.4.0-59-generic			install
linux-image-4.4.0-62-generic			install
linux-image-4.4.0-63-generic			install
linux-image-4.4.0-64-generic			install
linux-image-4.4.0-66-generic			install
linux-image-4.4.0-70-generic			install
linux-image-extra-4.4.0-21-generic		deinstall
linux-image-extra-4.4.0-53-generic		deinstall
linux-image-extra-4.4.0-57-generic		install
linux-image-extra-4.4.0-59-generic		install
linux-image-extra-4.4.0-62-generic		install
linux-image-extra-4.4.0-63-generic		install
linux-image-extra-4.4.0-64-generic		install
linux-image-extra-4.4.0-66-generic		install
linux-image-extra-4.4.0-70-generic		install
linux-image-generic				install
linux-libc-dev:amd64				install
linux-sound-base				install
syslinux-common					install
syslinux-legacy					install

放置っぷりが明らかに...
古いカーネルはたくさんあるし、最新のカーネルにアップデートもしてないし...

現在使用しているのが、4.4.0-63なので、それ以前のカーネルは削除しようと思います。

sudo apt-get autoremove --purge linux-image-4.4.0-57-generic

すると未解決の依存関係があるようで、削除できません。

パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています                
状態情報を読み取っています... 完了
以下の問題を解決するために 'apt-get -f install' を実行する必要があるかもしれません:
以下のパッケージには満たせない依存関係があります:
 linux-image-extra-4.4.0-57-generic : 依存: linux-image-4.4.0-57-generic しかし、インストールされようとしていません
 linux-image-extra-4.4.0-70-generic : 依存: linux-image-4.4.0-70-generic しかし、インストールされようとしていません
 linux-image-generic : 依存: linux-image-4.4.0-70-generic しかし、インストールされようとしていません
E: 未解決の依存関係です。'apt-get -f install' を実行してみてください (または解法を明示してください)

'apt-get -f install'を実行したとしても、bootの容量がいっぱいなのでどのみちインストールはできません。

sudo apt-get -f install linux-image-4.4.0-70-generic


けっこうな詰みです。
古いカーネルを消そうとしても、未解決の依存関係が原因で消せない、にもかかわらず、その未解決の依存関係を修復するために最新のカーネルをインストールしようとしてもbootの容量がいっぱい...


ということで最終手段、依存関係を無視して古いカーネルを消すことにしました。

cd /boot
ls boot

一番古い57系のものを色々と削除。

sudo rm linux-image-4.4.0-57-generic


ここでもう一度容量を確認。

df /boot

Filesystem     1K-blocks   Used Available Use% Mounted on
/dev/sda7         236876 184731     35608  84% /boot

無事にbootを整理できました。

以上終わり!

SQLite3をPandasから操作する

SQL文の操作を覚えるのが面倒...

select * from tbl_nameまでが覚えれる限界でした。

データの検索・変更などのwhere文等を覚えるのが面倒かつ、逃げ道を見つけてしまったので、逃げ道のほうに行くほかない。

ということで、SQLite3のデータをPandasのDataFrame型に出力してから、DataFrame型でデータを加工するという技を身につけたので、ここにメモとして保存します。

これだと、Pandasでデータをいじくり回して、最後にSQLite3に保存するだけなので、楽ちん。


Pandasの万能さを実感し、かつ、SQL文を覚える機会をまたまた失ったわけであります。

#SQLite3をインポート
import sqlite3
import pandas as pd


#db
dbname = "code.db"


#dbコネクト
conn = sqlite3.connect(dbname)
c = conn.cursor()


#dbを見てみる
pd.read_sql_query("select * from code_7776", conn)


#dbをpandasのDataFrame型で読み込む
df = pd.read_sql("select * from code_7776", conn)

#dfの中身はすでにDataFrame形式
#データの変更等はPandasでのデータいじりの操作方法でいける
df["Altering"] = None
print(df)


#データをいじり終わったら、SQLite3のdbに書き込む
df.to_sql("code_7776", conn, if_exists="replace")


【参考】

www.dataquest.io

PandasのデータをSQLite3で保存する

スクレイピングしたデータをどうすればいいかわからない問題発生

株価データ等をYa〇〇〇ファ〇〇〇〇などからスクレイピングして、今まではCSVファイルでローカルのディレクトリに置いていました。

言ってしまえば、スクレイピングして放置状態でした。

データを使用するときはCSV形式なので、今まではそれが都合良かったのですが...

毎回毎回スクレイピングするのは相手方のサーバーに申し訳ないので、そろそろデータベースで管理したほうがいいかなぁ、と思うようになりまして。

データベースで管理すれば、サイトが更新されたデータ分だけを拾って、データベースを更新するだけで済むので、かなり時間も節約できますし。

そこでSQLite3を勉強していたのですが、はてさてpandasのDataFrame形式はいかにしてDBに放り込むのか。


少し調べるとpandasでできるらしいぞ、と。

SQLiteに放り込むCSVファイルはこんな形式。

f:id:doz13189:20170322005408p:plain


コードを以下に貼ります。

import sqlite3
import pandas as pd


#pandasでcsvファイルを読み込む
df = pd.read_csv("code_1332.csv")
df.columns= ["Date", "Open", "High", "Low", "Close", "Volume", "Adj Close", "Number"]

#db作成
dbname = "code.db"

#dbコネクト
conn = sqlite3.connect(dbname)
c = conn.cursor()

#dbのnameをcode_1332とし、読み込んだcsvファイルをsqlに書き込む
#if_existsでもしすでにcode_1332が存在していても、置き換えるように指示
df.to_sql("code_1332", conn, if_exists="replace")

#作成したdbを見てみる
select_sql = 'select * from code_1332'
for row in c.execute(select_sql):
    print(row)

conn.close()

出力を見てみる。

f:id:doz13189:20170322004401p:plain

SQLite3に書き込まれ、DBが作成されています。

pandasって万能やなぁと今日も感じた次第であります。

以上報告終わり!なのですが...


他のシストレやる方たちはどのようにデータを集めて保存しているのですか?

もっと効率良い方法があるなら教えて欲しいです。

もしよければコメント下さい、お願いしますm(_ _)m

Yahooファイナンスの株価予想ページをScrapyでクローリング・スクレイピング

目次

本日やること

Yahooファイナンスでは、毎日アナリスト的な人たちが株価予想を行っています。
今回は、そのページをScrapyでクローリングしながら、各アナリストたちの予想をスクレイピングして、ファイルに保存します。

info.finance.yahoo.co.jp


完成予定ファイル

こんな感じのページを…

f:id:doz13189:20170321184350p:plain

こんな感じのファイルにまとめます!

f:id:doz13189:20170321184425p:plain

必要な環境

python3
scrapy
jq
ubuntu(OSは自由)

なぜscrapy?

scrapyはクローリング・スクレイピングを行うためのライブラリです。
ライブラリの記述ルールを覚えてしまえば、とてもシンプルにクローリング・スクレイピングを行うことができます。

分析等を行うには大前提としてデータが必要です。
データは様々なサイトから集めてくると思うのですが、個々のサイトごとにコードを書いていては大変ですし、また管理の手間も増えます。

scrapyでスクレイピングを行えば、トータル的に無駄が省けてハッピーということです。

Python以外にはあまりスクレイピングのためのライブラリはないようなので、scrapyをバンバン使って、無駄という無駄を省きパイソニストのメリットを最大限に活かしましょう!笑

スクレイピングの流れ

スクレイピング対象のサイトのHTML構成を確認。
scrapyでサイトからスクレイピング
以上!!!(めっちゃ簡単)

Scrapyのインストール

pip install scrapy

Scrapyプロジェクトの作成

scrapy startproject mypredict

Ubuntuで言う端末、Macで言うターミナル、Windowsで言うコマンドプロンプトに上記のコードを打ちましょう。

これでScrapyのプロジェクトが作成されます。
このコマンドを打つと、mypredictというファイルが作成されます。
以降、このフォルダの中で作業を行います。

念の為、ファイル構成を確認しておきましょう。

f:id:doz13189:20170321185616p:plain

中には、Scrapyの設定を記述してあるファイルやスクレイピングした内容を検証するためのファイルがありますが、今回は使用しません。

では、mypredictに移動します

cd mypredict

基本的には、作業はこの階層で行います。

Spiderの作成

scrapyでは、スクレイピングをするためのルールをこのspiderに記述します。
そのため、スクレイピングを行うサイトごとにspiderを作成するイメージです。

スクレイピングをするにはサイトごとに異なるページ構成を考慮する必要があります。
class名やid名などサイトによって使用しているものが異なるので、そういったサイトの個別のルールをこのspiderに記述します。
※ページ内容のダウンロード等は他のファイル(すでに用意されている)

まず、spidersフォルダの中にpredict.pyというファイルを作成します。
このファイルがクローリング・スクレイピングするファイルとなります。

今回作成するファイルはこれだけです。
このファイルで全ての処理を行います。
記述するコードも30行にもいきません。

※注意、ファイルを作成するのはspidersの中ですが、作業をするのはあくまでもmypredict直下です。scrapy.cfgがある階層です。

とりあえず、predict.pyというファイルだけ作成して、そのままにしておきます。

スクレイピングするサイトを確認

先ほど貼った画像と同じです。

f:id:doz13189:20170321184350p:plain


この予想一覧を上から順にクローリングしていき、URLをたどっていきます。

最終的には、このページ内にある全ての個別の予想ページの中身の文章をスクレイピングしていきます。

f:id:doz13189:20170321190701p:plain


ここでクローリングとスクレイピングの意味を確認しておきます。

私もそこまではっきりと認識しているわけではなくて、ニュアンスで使っていますが...

クローリングはサイトの情報を閲覧する

スクレイピングは、閲覧している情報をダウンロードしてくること

こんな感じだと思います、以降このような意味でクローリング・スクレイピングという言葉を使っていきます。

スクレイピングはじまるよー

ではまず、予想トップページをスクレイピングしてみましょう。
この予想トップページから個別ページに繋がるURLを拾ってくるのが目的です。
以下のコードを、ちょっと前に作成してほったらかしにしているpredict.pyに記述します。

import scrapy


class PredictSpider(scrapy.Spider):
	#predictはSpiderの名前。spider実行時に入力する。
	name = "predict"

        #必ず必要なコードです
     #相手サーバーに配慮するために、スクレイピングの間隔に1秒入れます
        custom_settings = {
            "DOWNLOAD_DELAY": 1,
        }

	#スクレイピングを行う際に一番最初にクローリングするページ
	start_urls = ["http://info.finance.yahoo.co.jp/kabuyoso/article/"]
	
	def parse(self, response):

		#Google Chromeの検証等でページの構成を確認すると。
		#URLはHTMLのh3タグのクラス st02の下に格納されていることがわかる。
		for href in response.css("h3.st02 a::attr('href')"):
			
			#相対URLから絶対URLに変換
			url = response.urljoin(href.extract())

			#スクレイピングした内容を確認
			print(url)

ちなみに、ページ構成のスクショは以下。

こればっかりはページのHTMLを確認しながら自分で地道にたどっていくしかないです。

f:id:doz13189:20170321190913p:plain


先ほどのコードを実行してみます。

mypredict直下で以下のコードを打ちます。

scrapy crawl predict

こんな感じでURLがリスト形式で表示されれば成功です。

f:id:doz13189:20170321191206p:plain


さて、これでたどるURLを入手することができました。
次にこのURLを辿って、ページの中身をスクレイピングします。
先ほどのコードを少し変更し、さらに個別ページをたどるコードを追加しています。

import scrapy


class PredictSpider(scrapy.Spider):
	name = "predict"

        custom_settings = {
            "DOWNLOAD_DELAY": 1,
        }

	start_urls = ["http://info.finance.yahoo.co.jp/kabuyoso/article/"]

	def parse(self, response):
		for href in response.css("h3.st02 a::attr('href')"):
			url = response.urljoin(href.extract())
			
			#ここから下が追加分
			#スクレイピングしたURLをたどるためにparse_topicsに投げる
			yield scrapy.Request(url, self.parse_topics)


	def parse_topics(self, response):
		#個別ページのHTML構造を確認する
		#h3タグのクラスst02にタイトルが格納されていることがわかる
		title = response.css("h3.st02 ::text").extract_first()

		#本文はdlタグのクラスmarB20に格納されていることがわかる
		#これを::textで抽出するとbrタグも含めまれて上手くスクレイピングできない
		#xpathのstring()を使用すると、うまい具合にスクレイピングすることができる
		body = response.css("dl.marB20").xpath("string()").extract()

		#辞書形式で格納する
		yield {
			"title" : title,
			"body" : body
		}

先ほどと同じように、たどるページ先のHTML構成を確認しておきます。

f:id:doz13189:20170321191511p:plain


さて実行してみます。

Scrapy crawl predict

実行結果はこちらです。

f:id:doz13189:20170321191620p:plain

スクレイピングが成功していたら、何かしらの文字列が表示されると思います。

スクレイピング内容をファイルに保存

とても簡単です。

#-oはファイルに保存する
#predict.json、ここは自由
#保存したいファイル名とファイル形式を指定するだけです
scrapy crawl predict -o predict.json

これだけです。

中身を見てみましょう。

#jqを事前にインストールする必要あり
cat predict.json | jq


実行結果です。

f:id:doz13189:20170321192042p:plain


まとめ
上手くできましたか?
思いの外簡単にスクレイピング出来たと思います。
なんせ、書いたコードは30行に満たない程度ですから。

今回はファイルに保存しましたが、これをデータベースに保存してもOKですし、実際には管理のしやすさ等を考えるとそちらのほうがベターかと思います。

私もまだまだscrapyを触り始めたばかりなので、これから色々と使っていきたいと思います。