てけとーぶろぐ。

ソフトウェアの開発と、お絵かきと、雑記と。

Pythonで株価アラートをつくる(4)

今回は前回までで作成した株価アラートプログラムをRaspberry Pi上で動かしていきます。

Raspberry Piの用意

Raspberry Piには幾つもモデルがあります。何を選ぶのがいいでしょうか。
最新モデルは Raspberry Pi 4 ですが「Raspberry Pi Raspberry Pi 3 Model B」「Raspberry Pi Raspberry Pi 2 Model B」あたりが価格も落ち着いていておすすめです。

jp.rs-online.com

jp.rs-online.com

性能もそれなりですが今回の用途には問題ないはずです。

Raspberry Pi Zeroは価格も消費電力も魅力的ですが経験上性能が十分ではないので今回の用途にはおすすめできません。

安定稼働のためにイーサネット接続(有線LAN接続)もできたほうがいいのでその点でもRaspberry Pi Zeroは候補から外れます。

アクセサリーの用意

電源
電源はアンペア数が十分なものを選びましょう。以下がおすすめです。

SDカード
容量はひとまず8GBもあれば十分です。あとは、ブランド、値段、読み書きの速度を見て選びます。
読み書きの速度についてはUHSスピードクラス1、SDスピードクラス10で、読み出し20MB/s もあれば十分です。
1,000円もしないと思います。例えば以下がおすすめです。

その他
お使いのルーターイーサネットケーブルの差込口があること、つまりRaspberry Piを有線LAN接続できることをご確認ください。
接続のためのイーサネットケーブルも必要になります。ルーターのすぐそばにRaspberry Piも設置するならば短いものを。
個人的に薄いケーブルが取り回しが良くておすすめです。例えば以下。

ルーターイーサネットケーブルの差込口がなくwifi接続にする場合は「wifi接続にする場合は」を参照してください。

OS(Raspbian)のインストール

Download Raspbian for Raspberry Pi
より「2020-02-13-raspbian-buster-lite.zip」をダウンロードします。
zipファイルを展開してできた「2020-02-13-raspbian-buster-lite.img」をSDカードに書き込みます。

imgファイルのSDカードへの書き込みにはWin32 Disk Imagerを使います。
Win32 Disk Imager download | SourceForge.net
からダウンロード、インストールします。

SDカードをSDカードリーダーにセットし、Win32 Disk Imagerを起動。
Image Fileとして「2020-02-13-raspbian-buster-lite.img」を指定して
Deviceとして書き込み先のSDカードのドライブを指定して「Write」ボタンをクリックします。

SSH接続の許可

Raspberry Piの操作はPCからSSH接続して行います。
Raspberry PiSSH接続ができるように、SSH接続を許可します。

具体的にはimgファイルを書き込んだSDカードのルートディレクトリー内に「ssh」という名前の空のファイルを作成します。

参照: Enable SSH when creating SD card - Raspberry Pi Forums

コマンドプロンプトで行う場合はコマンドプロンプトを開きまずカレントドライブをSDカードのドライブにします。
「D」ドライブであれば以下の通り。

> D:
> cd \

ファイルを確認します。

> dir

「overlays」というディレクトリーなどがあるはずです。
そこで「ssh」という名前の空のファイルを作成します。

> type nul > ssh
有線LAN接続

自宅のルーターRaspberry PiEthernetケーブルで接続します。

(参考)wifiで接続する場合

wifi接続(無線LAN接続)したい場合はRaspberry PiにUSBキーボード、マウス、ディスプレイを接続し、デスクトップPCのようにして設定してしまうのが簡単です。
(Raspberry Pi Raspberry Pi 2 Model B の場合にはUSB無線LANアダプターも必要です。)
その場合OSはGUIで設定するために「Raspbian Buster Lite」ではなく「Raspbian Buster with desktop」を選択します。
Ethernetケーブル接続の代わりに以下のサイトなどを参考にwifiでネットワーク接続します。
【STEP-07】ラズベリーパイを無線LAN(WiFi)でインターネット接続│FABSHOP.JP -デジタルでものづくり! ファブショップ !

一度接続すれば以降起動時に自動接続されるようになります。

Raspberry Piの起動

Raspberry PiにSDカードを挿し、電源ケーブルを接続すれば起動します。
電源スイッチ付きのケーブルの場合は電源スイッチをONにしてください。

