見出し画像

【GAS】Webスクレイピングで急落銘柄通知ツールを作成してみた

どうも、こじまるです。前回急落銘柄を通知するツールを作成しましたが、今回はそちらの続編になります。

急落銘柄ツールをTwitterの仲良しさんに共有したのですが、導入サポートをしている中でユーザが扱うには少し不便な点が見つかりました。そのため、ユーザの利便性を向上させるために、GASを使って急落銘柄通知ツールを再作成することにしました。

1.はじめに

前回作成したツールの不便な点

前回作成したツールはプロセスを定期的に実行させる必要があります。ユーザ環境によっては、常にPC/サーバが起動している状態でない場合もあるため、ユーザの実行環境に依存せず、システムを稼働できるようにしたいと思いました。

また、pythonをインストールしたり、CLIでpipを使ったりしないといけないので、プログラミングをほとんど知らないユーザに使っていただくには少し導入障壁が高いです。そのため、少しでもユーザが導入しやすい環境で構築したいと思いました。

前回作成したツールの不便な点
1. プログラムの実行環境が常に稼働していなければならない
2. ユーザの導入障壁が高い

解決策

上記の解決策として、スクレイピングを行うメイン処理をGAS(Google App Script)で置き換えることにしました。

GASの採用理由
・サーバレスでプロセスを実行させることができること
・プロセスを定期実行することができる
・ユーザは環境構築不要

GASはGoogleのプラットフォーム上で実行されるため、サーバーレスでプロセスを実行させることができます。また、トリガー実行によるスケジュール登録もできるので、スケジューラでプロセスを定期実行する必要もなくなります。それ以外にも、環境構築などもほとんどする必要が無いため、ユーザは導入しやすくなっています。

2. システムの概要

メイン処理は下記のような流れで実行します。下記には記載していませんが、事前にGASをScedulerに登録する必要があります。

メイン処理(定期実行)

画像1

メイン処理の流れ
①スクリプトの定期実行
②銘柄情報を取得
③急落銘柄を通知

※今回は導入方法のみ説明し、スクレイピングの内容については説明しません。

3. 導入方法

環境構築

環境構築
1. SpreadSheetの用意
2. Parserライブラリ追加
3. スクレイピングのコードを設定
4. トリガー実行でスケジュール登録

1. Spread Sheetの用意
下記のSpread Sheetは事前処理済みのSpread Sheetになります。(詳細はこちら)

[Spread Sheet]->[ファイル]->[ダウンロード]->[Microsoft Excel(.xlsx)]より、Excelファイルをダウンロードします。

画像2

次に、所有するGoogleアカウントでログインし、ドライブにExcelファイルをアップロードしてください。[ドライブ]->[新規]->[ファイルのアップロード]より、Excelファイルを選択することでファイルをアップロードすることができます。

ドライブにExcelファイルとしてアップロードされた状態になります。Excelファイルにアクセスし、[ファイル]->[Googleスプレッドシートとして保存]を選択し、Googleスプレッドシートとして扱える状態にします。

2. Paraserライブラリの追加

Googleスプレッドシートより、[ツール]->[スクリプトエディタ]を選択します。すると、下記のようなエディタが表示されます。

画像3

Parserライブラリが必要になりますので、下記の記事を参考に設定してください。

3. スクレイピングのコードを設定

 コードは下記になりますので、スクリプトエディタに貼り付けます。

// 急落率
TARGET_DROP_RATE = 0.5;

function isHoliday(date) {
 if (date.getDay() === 0 || date.getDay() === 6) {
   return true;
 }
 
 // 日本の祝日カレンダーに終日予定があれば祝日とする
 var calendar = CalendarApp.getCalendarById('ja.japanese#holiday@group.v.calendar.google.com');
 var events = calendar.getEventsForDay(date);
 
 return events.length > 0;
}

function createTriggers() {
 console.log('createTriggers');
 var now = new Date();

 // 残っているトリガーを掃除する
 var triggers = ScriptApp.getProjectTriggers();
 if (Array.isArray(triggers)) {
   triggers.forEach(function(trigger) {
     // mainのトリガーのみを削除する
     if(trigger.getHandlerFunction() === 'main') {
       ScriptApp.deleteTrigger(trigger);
     }
   })
 }
 
 // 土日祝はスケジュールしない
 if (isHoliday(now)) {
   console.log('there are no schedules today');
   return;
 }
 var hours = ["9:30", "10:00", "10:30", "11:00", "11:30", "12:30","12:45", "13:00", "13:30", "14:00", "14:30"];
 hours.forEach(function(time) {
   var date = new Date();
   var array = time.split(':');
   var hour = parseInt(array[0]);
   var minute = parseInt(array[1]);
   date.setHours(hour);
   date.setMinutes(minute);
   if (now.valueOf() < date.valueOf()) {
     // main() のトリガーを指定した日時で作成
     ScriptApp.newTrigger("main").timeBased().at(date).create();
   }
 })
}

function main() {
 console.log('hello.')
 myFunction()
}

function fetch_element(html, from_element, to_element){
 return Parser.data(html).from(from_element).to(to_element);
}

