dkrqr’s blog

僕がつくったものとやったこと考えたことの記録

熊野寮食メニューbotを改良した

熊野寮食メニューbotを改良したことについて。

一昨年の夏に作ってから1年半経って,いろいろひどいなぁと思ったのでほとんど作り直した。 この記事では改良前をv1.0,改良後をv2.0とします。 なんとなくかっこいいので。

熊野寮食メニューbotとは

熊野寮食メニューbotとは,寮食のメニューを https://menus.kumano-ryo.com/ からスクレイピングしてこんな感じでつぶやくbot

作ったときの話を一昨年のkmnacに書いたので気になる人は読んで。

dkrqr.hatenablog.com

改良ポイント

改良したポイントは次の4つ

  • 関数間で受け渡されるobjectの形を統一
  • スプレッドシートにメニュー記録
  • フォームからのメニュー入力に対応
  • キーワードを含むtweetにいいね
  • 月曜日にその週のメニューダイジェストをtweet

前に書いていたコードの関数の引数,返り値の設定が良くなかったので,ちょっと機能を変えたりコードをほとんどそうとっかえすることになってしまった。

引数,返り値の設定が良くなかったというのは,そのコードのその位置でしか使えないような設定をしていたということ。 例えばv1.0の新メニュー判定の関数では,カンマ区切りの文字メニューを入力して,新メニューの直前に🈟をつけたカンマ区切りの文字列を返すような処理をしていた。

v1.0の新メニュー判定関数

//新メニューか判定し,新メニューなら記録し🈟をつける
function isNewMenu(menu){
  //メニュー記録用ドキュメント
  var docId = PropertiesService.getScriptProperties().getProperty('docId');
  var doc = DocumentApp.openById(docId);
  var body = doc.getBody();

  //正規表現で処理するために','を2個に(<pre>から変換した,は1個のまま)
  //,めにゅー,,メニュー,,料理,
  menu = menu.replace(/,/g,',,').substr(1);
  Logger.log(menu);

  //,hoge,を抽出
  var dish = menu.match(/,.+?,/g);
  if(dish==null)  return menu;

  for(var i=0;dish[i];i++){
    dish[i]= dish[i].substr(1);
    Logger.log(dish[i]);
    if(!body.findText(dish[i])){
      body.setText(body.getText() + dish[i]);
      menu = menu.replace(dish[i], '🈟' + dish[i]);  //新メニュー機能を外すときはこの行をコメントアウト
    }
  }
  //連続する','を1個に
  menu = menu.replace(/,+/g,',');
  Logger.log(menu);
  return menu;
}

カンマをつけたり増やしたりよくわからん処理をしているのは,正規表現の先読み後読みができないから。

これのままではv2.0で使いにくかったので,メニュー名単体と検索すべきスプレッドシートを入力して新メニューかどうかを1,0で返すようにした。 すっきりしていいかんじ。

v2.0の新メニュー判定関数

/**
 * SS全体を検索して新メニューか判定
 *  
 * @param {string} menu
 * @param {SpreadSheetApp} spreadsheet
 * @return {Integer} true:1,false:0
 */
function isNewMenuSS(menu,spreadsheet){
  var textFinder = spreadsheet.createTextFinder(menu).matchEntireCell(true);
  if(textFinder.findNext() == null){
    return 1;
  }
  else{
    return 0;
  }
}

2021/02/10追記 下のコードに変えました

/**
 * SS全体を検索して新メニューか判定
 * 
 * @param {Number} unixtime
 * @param {String} menu
 * @param {SpreadSheetApp} spreadsheet
 * @return {Boolean} 
 */
function isNewMenuSS(unixtime,menu,spreadsheet){
  var date = new Date(unixtime);
  var year = date.getFullYear();
  var thisYear = new Date(year.toString() + '/01/01'); //メニューの年のJan 01 00:00:00 GMT+09:00
  var elapsed = date.getTime() - thisYear.getTime(); //経過時間msec
  var dataRow = elapsed/1000/60/60/24 + 2; //除算で日数-1がでて,スプレッドシート2行目が1/1なので+2

  var textFinder = spreadsheet.createTextFinder(menu).matchEntireCell(true);
  var foundRanges = textFinder.findAll();
  for(var i=0; i<foundRanges.length; i++){
    var sheetName = parseInt(foundRanges[i].getSheet().getName());
    if(sheetName < year || 
      (sheetName == year && foundRanges[i].getRow() < dataRow))   {
      return false;
    }
  }
  return true;
}

追記以上

どの処理を関数として切り出すべきなのかとか,入力と出力を何にすべきなのかとかの設計の基準みたいなのがきっとどこかにあるはずなので学びたいなぁと思う。 おすすめの何かありますか?