SSHログイン

Windows 10 バージョン1803以降であればsshコマンドが使えます。
このsshコマンドを使ってRaspberry PiSSH接続、ログインします。
コマンドプロンプトから以下のコマンドを実行します。

> ssh -l pi raspberrypi.local

「pi」はRaspbian標準のユーザー名、「raspberrypi.local」はRaspbian標準のホスト名です。
「pi@raspberrypi.local's password:」などとユーザー「pi」のパスワードを求められたら「raspberry」と入力します。

ログアウトする場合は

$ exit

シャットダウンするばあいは

$ sudo shutdown -h now

です。

Python 3 のバージョン確認

RaspbianにはPythonがはじめからインストールされています。
ただしpythonコマンドではPython 2が実行されます。
Python 3の実行にはpython3コマンドを使います。
詳細なバージョンを確認してみましょう。

$ python3 --version
Python 3.7.3
pipのインストール

Pythonパッケージのインストールのためにpipをインストールしておきましょう。

$ sudo apt-get update
$ sudo apt-get install python3-pip

pipについてもpip3コマンドを使うことになります。
例えばapschedulerをインストールするのであれば以下のコマンドになります。

$ pip3 install apscheduler
プログラムの配置

コマンドプロンプトからscpコマンドでRaspberry Piにファイルをコピーできます。
カレントディレクトリーにある「stock-price-watcher」ディレクトリー以下を
ホームディレクトリー内にコピーする場合は

> scp -rp stock-price-watcher pi@raspberrypi.local:~/

「-rp」はディレクトリー以下を再帰的にコピーし、コピー元のファイルの更新日時を保ってコピーします。

パッケージのインストール

プログラムの実行にあたって必要なパッケージをインストールします。

$ pip3 install apscheduler
$ pip3 install bs4
$ pip3 install dataclasses_json
環境変数の設定

プログラムの実行にあたって必要な環境変数の設定をします。
例えば環境変数GMAIL_USER」に「hoge@gmail.com」をセットするのであれば以下のコマンドになります。

$ export GMAIL_USER=hoge@gmail.com
Pythonプログラムの実行

実行したいプログラムの*.pyファイルがあるディレクトリーに移動してから
python3コマンドを使ってプログラムを実行します。

$ cd ~/stock-price-watcher
$ python3 main_multiple_code.py

Ctrl + C キーでプログラムを停止します。

Pythonプログラムのバックグラウンド実行

上記の方法で実行した場合ログアウトとすると
プログラムの実行が終了してしまいます。
ログアウトしても終了しないようにするためにはバックグラウンド実行をする必要があります。
具体的には以下のコマンドでバックグラウンド実行します。

$ nohup python3 main_multiple_code.py &

バックグラウンド実行したプログラムを停止する場合は
まず以下のコマンドでプロセスの一覧を表示して
停止したいプロセスのプロセス番号を確認します。

$ ps -fu pi

プロセスのプロセスID(PID)がわかったら
それを使ってkillコマンドでプログラムを停止します。
例えばPIDが17870であれば以下のコマンドで停止します。

$ kill 17870


これでRaspberry Pi上でPythonプログラムを実行することができるようになりました。
次回から本題のビットコインに入っていきたいと思います。

Pythonで株価アラートをつくる(3)

前回まででPythonで株価アラートプログラムをつくりました。

これを実際に使うとなると、どこかでこのプログラムを動かし続ける必要があります。
そのためには何を使うのがいいでしょうか?

この記事は「Raspberry Pi + Pythonビットコイン自動取引」へとつなげる、そこで使う要素技術を身につけるための記事という位置づけです。

そして「Raspberry Pi + Pythonビットコイン自動取引」では

  • 高実現性
  • 低コスト

をコンセプトにしたいと考えています。

プログラムを動かし続けるというとすぐに浮かぶのはクラウドサービスかと思います。
WindowsLinuxの仮想サーバーであれば手元のPCと同じ感覚で使えます。
しかし基本的に有償、従量課金となっています。料金体系の理解、理解した上での利用が必要となります。動かし続けるとなるとサーバーのスペックを絞っても月1500円程度はかかるでしょう。

プログラムを動かすというとPaaSという選択肢もありますが、こちらも動かし続けるとなると有償というものが多く、またストレージへの書き込みに制限があったりとそのプラットフォームに合わせた工夫が別途必要になってきます。

