kta-basket's blog

バスケット好きによるなにか

Bリーグの選手情報を返すLINE Botを作った② ~ GASでスプレッドシートを参照するLINE Botを作る ~

復習

こちらの続きです。


選手情報を書き込んだスプレッドシートができたので、いよいよLINE Botを作ります。

LINE Botを作ろう!サーバーはどこで動かす?

Botを作るにあたり、LINEで作ることは決めていたのですが、サーバーサイドをどうするかは決めていませんでした。

色々ググっているうちに、GASというものを知りました。

GAS

GoogleAppsScript.
日頃から大変お世話になっているGoogle様のサービスです。


無料で使える! Webアプリケーションとして公開できる!


というわけで使ってみました。
(実は今回初めて知りました。先に前回のスクレイピングのプログラムだけ作っていて、あとからGASから参照するのにスプレッドシートが楽だったためスプレッドシートに書き込むようにしました。)

LINEと繋ぐ

とりあえずLINEと接続してデバッグできるようにしよう!
と以下の記事を参考に作り始めました。

qiita.com

なるほど30分でできるのね〜


...


...


2時間経っても返事をしてくれない!


調べまわった結果、わかりました。


これです。

matonton.hatenablog.com


ウェブアプリケーションとして導入」で更新する際に、プロジェクトバージョンを"New"にしなければいけなかったんですね。

最初起動したときにアクセストークンをコピーし間違えていて、修正したのです。
それがいつまでも反映されていなかったんですね。。。
というわけで、LINEとつながり、更新の仕方も把握しました。

function doPost(e) {  
  var events = JSON.parse(e.postData.contents).events;
  events.map(function(event) {
    var replyToken = event.replyToken;
    var userMessage = event.message.text;
    
   // makeReplyMessageで返答メッセージを作るようにする
    var replyMessage = makeReplyMessage(userMessage);
    
    var replyContent = {
      "replyToken":replyToken,
      "messages":[
        {
          "type":"text",
          "text": replyMessage
        }
      ]
    };
    
    UrlFetchApp.fetch(REPLY_URL, makeOptions(replyContent));
  });

  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}

// あとでちょっと使い回すから分けてます
function makeOptions(replyContent){
    var options = {
    'headers': {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer '+ACCESS_TOKEN
    },
    'method': 'POST',
    'payload': JSON.stringify(replyContent)
  }
    return options;
}

送られてきたテキストからチーム名と背番号を抽出する

チーム名抽出。もうここはゴリッゴリにハードコーディング

