もなかアイスの試食品

「とりあえずやってみたい」そんな気持ちが先走りすぎて挫折が多い私のメモ書きみたいなものです.

【Javascript】select要素の開いた/閉じたイベントを実装した話

はじめに

前回の記事の続き

monakaice88.hatenablog.com

前回の話は、セレクトボックスを選択した時(値が決まった時)にモーダルを表示するという機能を作成したけれども、

onchangeイベントを使っていたため、同じ値を選択したときにモーダルが表示されない問題点があった。

調べてみてもあまりコレだ!とくる内容のサイト・記事が見つからなかった

なので、セレクトボックスに「開かれた」、「閉じた」、「値が変わらなかった」という独自イベントを作成した話

実装

動作するイベントを調べた結果、「開くとき」に発生するイベントは「click」で「閉じるとき」に発生するイベントは「click blur keyup」ということがわかった(Chromeで調査)

今回はjQueryとAngularJSを使って実装

HTML

<div ng-app="App" ng-controller="Sample">
  <select ng-model="Sample.selectValue" ng-select-event expanded="Sample.onOpened()" unexpanded="Sample.onClosed()"  nonchanged="Sample.onNonChanged()" ng-change="Sample.onChanged()">
    <option value="1">1</option>
    <option value="2">2</option>
    <option value="3">3</option>
  </select>
  {{Sample.selectValue}}
</div>

JavascriptjQuery、AngularJS)

(function(window, jQuery, angular) {
  'use strict';
  
  var module = angular.module('App', []);
  
  module.controller('Sample', [
    '$scope',
    function($scope) {
      $scope.Sample = this;
      
      this.selectValue = '1';
    
      this.onOpened = function() {
        window.console.log('おーぷん');
      };
      
      this.onClosed = function() {
        window.console.log('しまった');
      };
      
      this.onChanged = function() {
        window.console.log('変わった');
      };
      
      this.onNonChanged = function() {
        window.console.log('変わってない');
      };
    }
  ]);
  
  /**
   * 普通のselect要素では、「リストボックスが開かれた」、「リストボックスが閉じた」、
   * 「値が変わらなかった」等のイベントが無いため作成
   * 
   * 以下のイベントを発生させる
   *   ・リストボックスが開かれた
   *   ・リストボックスが閉じた
   *   ・値が変わらなかった
   */
  module.directive('ngSelectEvent', [
    '$timeout',
    function($timeout) {
      return {
        restrict: 'A',
        scope: {
          expanded: '&',
          unexpanded: '&',
          nonchanged: '&'
        },
        link: function(scope, element, atts, ctrl) {
          var jQueryElement = jQuery(element);
          
          var WATCH_EXPAND_EVENTS = 'click';
          var WATCH_UNEXPAND_EVENTS = 'click blur keyup';
          
          jQueryElement.data('expanded', false);

          var __expanding = function() {
            jQueryElement.off(WATCH_EXPAND_EVENTS, __expanding);
            jQueryElement.data('expanded', true);
            jQueryElement.data('oldvalue', jQueryElement.val());
            jQueryElement.on(WATCH_UNEXPAND_EVENTS, __unexpanding);

            (scope.expanded)();
          };

          var __unexpanding = function() {
            jQueryElement.off(WATCH_UNEXPAND_EVENTS, __unexpanding);
            jQueryElement.data('expanded', false);
            jQueryElement.on(WATCH_EXPAND_EVENTS, __expanding);

            (scope.unexpanded)();
            
            var oldValue = jQueryElement.data('oldvalue');
            $timeout(function() {
              var newValue = jQueryElement.val();
              if (oldValue == newValue) {
                (scope.nonchanged)();
              }
            }, 0);
          };

          jQueryElement.on(WATCH_EXPAND_EVENTS, __expanding);
        }
      };
    }
  ]);
  
})(window, window.jQuery, window.angular);

今後の自分のためにも、AngularJSを使用しないソースコードも作ろうと思ったけど面倒くさくなったでござる

問題点?

見直しでjsfiddleで動作確認したところ、「ESC」キーを押したときの挙動がイマイチかも

「ESC」キー押してもイベントが走るように、「keyup」イベントを監視するようにしているけれども、2回押さないと反応してくれない・・・

他のキー(数字の1とか)を押した後だと、1回でイベントが呼ばれる感じ

keyupイベントは別処理にして、押されたキーのチェックをしたほうが良いかもしれない