考えてみれば外部サービスは、提供側はそれで儲けようとしているわけですから、それなりに費用はかかりますよね。

そこで内部に目を向けます。
実現性という意味では既に手元のPCでプログラムを動かしているわけで、自分のPCが一番です。
手元のPCを動かし続ければ…?いわゆる自宅サーバーですね。
この場合のネックは電気代です。あと、普段使うPCでとはいかないでしょうからPC代もでしょうか。

電気代は例えばノートPCだとつけっぱなしにすると月1800円とかかかってきますね。

参考:
パソコンの電気代はどれくらい?節約するにはどうしたら良いの?

普通のPCと同じ感じで使えて、安価で、消費電力が少ないPCはないでしょうか…?

あぁ、ありますね、「Raspberry Pi」。

Raspberry Pi であれば数千円で買えて、消費電力もノートPCの1/10とか。

Raspberry Pi + Pythonビットコイン自動取引」の片方のキーワードが出てきました。

ということで自宅でRaspberry Piを使って株価アラートプログラムを動かします。

次回は具体的にRaspberry Piとその他必要なものを選び、セットアップ、プログラムの実行まで行います。

Pythonで株価アラートをつくる(2)

Raspberry Pi + Pythonビットコイン自動取引」を目指して
まずは株価アラート、の2回目。

前回はアラート条件をソースコードに埋め込んでしまっていたので
それを外部のアラート条件定義ファイルで定義できるようにする。
複数銘柄の監視にも対応させる。

アラート条件定義ファイル

アラート条件定義ファイルは、手での編集のしやすさ、プログラムからの扱いやすさ、表現力を考えて以下のようなjsonファイルとする。

{
    "alertConditions": [
        {
            "alertConditionId": "f870215c-944a-11ea-8747-185e0fdcc982",
            "stockCode": "4755",
            "conditionType": "isEqualToOrHigher",
            "values": [
                1000
            ],
            "isEnabled": true
        },
        {
            "alertConditionId": "f870215d-944a-11ea-80ac-185e0fdcc982",
            "stockCode": "6758",
            "conditionType": "isEqualToOrHigher",
            "values": [
                5000
            ],
            "isEnabled": false
        }
    ]
}

各プロパティの説明は以下の通り。
これで複数銘柄のアラート条件を定義できる。
条件の種類としてはとりあえず基本的な「X円以上になったら」「X円以下になったら」の2種類に対応させることにする。

プロパティ 説明
alertConditions アラート条件の配列
alertConditionId アラート条件のID
stockCode 銘柄コード
conditionType アラート条件の種類
"isEqualToOrHigher":X円以上になったら
"isEqualToOrLower":X円以下になったら
values アラート条件に使う値の配列(現状1つの値しか使わない)
isEnabled 有効か否か(このアラート条件について監視を行うか)
アラート条件のクラス化

ファイルのフォーマットが決まったので
まずはファイルを扱うクラスを「alert_condition.py」としてつくる。

dataclass, dataclasses_jsonを使うと
そのクラスのインスタンスjsonにしたり
jsonからそのクラスのインスタンスをつくったり
といった機能を持つクラスが簡単につくれる。

そこでdataclass, dataclasses_jsonを使ってアラート条件定義ファイルのjsonに対応したAlertConditionクラス、AlertConditionListクラスを作成する。

これだけの定義でもう alert_condition_list.to_json() でクラスのインスタンスjsonにしたり、AlertConditionList.from_json() でjsonからクラスのインスタンスをつくったりできてしまう。

from typing import List, Dict
from dataclasses import dataclass
from dataclasses_json import LetterCase, dataclass_json
import uuid
from collections import OrderedDict


class AlertConditionType:
    IS_EQUAL_TO_OR_HIGHER = 'isEqualToOrHigher'
    IS_EQUAL_TO_OR_LOWER = 'isEqualToOrLower'

@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class AlertCondition:
    alert_condition_id: str
    stock_code: str
    condition_type: str
    values: List[int]
    is_enabled: bool

@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class AlertConditionList:
    alert_conditions: List[AlertCondition]