function detectTeam(message) {
  if(checkInclude(message, ["レバンガ","北海道"])){ return "レバンガ北海道";
  } else if(checkInclude(message, ["秋田","ハピネッツ","ノーザン"])){ return "秋田ノーザンハピネッツ";
  } else if(checkInclude(message, ["栃木","ブレックス","リンク"])){ return "栃木ブレックス";
  } else if(checkInclude(message, ["千葉","ジェッツ","ふなばし"])){ return "千葉ジェッツふなばし";
  } else if(checkInclude(message, ["アースフレンズ","東京Z","アスフレ"])){ return "アースフレンズ東京Z";
  } else if(checkInclude(message, ["アルバルク","東京","トヨタ"])){ return "アルバルク東京";
  } else if(checkInclude(message, ["サンロッカーズ","渋谷"])){ return "サンロッカーズ渋谷";
  } else if(checkInclude(message, ["川崎","ブレイブサンダース","ブレサン"])){ return "川崎ブレイブサンダース";
  } else if(checkInclude(message, ["横浜","ビーコル","コルセアーズ"])){ return "横浜ビー・コルセアーズ";
  } else if(checkInclude(message, ["新潟","アルビ"])){ return "新潟アルビレックス";
  } else if(checkInclude(message, ["富山","グラウ"])){ return "富山グラウジーズ";
  } else if(checkInclude(message, ["三遠","ネオ"])){ return "三遠ネオフェニックス";
  } else if(checkInclude(message, ["シーホース","三河","アイシン"])){ return "シーホース三河";
  } else if(checkInclude(message, ["イーグルス"])){ return "Fイーグルス名古屋";
  } else if(checkInclude(message, ["名古屋","ダイヤモンド","ドルフィンズ"])){ return "名古屋ダイヤモンドドルフィンズ";
  } else if(checkInclude(message, ["滋賀","レイク"])){ return "滋賀レイクスターズ";
  } else if(checkInclude(message, ["京都","ハンナリ"])){ return "京都ハンナリーズ";
  } else if(checkInclude(message, ["大阪","エヴェッサ","エべッサ"])){ return "大阪エヴェッサ";
  } else if(checkInclude(message, ["ライジングゼファー","福岡"])){ return "ライジングゼファー福岡";
  } else if(checkInclude(message, ["琉球","キングス"])){ return "琉球ゴールデンキングス";
  } else if(checkInclude(message, ["青森","ワッツ"])){ return "青森ワッツ";
  } else if(checkInclude(message, ["仙台","ERS","ers"])){ return "仙台89ERS";
  } else if(checkInclude(message, ["山形","ワイヴァンズ"])){ return "山形ワイヴァンズ";
  } else if(checkInclude(message, ["福島","ファイヤーボンズ"])){ return "福島ファイヤーボンズ";
  } else if(checkInclude(message, ["茨城","ロボッツ"])){ return "茨城ロボッツ";
  } else if(checkInclude(message, ["群馬","クレインサンダーズ"])){ return "群馬クレインサンダーズ";
  } else if(checkInclude(message, ["八王子","ビートレインズ"])){ return "八王子ビートレインズ";
  } else if(checkInclude(message, ["金沢","武士"])){ return "金沢武士団";
  } else if(checkInclude(message, ["信州","ブレイブウォリアーズ"])){ return "信州ブレイブウォリアーズ";
  } else if(checkInclude(message, ["西宮","ストークス"])){ return "西宮ストークス";
  } else if(checkInclude(message, ["バンビシャス","奈良"])){ return "バンビシャス奈良";
  } else if(checkInclude(message, ["島根","スサノオマジック"])){ return "島根スサノオマジック";
  } else if(checkInclude(message, ["香川","ファイブアローズ"])){ return "香川ファイブアローズ";
  } else if(checkInclude(message, ["愛媛","オレンジ","バイキングス"])){ return "愛媛オレンジバイキングス";
  } else if(checkInclude(message, ["熊本","ヴォルターズ"])){ return "熊本ヴォルターズ";
  } else { return undefined;
  }
}

function checkInclude(text, matchArary) {
  var flg = false;
  matchArary.some(function(key){
    if(text.match(key)) {
      flg = true;
      return true;
    }
  });
  return flg;
}

B2を追加したら東京と名古屋が2チームとなり、仙台はチーム名に番号入ってる...!!!(後述)とかありましたがB1優先にさせて頂きました。


次に、番号を抽出します。

function detectNum(message) {
  // 全角数字を半角に直す
  message = message.replace(/[0-9]/g, function(s){
    return String.fromCharCode(s.charCodeAt(0) - 65248);
  });
  # 仙台89ERSから89を抽出しないようにする
  if (message.match("89ERS")) message = message.replace("89ERS","");
  if (message.match("89ers")) message = message.replace("89ers","");
  if (message.match("仙台89")) message = message.replace("仙台89","");
  num = message.replace(/[^0-9]/g,"")
  return num;
}

仙台に89番の選手がいなくて良かった。(永久欠番になってたりする?)

スプレッドシートから選手情報を取得する