【Javascript】ある要素のすべてのイベントの発生タイミングを調べる

経緯

セレクトボックスを選択した時(値が決まった時)にモーダルを表示するという機能を作成した

この時、セレクトボックスのonchangeイベントに関数を登録し、内部で値をチェック後、モーダルを表示する動きをしていた

この時の問題点は、同じ値を選択したときにモーダルが表示されないこと

onchangeイベントが発生しないため仕方ない・・・

チェックするイベントを増やせば良いと思ったものの、「選択中」とか「変わらなかった」とか分かりやすいイベントが無い

今回はセレクトボックスだったので、セレクトボックスに「開かれた」、「閉じた」、「値が変わらなかった」という独自イベントを追加したくなった

なので、セレクトボックスが「開かれた」・「閉じた」ときにどんなイベントが発生しているかチェックする処理を他のサイトを参考にしながら作成した

実装

以下のサイトを参考に作成

note.onichannn.net

(function(window, jQuery) {
  'use strict';

  function getAllEvents(element) {
    var result = [];
    for(var key in element) {
    if (key.indexOf('on') === 0) {
    var text = key.substr(2, key.length) ;
        result.push(text);
    }
  }
  return result.join(' ');
}

  window.console.log(jQuery('#test').context);
  window.console.log(getAllEvents(jQuery('#test')[0]));

  jQuery('#test').bind(getAllEvents(jQuery('#test')[0]), function(e) {
    window.console.log(e.type);
  });

})(window, window.jQuery);

↓実際のコード

セレクトボックスに「開かれた」、「閉じた」、「値が変わらなかった」という独自イベントを追加した話は次に書こうと思う

【Python】運行情報をSlack API を使って通知するデーモンを作ってみた

前にPythonでYahoo路線情報から運行情報をスクレイピングで取得するアプリを作った

monakaice88.hatenablog.com

あとPythonスクリプトをデーモン化させる方法もわかった

monakaice88.hatenablog.com

なので今回は運行情報が変わったときに、Slack APIで通知するデーモンを作ることにした

ざっくり機能まとめ

  • 10分間隔でYahoo路線情報にアクセスして、スクレイピング
  • 情報を監視する路線は複数対応可能
  • 以下の状態でSlackに通知する
    • トラブルを検知したとき
    • トラブルが無くなったとき
    • 情報が更新されたとき(掲載日時が変わったとき)

Pythonスクリプトの作成

普段Pythonを触らないので、命名規則やら標準関数・クラスに苦しみながら作ってみた

まずは、Yahoo路線情報からスクレイピングする機能を作成

#!/ust/bin/env python
# -*- coding: utf-8 -*-

from abc import ABCMeta, abstractmethod
from bs4 import BeautifulSoup
from datetime import datetime
from urllib import request
from my_util import logger, dict_to_str


class Scraper(metaclass=ABCMeta):
    def __init__(self, url):
        self._url = url

    @abstractmethod
    def get_data(self):
        pass

    def __make_proxy_url(self):
        pass


class JRService(Scraper):
    def __init__(self, url):
        super().__init__(url)

    def get_data(self):
        html = request.urlopen(self._url).read()
        soup = BeautifulSoup(html, 'lxml')

        # 路線情報
        route_element = soup.find('div', {'id': 'main'}).find('div', {'class': 'mainWrp'}).find('div', {'class': 'labelLarge'})
        route_name = route_element.find('h1', {'class': 'title'}).find(text=True, recursive=False)
        update_time_str = route_element.find('span', {'class': 'subText'}).find(text=True, recursive=False)

        update_time = self.__get_update_datetime(update_time_str)

        # 運行情報
        status_element = soup.find('div', {'id': 'mdServiceStatus'})
        information = status_element.find('p').find(text=True, recursive=False)

        posting_element = status_element.find('p').find('span')
        posting_data = self.__get_posting_datetime(posting_element)

        message = information + posting_data['datetime_str']

        trouble_class = status_element.find('dd', {'class': 'trouble'})
        in_trouble = False
        if trouble_class is not None:
            in_trouble = True

        data = {
            'route_name': route_name,
            'update_datetime': update_time,
            'posting_datetime': posting_data['datetime'],
            'message': message,
            'in_trouble': in_trouble
        }

        logger.info(dict_to_str(data))

        return data

    @staticmethod
    def __get_update_datetime(update_time_str):
        year = datetime.today().year
        convert_str = '%d年%s' % (year, update_time_str)
        update_time = datetime.strptime(convert_str, '%Y年%m月%d日 %H時%M分更新')
        return update_time

    @staticmethod
    def __get_posting_datetime(element):
        posting_date_str = ''
        posting_date = None
        if element is not None:
            posting_date_str = element.find(text=True, recursive=False)
            year = datetime.today().year
            convert_str = '%d年%s' % (year, posting_date_str)
            posting_date = datetime.strptime(convert_str, '%Y年(%m月%d日 %H時%M分掲載)')

        return {
            'datetime_str': posting_date_str,
            'datetime': posting_date
        }