class AlertConditionUtil:
    JSON_FILE_NAME = 'conditions.json'
    
    @classmethod
    def save(cls, alert_condition_list: AlertConditionList):
        with open(cls.JSON_FILE_NAME, 'w', encoding='utf-8') as f:
            f.write(alert_condition_list.to_json(indent=4, ensure_ascii=False))

    @classmethod
    def load(cls) -> AlertConditionList:
        with open(cls.JSON_FILE_NAME, 'r', encoding='utf-8') as f:
            return AlertConditionList.from_json(f.read())
    
    @staticmethod
    def generate_id() -> str:
        return uuid.uuid1()


if __name__ == '__main__':

    ac1 = AlertCondition(AlertConditionUtil.generate_id(), '4755', AlertConditionType.IS_EQUAL_TO_OR_HIGHER, [1000], True)
    ac2 = AlertCondition(AlertConditionUtil.generate_id(), '6758', AlertConditionType.IS_EQUAL_TO_OR_HIGHER, [5000], True)

    acl = AlertConditionList([ac1, ac2])

    AlertConditionUtil.save(acl)
    new_acl = AlertConditionUtil.load()
    print(new_acl.to_json(indent=4, ensure_ascii=False))

あとはjsonファイルへの保存、jsonファイルからの読み込み、アラート条件IDの生成といったユーティリティメソッドを持ったAlertConditionUtilクラスもつくっておく。

実行してみるとアラート条件定義ファイル「conditions.json」が作成される。

アラート条件に従った監視

アラート条件定義ファイルからアラート条件を読み込むことができるようになったので
このアラート条件を使って株価の監視をしてみる。

import os
from apscheduler.schedulers.background import BackgroundScheduler
from mail_sender import MailSender
from stock_price import StockPrice
from time import sleep
from alert_condition import AlertConditionUtil, AlertConditionType
import datetime

GMAIL_USER = os.environ.get("GMAIL_USER")
GMAIL_PASSWORD = os.environ.get("GMAIL_PASSWORD")
TO_ADDRESS = os.environ.get("TO_ADDRESS")

mail_sender = MailSender(GMAIL_USER, GMAIL_PASSWORD, TO_ADDRESS)

is_condition_enabled = True
is_exception_raised = False


def update():
    global is_exception_raised

    acl = AlertConditionUtil.load()

    # 有効なアラート条件の銘柄コードのユニークなリストを取得
    stock_codes = [ac.stock_code for ac in acl.alert_conditions if ac.is_enabled]
    stock_codes = list(set(stock_codes))

    # 株価を取得
    ticker_dict = {}
    try:
        for stock_code in stock_codes:
            ticker = StockPrice.get_ticker(stock_code)
            ticker_dict[stock_code] = ticker
    except Exception as exception:
        mail_sender.send_mail('update() Exception Alert', f'exception:\n{exception}')
        is_exception_raised = True
        return

    # アラート条件の確認
    for alert_condition in acl.alert_conditions:
        if not alert_condition.is_enabled:
            continue

        stock_code = alert_condition.stock_code
        values = alert_condition.values
        ticker = ticker_dict[stock_code]
        if alert_condition.condition_type == AlertConditionType.IS_EQUAL_TO_OR_HIGHER:
            if ticker.price >= values[0]:
                mail_sender.send_mail(f'[株価アラート] 銘柄コード:{stock_code}', 
                                      f'銘柄コード:{stock_code}の株価が{values[0]}円以上になりました。')
                alert_condition.is_enabled = False

        elif alert_condition.condition_type == AlertConditionType.IS_EQUAL_TO_OR_LOWER:
            value = alert_condition.values[0]
            if ticker.price <= value:
                mail_sender.send_mail(f'[株価アラート] 銘柄コード:{stock_code}', 
                                      f'銘柄コード:{stock_code}の株価が{values[0]}円以下になりました。')
                alert_condition.is_enabled = False

    AlertConditionUtil.save(acl)


if __name__ == '__main__':
    scheduler = BackgroundScheduler()

    # 毎分0秒に実行する
    scheduler.add_job(update, 'cron', second=0, max_instances=10)
    scheduler.start()

    try:
        while True:
            if is_exception_raised:
                break

            sleep(0.001)

    except KeyboardInterrupt:
        pass

    scheduler.shutdown()

前回作成した監視と基本は同じ。主な違いは以下の点。

  • アラート条件をアラート条件定義ファイルから読み込んだものを使っていること
  • 複数銘柄対応のためにループしていること