function substitute(str, delimiter_list){
 for(var value of delimiter_list){
   str = str.replace(value, '');
 }
 return str;
}

function send_message(message){
 var url = "https://hooks.slack.com/XXXXXXXXXX"; // <- 変更が必要
 var payload = {
   "text":message,
   'icon_emoji':':squirrel:',
   'username':'StockBot'
 };
 
 var params = {
   "method" : "POST",
   "payload" : JSON.stringify(payload)
 };
 
 UrlFetchApp.fetch(url, params);
}

function myFunction() {
 const workbook = SpreadsheetApp.getActive();
 const sheet= workbook.getSheetByName("stockBot");
 const lastRow = sheet.getLastRow();
 const values = sheet.getRange("A2:E").getValues();

 let dt_now = new Date();
 dt_month = dt_now.getMonth() + 1;
 dt_now.setMonth(dt_now.getMonth() + 1);
 dt_next_month = dt_now.getMonth() + 1;
 var list = [];

 for (let i = 0; i < lastRow - 1; i++) {
   rowData = values[i];
   var stock_code = rowData[0] // コード
   var stock_name = rowData[1] // 銘柄名
   var stock_vesting_date = rowData[2] // 権利確定月

   if(stock_vesting_date == "随時"){
     continue;
   }else{
     var pattern = /\d+月/g;
     var month_list = stock_vesting_date.match(pattern);
     var array = []
     month_list.forEach(function(value,i){
       array[i] = substitute(value, [/月/g])
     });
     if(array.includes(String(dt_month)) || array.includes(String(dt_next_month))){
       console.log(stock_code);
     }else{
       continue;
     }
   }
   try{
     var url = "https://minkabu.jp/stock/" + stock_code + "/chart";
     var html = UrlFetchApp.fetch(url).getContentText("UTF-8");
     console.log("銘柄コード : " + stock_code);

     // 現在値
     var integer_str = fetch_element(html, '<div class="stock_price">', '<span class="decimal">').build();
     var decimal_str = fetch_element(html, '<span class="decimal">', '</span>').build();
     var current_price = parseFloat(substitute(integer_str,[/\,/g,/ /g,/\n/g]) + decimal_str);
     
     // 株価
     var stock_table = fetch_element(html, '<table class="md_table md_table_vertical">', '</table>').build();
     var stock_list = fetch_element(html, '<tr>', '</tr>').iterate();
 
     // 前日終値
     var previous_closing_place_str = fetch_element(stock_list[0], '<td class="num">', '</td>').build();
     var previous_closing_place = parseFloat(substitute(previous_closing_place_str,[/\,/g,/円/g]));
     
     // 急落率
     price_drop_rate = (1 - current_price / previous_closing_place) * 100;
     console.log(price_drop_rate);
     
     if(price_drop_rate >= TARGET_DROP_RATE){
       dict = {'コード':stock_code, '名前':stock_name, '現在値':current_price, '前日終値':previous_closing_place, '急落率':price_drop_rate.toFixed(2)};
       list.push(dict);
     }
   }catch(e)
   {
     console.error( "エラー:", e.message );
   }
 }
 message = "";
 for(message_dict of list){
       message += `【銘柄名:${message_dict["名前"]}】(コード:${message_dict["コード"]})\n`;
       message += `現在値:${message_dict['現在値']}\n`;
       message += `前日終値:${message_dict['前日終値']}\n`;
       message += `急落率:${message_dict['急落率']}%\n\n`;
 }
 send_message(message);
}

急落率(TARGET_DROP_RATE)は都合がいい値に変更してください。

// 急落率
TARGET_DROP_RATE = 0.5;

また、下記のURLはSlackのIncoming Webhooksを設定する必要があります。

function send_message(message){
 var url = "https://hooks.slack.com/XXXXXXXXXX"; // <- 変更が必要
 var payload = {
 ...

詳細はこちらでは割愛しますので、必要であれば、下記を参考にしてください。

参考
https://developers.wonderpla.net/entry/2020/06/18/110005

上記の変更後、Slackに通知が飛ぶことを確認する必要があります。そのため、下記のようにmyFunction(メイン処理)を選択し、実行ボタンを押下することで実行できます。

画像4

4. トリガー実行でスケジュール登録

最後に、定期実行させるためにスケジュール登録を行います。スクリプトエディタより、[トリガー]->[トリガーを追加]を選択します。

画像5

ポップアップが表示されるため、下記のように指定してください。

画像6

これにより、午前0時~1時の時間にcreateTriggers関数が実行され、myFunctionが定期(下記の時間に)実行されるようトリガー登録されます。

var hours = ["9:30", "10:00", "10:30", "11:00", "11:30", "12:30","12:45", "13:00", "13:30", "14:00", "14:30"];

4. まとめ

GASを使ってWebスクレイピングを行うツールを再作成してみました。始めてGASでコードを作ってみましたが、割と作りやすかったです。サーバレスで自動実行できるので、ものすごく便利ですね。もし、興味がありましたら活用してみてください。

この記事が気に入ったらサポートをしてみませんか?