次に周期的にスクレイピング処理を実行し、情報が変わっていたら、通知する処理を作成

#!/ust/bin/env python
# -*- coding: utf-8 -*-


from configparser import ConfigParser
from datetime import datetime
from scraping import JRService
from threading import Timer
from time import sleep
from sys import exit
from my_util import logger
import requests


config = ConfigParser()
config.read('./.env')
thread_time_min = config.getint('settings', 'watch_interval_min')
thread_time_sec = thread_time_min * 60
services = config.items('jr_services')


def push_slack_chat(name, message):
    url = config.get('settings', 'post_url')
    token = config.get('settings', 'token')
    channel = config.get('settings', 'channel')
    data = {
        'token': token,
        'channel': channel,
        'username': name,
        'text': message
    }
    requests.post(url, data=data)


def scrape_service():
    try:
        for key, url in services:
            scraper = JRService(url)
            data = scraper.get_data()

            last_posting_datetime_str = config.get('posting_datetime', key)
            last_posting_datetime = datetime.strptime(last_posting_datetime_str, '%Y-%m-%d %H:%M')

            last_in_trouble = config.getboolean('in_trouble', key)
            if data['in_trouble'] and (data['posting_datetime'] <= last_posting_datetime):
                continue
            elif (data['in_trouble'] and not last_in_trouble)\
                    or (last_in_trouble and not data['in_trouble'])\
                    or data['in_trouble'] and (data['posting_datetime'] > last_posting_datetime):

                push_slack_chat(data['route_name'], data['message'])

                in_trouble_str = 'True' if data['in_trouble'] else 'False'
                config.set('in_trouble', key, in_trouble_str)
                if data['in_trouble']:
                    posting_datetime_str = data['posting_datetime'].strftime('%Y-%m-%d %H:%M')
                    config.set('posting_datetime', key, posting_datetime_str)

                config.write(open('./.env', 'w'))

    except Exception as e:
        logger.error(e.with_traceback())
    finally:
        thread = Timer(thread_time_sec, scrape_service)
        thread.start()


if __name__ == '__main__':

    scrape_service()

    try:
        while True:
            sleep(0.1)
    except KeyboardInterrupt:
        exit(0)

全体的に日時の文字列をdatetime型に変換する方法がなんか冗長な気がする・・・

↓参考にならないかもしれないソースコードはコチラ

github.com

supervisorの設定

作成したPythonスクリプトをデーモン化するため、supervisorの設定を追記

ここで使用しているsupervisorのバージョンは2.1

ちなみに、Pythonスクリプトの配置場所は「/usr/local/bin/Train-Service-Information」

[program:train_service_information]
command=bash -c '. /etc/profile; cd /usr/local/bin/Train-Service-Information; python -u main.py'
autostart=true
autorestart=true
startsecs=10
startretries=3
log_stdout=true
log_stderr=true
logfile=/var/log/supervisor/train_service_information.log

commandで「. /etc/profile」や「cd /usr/local/bin/Train-Service-Information」をしているのは、環境変数がなかったり、カレントがsupervisor2.1で設定出来ないため

あと「python -u main.py」の【-u】オプションは、スクリプトデバッグログが全然出力されないのを防ぐため、バッファリングを無効化するオプション

↓参考サイト

pythonで標準出力のバッファリングを無効にする - blog.mouten.info

実行結果

f:id:monakaice88:20170207065231p:plain

ちゃんと遅延が発生した時と遅延が収まった時にSlackに通知することが出来た。

次の機能について

Yahoo路線情報は情報の更新に大きめなタイムラグがある

ツイッターのつぶやきを解析して通知する機能を入れたほうがリアルタイムな感じになるかも

あと気象庁XMLを受信し始めてからかなり時間が経っているので、いい加減利用しなければ・・・