それから一定時間ごとの処理は毎分0秒のタイミングで実行するようにした。
apschedulerだとこの変更がパラメーターの変更だけでできてしまう。

実行すると、プログラムは毎分0秒のタイミングでアラート条件に従った監視を行い、条件を満たすとその旨を知らせるメールを送信する。
一度条件を満たしたアラート条件については無効になる。

だいぶ実用的になってきたのではないだろうか。

しかし実際に使うとなると、プログラムを実行しつづけなくてはならない。
PCをずっと起動しておくの…?
電気代大丈夫…?

ということで、次回はどう動かし続けるかについて書きたい。

Pythonで株価アラートをつくる(1)

Raspberry Pi + Pythonビットコイン自動取引」というところを目指して
何回かにわたって記事を書いていこうと思う。

最終的にはビットコインなのだけど
まずは手始めにより多くの人に馴染みがある日本の株式を扱う。

プログラムで好きな銘柄の株価を取得して
取得するだけではつまらないので
株価が設定した条件を満たしたらメールで通知するようにしてみよう。

証券会社のサイトにも似たような機能があるとは思うが
プログラムを書けばより高度な条件も設定できる。

環境

PythonPython Releases for Windows | Python.orgから
Python 3.7.3 - March 25, 2019」の「Windows x86-64 executable installer」をダウンロードしてインストールしている。

株価の取得

株探(かぶたん)からBeautifulSoup4を使ってスクレイピングで株価を取得する。

ある銘柄の株価は例えば銘柄コード「7860」であれば
https://kabutan.jp/stock/kabuka?code=7860
というURLから見られるようになっている。

そこでここから株価を得るクラスを「stock_price.py」としてつくる。

from bs4 import BeautifulSoup
from urllib import request
import datetime
from dataclasses import dataclass


@dataclass
class Ticker:
    code: str
    price: int
    time: datetime.datetime
    opening_price: int
    high_price: int
    low_price: int
    closing_price: int
    from_the_day_before: int
    from_the_day_before_percentage: float
    volume: int


class StockPrice:
    @staticmethod
    def get_ticker(code: str):
        url = 'https://kabutan.jp/stock/kabuka?code=' + code
        response = request.urlopen(url)
        soup = BeautifulSoup(response, 'html.parser')
        response.close()

        # 現在の株価と時刻は画面上部のstockinfo_i1から取得する
        div = soup.find('div', id='stockinfo_i1')
        span = div.find('span', class_='kabuka')
        time = div.find('time')

        # 時刻以外はstock_kabuka0クラスのテーブルから取得する
        table = soup.find('table', class_='stock_kabuka0')
        tbody = table.find('tbody')
        tds = tbody.find_all('td')
        if len(tds) != 7:
            raise RuntimeError('Unexpected HTML Structure.')

        price = int(span.text.replace(',', '').replace('円', ''))
        dt = datetime.datetime.fromisoformat(time.attrs['datetime'])
        opening_price = int(tds[0].text.replace(',', ''))
        high_price = int(tds[1].text.replace(',', ''))
        low_price = int(tds[2].text.replace(',', ''))
        closing_price = int(tds[3].text.replace(',', ''))
        from_the_day_before = int(tds[4].text.replace(',', '').replace('+', ''))
        from_the_day_before_percentage = float(tds[5].text.replace(',', '').replace('+', ''))
        volume = int(tds[6].text.replace(',', ''))

        daily_ticker = Ticker(code, price, dt, 
                                opening_price, high_price, low_price, closing_price, 
                                from_the_day_before, from_the_day_before_percentage, volume)
        return daily_ticker


if __name__ == '__main__':

    print(StockPrice.get_ticker('1775'))

株価が取得できるか実行してみる。

> python .\stock_price.py
Ticker(code='1775', price=1798, time=datetime.datetime(2020, 5, 8, 15, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400))), opening_price=1770, high_price=1800, low_price=1740, closing_price=1798, from_the_day_before=33, from_the_day_before_percentage=1.9, volume=11300)

取得できた。

メール通知

続けてメール通知をできるようにする。
メール送信にはGmailアカウントを使うことにする。

今回はGmailアカウントを使って自作プログラムからのメール送信を簡単に行うために
「安全性の低いアプリのアクセス」を許可することにする。