function getPlayersListByTeam(team) {
  // 参照したいシートのURL(https://docs.google.com/spreadsheets/d/{$SHEET_ID}/edit)からシートIDがわかります
  var spreadsheet = SpreadsheetApp.openById(SHEET_ID);

  // チームごとにシートを分けているのでチーム名が一致するシートを取得します
  // (どちらもシートでややこしいけどopenByIDで開いているspreadsheetがexcelでいうところのブック)
  var sheet = spreadsheet.getSheetByName(team);
    
  var players_list= [];
  var lastRow = sheet.getLastRow();    // 空白でない最終行を返してくれる

  var range_values = sheet.getRange(1,1,lastRow,20).getValues();
  for (var r = 0; r < lastRow; r++) {
    var values = range_values[r];
    # key-valueに整形
    var data = {
      "url": values[1],
      "name": values[2],
      "name_en": values[3],
      "num": values[4],
      "position": values[5],
      "school": values[6],
      "home": values[7],
      "birth": values[8],
      "height": values[9],
      "weight": values[10],
      "contry": values[11],
      "pta": values[12],
      "rba": values[13],
      "asa": values[14],
      "sta": values[15],
      "bla": values[16],
      "fgp": values[17],
      "tpp": values[18],
      "ftp": values[19],
    }
    players_list.push(data);
  }
  return players_list;
}

var range_values = sheet.getRange(1,1,lastRow,20).getValues();では1行分の値が一つの配列になっている配列の配列が返ってきます。
スプレッドシートとのやりとりをなるべく減らすため、1チーム分まるっと取得して選手ごとにkey-valueにまとめた配列を返すようにしました。

リプライメッセージを作る

function makeReplyMessage(message) {
  var team = detectTeam(message);
  if (!team) return "チーム名がわかりませんでした。";
  team_players_data = getPlayersListByTeam(team);
  
  var num = detectNum(message);
  
  var num = detectNum(message);
  if(!num){
    return "番号が分かりませんでした。\nチームと背番号を言ってね。\n"+team+makePlayersListString(team_players_data);
  }
  
  // 背番号一致を探す
  var result = team_players_data.filter(function(player) { return player.num == num;})[0];
  if (result) {
    var format = team+'\n';
    format += '#'+result.num+'\n';
    format += result.name+'/'+result.name_en+'\n';
    format += result.position+'\n';
    format += '出身校:'+result.school+'\n';
    format += '出身地/国籍:'+result.home+'/'+result.contry+'\n';
    format += result.birth+'生\n';
    format += result.height+'/'+result.weight+'\n';
    format += 'FG: '+result.fgp+', 3P: '+result.tpp+', FT: '+result.ftp+'\n';
    format += 'PPG: '+result.pta+', RPG: '+result.rba+', APG: '+result.asa+'\n';
    format += 'SPG: '+result.sta+', BPG: '+result.bla+'\n';
    format += result.url;
    return format;
  } else {
    return "選手情報が見つかりませんでした。\n"+team+makePlayersListString(team_players_data);
  }
}

function makePlayersListString(playersdatalist) {
  var list_string = '';
  playersdatalist.map(function(data){
    list_string += '\n#'+data.num+' '+data.name;
  });
  return list_string;
}

試合をみている時以外に「あの選手どこ出身なのかな〜」と思ったときに番号がわからなかったので、番号がないときは背番号付きのリストを返すようにしました。

できたよ

チーム名+背番号でプロフィールと簡単なスタッツを返します。

おまけ(だけど結構大事だと思う)

LINE Messaging APIGUIから作るアクセストークンって最長で24時間となっていて、毎日更新するのが面倒なので自動でリフレッシュするようにします。
調べたらちゃんとAPIありました。

チャネルアクセストークンを発行する

function refreshToken() {
  var options = {
    'headers':{
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    'method': 'POST',
    'payload': {
      'grant_type': 'client_credentials',
      'client_id': CLIENT_ID,
      'client_secret': CLIENT_SECRET
    }
  }
  var res = UrlFetchApp.fetch(REFRESH_URL, options);
  ACCESS_TOKEN = JSON.parse(res).access_token;
}

というわけでLINEへのリプライが送れなかったらrefreshするようにして、あとは放置。

感想

  • 初めてプライベートで最後まで作った!
  • 実はjavascriptも初めて!(仕事でtypescriptは触ってたよ)
  • GASの編集画面でカーソルが消えるのは仕様ですか?PC(もといブラウザ)との相性が悪い?
  • スクレイパーもGASに移行しようと思ったらUrlFetchAppで取ってきたhtmlが不完全で選手一覧が入ってなくて諦めました。誰か教えてください


友人に紹介するとみんな推し選手を一番最初に調べるのが微笑ましかったです。