個人的に読んでるWeb漫画の更新通知botをGAS+TwitterAPIで作りました


[完成したbotはこちら](https://twitter.com/webbot17)




## モチベーション

僕はめちゃくちゃ異世界転生漫画を読んでます。多分100本くらい読んでるシリーズがあると思います。



見てるやつは片っ端からブックマークに入れて、毎月25日くらいに全部読んでいくのですが、流石に100本をまとめて読むのは辛い。（まあ逆に良い感じにしょうもないやつ切っていけるから良いという説もあるが…）



できれば毎晩寝る前に数本読むのが一番最高。



ということで更新されたときに通知が来るようにしたい。

もちろんそれぞれの漫画サイト毎で通知は何かしらあると思うが、いろんなサービスをまとめて、取捨選択してとなると自分で作るしかない。



そしてできればサーバーを立てたくない。

<br>

## 方法



通知はGmailでもいいが、Twitter APIを用いてbotでツイートして、その通知はTwitter公式クライアントで拾えば公開されてなんかまあ良い感じなのでそうします。


Twitter APIの使い方は調べれば無限に見つかるので割愛。



更新情報の取得やAPI処理をサーバーを立ててそこでPythonプログラムを自動実行させるとかだったら言語の学習はほとんど必要ないのですが(Pythonはある程度使えるので)、なんせサーバーを立てたくないのでGASでやることにしました。

今回やるような処理をGASでやってるQiitaの記事を2年前くらいにみたことあるような気がするので、GASならできそうだなと思ってやっていきます。


### GAS(Google Apps Script)

> Google Apps Script(略称GAS/ガス)とは、Googleによって開発されたスクリプトプラットフォームである軽量のアプリケーション開発のためのGoogleのワークスペースプラットフォーム。 主にGoogleのサービスを自動化するスクリプト言語である。JavaScriptがもとになっているため汎用性が高く、開発環境はGoogle Chromeだけでいいのでプログラミング初心者が始めやすい言語の1つである。
>
> ​	引用 [wikipedia](https://ja.wikipedia.org/wiki/Google_Apps_Script)

例えばGoogleスプレッドシートを作って、そのシートに対して何か操作をしたりするスクリプトを簡単に作れて、当然紐付けも簡単に行え、自動定期実行も無料でできる。



もちろん無料枠だと[制限がある](https://monoblog.jp/archives/2349)ので、その範囲でできるように作っていく。

とはいえそこは流石天下のGoogle、引っかかる可能性のある制限は6分制限くらいでした。



<br>



<img src="https://drive.google.com/uc?export=view&id=1PbUGdGKIZ7Y_Sw8-KRP2PCdKORmAI5nc" width="" alt="fig1">

 

なるべく簡単に作りたかったので、上図のように読んでる漫画とURLのリストをサイト別に用意して、

これを上から1行ずつURLからアクセスし、最新話の更新日をスクレイピングで取得、その日が前日であればツイートする。[^1]



[^1]: 実行時間が17時前後なので、当日をキーにすると18時以降に更新された場合に拾い漏らす。



<br>



当然サイトによって更新日の位置が違うので、それぞれに違う処理を書く必要がある。



上図の下部のシート名を見ると分かるように、サイト毎にシートを分けて同じようにリスト化しておく。



また、前述の6分制限もあるので、スクリプト自体を分けて6分を超えないようにしつつ、どれがどのサイトのスクリプトかも分かりやすくする。



<br>

## スクリプト本体



```js
// @ts-nocheck
// 認証用URL取得
function getOAuthURL() {
  Logger.log(getService().authorize());
}
 
// サービス取得
function getService() {
  return OAuth1.createService('Twitter')
      .setAccessTokenUrl('https://api.twitter.com/oauth/access_token')
      .setRequestTokenUrl('https://api.twitter.com/oauth/request_token')
      .setAuthorizationUrl('https://api.twitter.com/oauth/authorize')
      // 設定した認証情報をセット
      .setConsumerKey(PropertiesService.getScriptProperties().getProperty("CONSUMER_API_KEY"))
      .setConsumerSecret(PropertiesService.getScriptProperties().getProperty("CONSUMER_API_SECRET"))
      .setCallbackFunction('authCallback')
      // 認証情報をプロパティストアにセット（これにより認証解除するまで再認証が不要になる）
      .setPropertyStore(PropertiesService.getUserProperties());
}
 
//  認証成功時に呼び出される処理を定義
function authCallback(request) {
  var service = getService();
  var authorized = service.handleCallback(request);
  if (authorized) {
    return HtmlService.createHtmlOutput('success!!');
  } else {
    return HtmlService.createHtmlOutput('failed');
  }    
}

// ツイート用のAPIを起動する関数
function toTweet(text) {
  var twitterService = getService();
  
  if (twitterService.hasAccess()) {
    // 投稿
    var twMethod = { method:"POST" };
    twMethod.payload = { status: text };
    var response = twitterService.fetch("https://api.twitter.com/1.1/statuses/update.json", twMethod);
    
    Logger.log(response.getContentText());
 
  } else {
    Logger.log(service.getLastError());
  }
}


function notify_update_cw() {
  var spreadsheet = SpreadsheetApp.openById('スプレッドシートのIDを入れる');
  // var spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); //これだとスプレッドシートを閉じてるときに使えない
  var sheet = spreadsheet.getSheetByName('comic_walker');
  var lastrow = sheet.getLastRow();
  // スプレッドシートの全行取得
  const range = sheet.getRange('A1:B' + lastrow);
  //昨日の日付を取得
  var now = new Date();
  var yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
  var year_str = yesterday.getFullYear();
  //月は0~11なので+1する
  var month_str = 1 + yesterday.getMonth();
  var day_str = yesterday.getDate();
  // comic_walkerは0埋めが必要
  const yesterday_str = year_str + "/" + ('0' + month_str).slice(-2) + "/" + ('0' + day_str).slice(-2)

  for(var i = 1; i <= lastrow - 1; i++){
    var l = range.getValues()[i];
    var getUrl = l[1];
    var html = UrlFetchApp.fetch(getUrl).getContentText('UTF-8');
    var update_date = Parser.data(html).from('<span class="comicIndex-date">').to('</span>').iterate()[0].split(" ")[0];
    if (update_date == yesterday_str) {
      toTweet("昨日 " + update_date + " に更新されました。\n" + l[0] + " " + l[1]);
    }
    Utilities.sleep(10000);
  }
}

```

こちらはComic Walker版のスクリプト、他のサイト用のスクリプトもシートの指定と日付の形式とスクレイピングの要素を指定する部分以外は共通。



` notify_update_cw`より前は[参考](#参考)の通りにTwitterAPI周りの処理を記述。



` Utilities.sleep(10000);`は10秒間何もせずに止める操作で、6分制限のことを考えると一番きつい処理だが、[Comic Walkerのrobot.txt](https://comic-walker.com/robots.txt)を見ると（見方が合ってるか分からないが）、クローラは10秒間Delayしろって書いているので、それに従っています。
これでもギリギリ6分には届かないくらいの漫画数ですが、今後増えてきたらシートを分けてスクリプトを分ければ大丈夫ですね。

<br>



とりあえず数の多いComic Walkerと更新日が分かりやすいヤングエースUP、ガンガンONLINEを実装したが、コミックガルドは更新日には無料公開せず、公開約1ヶ月後の無料公開予定日を拾う必要がある。



しかし、無料公開予定日は無料公開されると消えてしまうので、前日に「明日無料公開される予定です」といった感じに通知する必要がある。（実装しても当分はテスト版という扱いをすると思う）

また、動的に過去話を表示しているようなので、どうやって拾ってくれば良いのか不明。


<br>

なんにせよ
大量にあった漫画のブックマークが半分くらい減ったので、月末の負担が半分に減り、毎晩更新があれば寝る前にちょちょいっと読めるようになったので、快適な漫画ライフには一歩近づきました。



## 参考

- [Google Apps Script(GAS)を使ったwebスクレイピング](https://qiita.com/takaito0423/items/259097b55b026800c875)
  - ParserのIDが変わった？ので別で調べてやりました。
- [GASでTwitterAPIを叩いてみる](https://tech-cci.io/archives/4228)
  - まんまこの通りやりました。



<br>