Webブラウザーで以下の操作を行う。

  1. メール送信に使うGmailアカウントでログイン
  2. Googleアカウントの「セキュリティ」→「安全性の低いアプリのアクセス」から安全性の低いアプリのアクセスをオンにする。

「mail_sender.py」としてメール送信を行うクラスをつくる。

import smtplib
from email.mime.text import MIMEText
from email.utils import formatdate
import os


class MailSender:
    def __init__(self, gmail_user, gmail_password, to_address):
        self._gmail_user = gmail_user
        self._gmail_password = gmail_password
        self._to_address = to_address

    def send_mail(self, subject, body):
        msg = MIMEText(body)
        msg['Subject'] = subject
        msg['From'] = self._gmail_user
        msg['To'] = self._to_address
        msg['Bcc'] = ''
        msg['Date'] = formatdate()

        smtpobj = smtplib.SMTP_SSL('smtp.gmail.com', 465, timeout=10)
        smtpobj.login(self._gmail_user, self._gmail_password)
        smtpobj.sendmail(self._gmail_user, self._to_address, msg.as_string())
        smtpobj.close()


if __name__ == '__main__':

    GMAIL_USER = os.environ.get("GMAIL_USER")
    GMAIL_PASSWORD = os.environ.get("GMAIL_PASSWORD")
    TO_ADDRESS = os.environ.get("TO_ADDRESS")

    mail_sender = MailSender(GMAIL_USER, GMAIL_PASSWORD, TO_ADDRESS)

    mail_sender.send_mail('subject', 'body')


メール送信ができるか実行してみるのだがその前に
このプログラムはOSの環境変数からGmeilアカウントと送信先メールアドレスを取得するようになっている。
そこであらかじめ環境変数に値を設定しておく必要がある。
以下の環境変数を設定する。

環境変数 説明
GMAIL_USER メール送信に使うGmailアカウントのメールアドレス
GMAIL_PASSWORD メール送信に使うGmailアカウントのパスワード
TO_ADDRESS 送信先メールアドレス

コマンドプロンプトであれば以下のように設定できる。

> set GMAIL_USER=hoge@gmail.com

PowerShellであれば以下の通り。

> $env:GMAIL_USER="hoge@gmail.com"

準備ができたら実行する。

> python .\mail_sender.py

おそらく初回は送信できない。
その代わりに、メール送信に使うGmailアカウントのGmail受信トレイに
「不審なログインの試みをブロックしました」
といったメールが届いている。
これがまさに自作プログラムからのログインであるということであれば
「心当たりがある」とすることで以降ブロックされないようになる。


簡単な株価の監視

株価の取得とメール送信ができたので
それらを使って簡単な株価の監視をつくる。

一定時間ごとに指定の銘柄の株価の取得を行い
株価が指定の価格以上になったらメール通知するようにする。

一定時間ごとの実行は以下のような単純なものでもいいのだが

if __name__ == '__main__':

    while True:
        update()
        sleep(3)

後で拡張しやすいようにapschedulerのBackgroundSchedulerクラスを使う。
「main_single_code.py」として作成する。

import os
from apscheduler.schedulers.background import BackgroundScheduler
from mail_sender import MailSender
from stock_price import StockPrice
from time import sleep

GMAIL_USER = os.environ.get("GMAIL_USER")
GMAIL_PASSWORD = os.environ.get("GMAIL_PASSWORD")
TO_ADDRESS = os.environ.get("TO_ADDRESS")

mail_sender = MailSender(GMAIL_USER, GMAIL_PASSWORD, TO_ADDRESS)

is_condition_enabled = True
is_exception_raised = False


def update():
    global is_condition_enabled
    global is_exception_raised

    if not is_condition_enabled:
        return

    try:
        ticker = StockPrice.get_ticker('1775')

        if ticker.price >= 100:
            mail_sender.send_mail('株価アラート', '株価が条件を満たしました')
            is_condition_enabled = False

    except Exception as exception:
        mail_sender.send_mail('StockPrice.get_ticker() Exception Alert', f'exception:\n{exception}')
        is_exception_raised = True


if __name__ == '__main__':
    scheduler = BackgroundScheduler()
    scheduler.add_job(update, trigger='interval', seconds=3, max_instances=10)
    scheduler.start()

    try:
        while True:
            if is_exception_raised:
                break

            sleep(0.001)

    except KeyboardInterrupt:
        pass

    scheduler.shutdown()