各改良ポイントの詳細

関数間で受け渡されるobjectの形を統一

上にも書いたが,関数の引数と返り値の汎用性?みたいなものが保たれるように,関数の入出力をなるべく同じような形で,各メニューを1つずつ分けたまま扱うことを意識した。 メニューを記録する部分では,関数間のデータの受け渡しは {date:Number,lunch1:[String],lunch2:[String],dinner:[String]} の形のobjectで行うようにした。 記録したメニューをつぶやく部分でもこの形で処理したいので, {date:Number,lunch1:[String],lunch1New:[Boolean],lunch2:[String],lunch2New:[Boolean],dinner:[String],dinnerNew:[Boolean]} の形で受け渡しを行っている。 新しい機能を追加したくなったときにも引数と返り値をこれにすればいいので追加しやすくなったはず。

新メニューであるかの情報を,lunch1に対してlunch1Newで対応する番号に持たせているのはちょっと微妙な気がするので改良の余地がありそう。 教えられたい。

スプレッドシートにメニュー記録

今回の改良のメイン。 そもそも新メニューを判定するための記録をドキュメントでやってたのアホ過ぎない?アホ過ぎ。 ということに(かなり前から)気づいて(いたけど放置していたのを)思い切って改善した。 これからのメニューを全部スプレッドシートに記録するし,今までのメニューもtwitterから発掘して記録した。

このgoogleスプレッドシート( 寮食記録 - Google スプレッドシート )に年ごとのシートを作成し,1行に1日のメニューを記録している。 1セルに1メニューを入れ,そのメニューが新メニュー*1ならば右のセルに🈟を入力。

これのおかげで寮食のメニューを記録したスプレッドシートができた。 いつでも寮食を懐かしむことができるね。 日付と紐づけて記録しているので新メニュー判定を適当な時期で切ることができる。

googleフォームからのメニュー入力に対応

http://menus.kumano-ryo.comの入力は厨房員さんがやっているらしく,更新されないことが時々ある。 上のスプレッドシートにも長期休暇じゃないのに抜けている週がいくつかある。 そこで,更新されない場合には手動で入力できるようにした。 スプレッドシートに記録して読み込む形式にしたことで,更新されないときに手入力できるようになった。 今までは毎日スクレイピングしていたのでフォーム入力したものを記録しておくことができなかった。(やろうと思えばできたけど面倒だった)

2方面からの入力があるが,新メニュー判定方法がスプレッドシート全体でメニューを検索して一致するものがあるかという雑な方法なのでどちらかから入力があった時点でスクレイピングをやめないといけない。 そうしないと,2回目の入力では新メニュー判定をしてもらえなくなる。 なので,

  1. 日曜0時から10分毎にスクレイピングとフォームの新規入力のチェックを実行
  2. 更新された内容があればスプレッドシートに記録し,10分毎のチェックをやめる

としている。 今気づいたけどこのフローだと過去のメニューを入力したり,フォームで日付の入力を間違えたりするとその週のメニューが入力できなくなるね。 メニュー判定をきちんとしないと。

2020/02/10 追記

ということで,新メニュー判定の関数を改善した。 入力されたメニュー名に対して,記録しようとしている日付より前の日付欄に記入があるかどうかを判定することにした。 これで同じ日付に何回入力しても新メニュー判定が正しく行われるようになる。 また, https://developers.google.com/apps-script/reference/script/trigger-builder#forForm(String) なる機能があることに気づいてフォーム入力をトリガーにできることが分かって実装した。 2点を改善したので,

  1. 日曜0時から10分毎にスクレイピングを実行
  2. 更新された内容があればスプレッドシートに記録し,10分毎のチェックをやめる
  3. googleフォームからの入力は常に受け付ける

という流れに変え,上記の問題は解決した。

キーワードを含むtweetにいいね

これは「寮食」とか「残置」とかでfollowerのtweetを検索してふぁぼするだけ。 follow backするのはどうなのかなぁと思って,followerを全員ぶち込むlistを作ってそのlist内のtweetを見るようにした。 鍵垢は追えないけど,botにfollowされるの嫌な人が一定数いそうなのでまあいいかな。

月曜日にその週のメニューダイジェストをtweet

v1.0ではその日の分だけをスクレイピングしていたがv2.0では1週間分を一気に取るので,全部tweetできるやんとなって実装した。

おわり

新機能を実装した寮食メニューbotは4月から実際に動き始めるのでお楽しみに! こんな機能あったらいいなとかあったら,言ってくれたら実装するので言ってください。

*1:寮食メニューbotを動かし始めた2019年9月以降初めて提供されるメニュー