実行すると銘柄コード「1775」の株価が100円以上になったら
指定のメールアドレスにメールが送られる。

次回はアラートの条件を外部ファイルで設定できるようにする。

Node.jsでJSONを返すHTTPサーバーを作る

最近TypeScriptでWebアプリを作っています。
そのなかで使ったことないけどAPIサーバーにNode.jsという選択はありかもなぁ。
本格的に使わなくともちょっとしたものを作るのに使えたらよさそう。
と思うようになりました。

以下の点でよさそうだと思っています。

  • サーバー側もJavaScriptやらTypeScriptで書ける
  • Web APIで使うJSONと相性がいい
  • 気になっているMongoDBと相性がいい
  • 認証などに使っているFirebaseと相性がいい

ということでちょっと使ってみることに。

本屋でピッタリな書籍を見つけました。これで学びました。

入門Node.jsプログラミング

入門Node.jsプログラミング

  • 作者:Jonathan Wexler
  • 出版社/メーカー: 翔泳社
  • 発売日: 2019/09/25
  • メディア: 単行本(ソフトカバー)

大体わかったので試しに以前Pythonで書いたJSONを返すサーバーをNode.jsで作ってみます。
publicディレクトリーにJSONファイルを置いておいて、例えば「test.json」を置いて
そのファイル名のURL、この例だと「localhost:3000/test」にアクセスすると「test.json」の内容が返ってきます。
Webアプリの開発中にWeb APIのモックなんかに使えるかと思います。

以下手順。

Node.jsのインストール

以下からNode.jsをダウンロードしてインストールします。
Node.js

アプリケーションの初期化

ディレクトリーを作って「npm init」

$ mkdir json_server
$ cd json_server/
$ npm init

各質問にはエンターキーでデフォルトを選んで行けば問題なしですが
「entry point:」に対しては「main.js」を指定します。

ライブラリーのインストール

必要なライブラリーをインストールします。

$ npm install express http-status-codes --save

簡単にWebサーバー、Webアプリが作れるexpressと
HTTPのステータスコードの定義が入ったhttp-status-codesです。

コードの作成

json_server/main.js を以下の内容で作ります。

const express = require("express");
const app = express();

const errorController = require("./controllers/errorController")
const jsonFileController = require("./controllers/jsonFileController")

app.set("port", process.env.PORT || 3000);

app.get(/\/.+/, jsonFileController.showJsonFile);

app.use(errorController.pageNotFoundError); 
app.use(errorController.internalServerError);

app.listen(app.get("port"), () => {
    console.log(`Server is running at http://localhost:${app.get("port")}`);
});

json_server/controllers/jsonFileController.js を以下の内容で作ります。

const fs = require("fs");
const path = require("path");

exports.showJsonFile = (req, res, next) => {
    const dirPath = 'public';
    fs.readdir(dirPath, function(err, files) {
        if (err) {
            next(err);
            return;
        }
        if (err) next(err);
        for (const file of files) {
            if (!fs.statSync(path.join(dirPath, file)).isFile() ||
                path.extname(file) !== ".json") {
                continue;
            }
            if (req.url !== '/' + path.basename(file, path.extname(file))) {
                continue;
            }
            fs.readFile(path.join(dirPath, file), (err, data) => {
                if (err) {
                    next(err);
                    return;
                }
                res.send(JSON.parse(data.toString()));
            });
            return;
        }
        next();
    });
};

json_server/controllers/errorController.js を以下の内容で作ります。
こいつはエラー処理に特に気を使わない場合は不要です。

const httpStatus = require("http-status-codes");

exports.pageNotFoundError = (req, res) => { 
    let errorCode = httpStatus.NOT_FOUND; 
    res.status(errorCode);
    res.send("Page not found");
};

exports.internalServerError = (error, req, res, next) => { 
    let errorCode = httpStatus.INTERNAL_SERVER_ERROR; 
    console.log(`Error occurred: ${error.stack}`);
    res.status(errorCode);
    res.send({ error: error });
};

json_server/public/test.json としてサンプルのjsonファイルを作ります。

{
    "message": "こんにちは"
}
実行

以下のコマンドで実行します。
終了するときは Ctrl+C です。

$ node .\main.js

使い始めのお作法が分かればあとは楽ちんですね。

ヘッドレスChromeCastでYoutube音声をディスプレイ無しで再生

Youtube音声をバックグラウンドでダラダラ流したいなと思いました。

スマホで月額払ってYouTube Premiumでも使えという話ですが
押し入れに眠っているChromeCastを使ってもできそうだと思いました。

普通にChromeCastをディスプレイなりに繋げばいいわけですが
余っているディスプレイもないし、音だけ聞きたいのでディスプレイをわざわざ点けたくない。

ディスプレイ無しでChromeCastでYoutube音声は流せないのか?
いくつかのコネクターの接続でできました。

f:id:kurimayoshida:20190218133345j:plain
完成品(かっこわるい…)

HDMIVGA 変換コネクターをつないで
VGAネクター側に抵抗を差し込むとディスプレイがつながっている扱いになる
という仕組みです。

f:id:kurimayoshida:20190218133321j:plain
VGAネクター側の抵抗

スマホからChromeCastにYoutubeをキャストしてやるとスピーカーからYoutube音声が流れます。
スマホの画面を消しても流れ続けます。

f:id:kurimayoshida:20190218133456j:plain
再生中

以下が材料

Chromecast(第1世代)(他でもいけるはず。リンクは第2世代。)

HDMIとHDMI延長コネクター《メスとメス》

HDMIとHDMI延長コネクター《メスとメス》

f:id:kurimayoshida:20190218133029j:plain
抵抗(75Ω)3本

抵抗を指すと…というところは以下を参考にしました。

qiita.com

試していませんが
HDMIVGA 変換コネクターは以下のほうが小型でよさそうですね…。

また抵抗がむき出しなのは危険なので
55円でダミープラグを作ってVirtualDesktopのデスクトップを広げた話 - Qiita
のようにコネクターにしたほうがいいですね。

出来合いのものもあるようですが抵抗つけただけでできると知ると買う気が起きませんね…。

レーザーカッターでラズパイケースをつくる

以前100均素材でラズパイケースをつくったが
最近はもっぱらMDFボードをレーザーカッターで切ってつくっている。

こういうものには3Dプリンターだろうと思って試したのだが
現状では設計時間もコストも工作時間も精度も
レーザーカッターの方がいいという結論に。
箱状のものだからなのだとは思う。

自分のつくりかたを紹介する。

まずおおまかな設計。
今回はラズパイZeroの公式ケースがそのまま収まるケースを作る。

raspberry pi zero size」などでWeb検索してサイズの入った図面を手に入れ
サイズを把握する。

以下のサイトでベースとなる図面を作成する。

www.makercase.com

2.5mm厚のMDFを使う予定なので「Material Thickness」は2.5mmに。
「Generate Laser Cutter Case Plans」でsvgファイルを出力する。

f:id:kurimayoshida:20190202214854p:plain

このSVGIllustratorなどで編集して
カメラ用の穴やUSB端子用の穴を加えたりする。

正しい位置に穴を加えるために
前述のサイズの入った図面を使ったり
実際に公式ケースのサイズを測ったりする。
サイズを測るのにはデジタルノギスが便利。

できた図面。

f:id:kurimayoshida:20190202214838p:plain

図面ができたら2.5mm厚のMDFボードを買う。
大きなホームセンターに行けば手に入ると思う。
自分は近くのUnidy等で買う。
大きなものを買ってn等分にカットしてもらうと割安に。

f:id:kurimayoshida:20190202214924j:plain

あとはMDFボードをレーザーカッターで図面に従ってカットする。

レーザーカッターを時間貸ししている施設で行う。
自分はFabLabをよく使っている。

f:id:kurimayoshida:20190202214907j:plain

図面と素材を持っていくとレーザーカッターでカットしてくれるという場所もある。
自分はCAINZ工房を一度使ったことがある。

CAINZというホームセンター内にレーザーカッターがあり
そこで買ったMDFならカットしてくれる。
量産するならば割安になると思う。

f:id:kurimayoshida:20190202214903j:plain

カットできたら組み立てる。

f:id:kurimayoshida:20190202214928j:plain

ネジで止めて、お得意の100均の三脚をつける。

f:id:kurimayoshida:20190202214938j:plain

ちなみに
こちらの「ラズパイZero公式ケース用ケース(三脚付き)」を
2/10の同人ハードウェアフェスにて販売予定です。
よろしければお越しください。
mag.switch-science.com