【GAS】週間天気予報をスプレッドシートに反映（画像付き）【スクレイピング】

## 概要
職場のシフトやスケジュールなどの予定表を、スプレッドシートで管理しているケースは多いと思う。かく言う私もその一人。
予定表に天気予報が反映されれば良いのになと、試してみたのがこの記事。

## スクレイピング
### 天気予報のソース
天気予報のAPIは数多くあるけど、登録が必要だったり、必要以上に細かい情報だったりと面倒。そんな大仰なものを作りたいのではなく、晴れ曇り雨くらいが分かれば十分なので、Yahoo!の天気予報をスクレイピングすることに。

今回はコチラのページを参照する（東京の天気）。
https://weather.yahoo.co.jp/weather/jp/13/4410.html#week
13/4410の部分が地域によって異なる。

### ライブラリを使用
上記ページのソースから必要な情報をスクレイピングするのに、TextPickerと言うライブラリを使用した。

TextPickerのID
`16wu9-4GnWwx_DXrldA6Zn2MwRFS3TMu4G6dFwl3t2Ngmn5d9z8R1QZHp`
ライブラリの追加方法はググると丁寧なサイトがいっぱいあるので割愛。

`TextPicker.open(text)` 文字列textを格納
`TextPicker.skipTo('xxx')` 文字列内のxxxまで飛ぶ
`TextPicker.pickUp('yyy','zzz')` 文字列内のyyyとzzzの間の文字列を取得

他にもメソッドはあるけど、今回使うのはこれだけ。

## 基本のコード
Yahoo!天気予報をスプレッドシートに書き出すコードがコチラ。
```javascript
function weatherReport() {

  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var data = UrlFetchApp.fetch('https://weather.yahoo.co.jp/weather/jp/13/4410.html#week').getContentText();

  var skipWord = ['今日・明日の予報','<ul class="temp">','<small>天気</small>',,,,,,].fill('</small>',3);
  for(var arr=[]; arr.length<2;) arr.push(Array(8));

  TextPicker.open(data);

  for(var i=0; i<8; i++){
    TextPicker.skipTo(skipWord[i]);
    var imgUrl = TextPicker.pickUp('<img src="','" border');
    arr[0][i] = SpreadsheetApp.newCellImage().setSourceUrl(imgUrl).build();  
    arr[1][i] = TextPicker.pickUp('alt="','">');
  } 
  sheet.getRange(1,1,2,8).setValues(arr); //A1セルからH2セルにかけて書き出す
}
```
実行するとスプレッドシートはこんな感じに。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/708687/2ba471e4-da86-9ddd-2343-edbec7a5387e.png)

TextPickerの追加が前提だけど、コピペで同じ結果が得られる。2022年4月現在
※Yahoo!天気のレイアウトが変われば、同じコードでスクレイピングできなくなるので動作しなくなるはず。

コードの解説は後で。

## 検討
あとは、この２行８列の天気予報を、予定表のどの位置に書き出すか変えるだけ。

私の使っている予定表は横軸が日付なので、当日の日付が入ったセルを探し、その列の天気予報用の行に上記の結果を書き出すスクリプトを作り、トリガーで毎日実行している。

月替わりのタイミングで入力箇所が無くなってしまうので、翌月分の天気予報は削る工夫をしている。

この辺の加工は使っている予定表の形で変わるだろうし、記事の本質とも離れてしまうので割愛。

## コードの解説
以下に、スクリプトの流れが分かるようにコードを記載し、コメントで解説する。得られる結果は前述のコードと同じ。
```javascript
function weatherReport() {

  //シートを指定
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();

  //Yahoo!天気の東京の天気予報ページソースを取得してテキスト化
  var url = 'https://weather.yahoo.co.jp/weather/jp/13/4410.html#week';
  var data = UrlFetchApp.fetch(url).getContentText();

  //天気のアイコンと予報を格納する配列を宣言
  var icon  = [];
  var tenki = [];

  //TextPickerにテキスト化したソースを格納
  TextPicker.open(data);

  //今日の天気の記述手前までスキップし アイコンのURL,予報を取得
  TextPicker.skipTo('今日・明日の予報');
  icon[0] = TextPicker.pickUp('<img src="','" border');
  tenki[0] = TextPicker.pickUp('alt="','">');

  //明日の天気の記述手前までスキップし 以下同文
  TextPicker.skipTo('<ul class="temp">');
  icon[1] = TextPicker.pickUp('<img src="','" border');
  tenki[1] = TextPicker.pickUp('alt="','">');

  //明後日の天気の記述手前までスキップし 以下同文
  TextPicker.skipTo('<small>天気</small>');
  icon[2] = TextPicker.pickUp('<img src="','" border');
  tenki[2] = TextPicker.pickUp('alt="','">');

  //３日後の天気の 以下同文
  TextPicker.skipTo('</small>');
  icon[3] = TextPicker.pickUp('<img src="','" border');
  tenki[3] = TextPicker.pickUp('alt="','">');

  //４日後の天気の 以下同文
  TextPicker.skipTo('</small>');
  icon[4] = TextPicker.pickUp('<img src="','" border');
  tenki[4] = TextPicker.pickUp('alt="','">');

  //５日後の天気の 以下同文
  TextPicker.skipTo('</small>');
  icon[5] = TextPicker.pickUp('<img src="','" border');
  tenki[5] = TextPicker.pickUp('alt="','">');

  //６日後の 以下同文
  TextPicker.skipTo('</small>');
  icon[6] = TextPicker.pickUp('<img src="','" border');
  tenki[6] = TextPicker.pickUp('alt="','">');

  //７日後の 以下同文
  TextPicker.skipTo('</small>');
  icon[7] = TextPicker.pickUp('<img src="','" border');
  tenki[7] = TextPicker.pickUp('alt="','">');

  //８日分繰り返し
  for(var i=0; i<=7; i++){

    //URLからセル埋め込み画像を取得するメソッド※
    var image = SpreadsheetApp.newCellImage().setSourceUrl(icon[i]).build();

    //シートに画像を書き出す
    sheet.getRange(1,i+1).setValue(image);

    //シートに天気を書き出す
    sheet.getRange(2,i+1).setValue(tenki[i]);
  }
}
```
スクレイピングしている部分について、実際のページへ行きソースを表示させると分かりやすいかも？

### ※URLからセル埋め込み画像を取得するメソッド
あまり定番ではないけど地味に便利なメソッド。
`SpreadsheetApp.newCellImage().setSourceUrl(URL).build();`で取得した画像を`setValue`でシートに書き出すと、指定したセルに画像が埋め込まれる。

`insertImage`だと、指定したセルを先端にして画像が貼り付けられるので、画像サイズによってはセルをはみ出てしまう（と言うかほぼはみ出る）。

### コードの変形
８日分の天気予報を取得している部分について、必要ヵ所手前までスキップするためのキーワードが違うだけで、アイコンのURLと予報を取得する部分は共通している。
なのでスキップ用のキーワードを、for文の繰り返しに合わせて配列`skipWord[]`に格納すれば、繰り返しの中で順番に取得できる。
しかも４つめ以降はキーワードも同じだから、`fill()`でおまとめできる。

アイコンと予報は、スプレッドシートに２行８列で書き出されるので、最初から２行８列の２次元配列に格納しておけば`setValues`一発で済む。

と言う事で変形したのが、最初に紹介したコード。
以上を踏まえて、解説コメント付きで最初のコードを再度記載する。
```javascript
function weatherReport2() {

  //シートの指定
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();

  //ソースを取得しテキスト化
  var data = UrlFetchApp.fetch('https://weather.yahoo.co.jp/weather/jp/13/4410.html#week').getContentText();

  //スキップキーワード設定（４つめ以降は同じなので fill を使い省略）
  var skipWord = ['今日・明日の予報','<ul class="temp">','<small>天気</small>',,,,,,].fill('</small>',3);

  //２行８列の配列を宣言※
  for(var arr=[]; arr.length<2;) arr.push(Array(8));

  //TextPickerにテキスト化したソースを格納
  TextPicker.open(data);

  //８日分繰り返し
  for(var i=0; i<8; i++){

    //スキップキーワードまでスキップ
    TextPicker.skipTo(skipWord[i]);

    //アイコンのURL取得
    var imgUrl = TextPicker.pickUp('<img src="','" border');

    //URLからセル埋め込み画像を取得し配列へ格納
    arr[0][i] = SpreadsheetApp.newCellImage().setSourceUrl(imgUrl).build();  

    //予報を取得し配列へ格納
    arr[1][i] = TextPicker.pickUp('alt="','">');
  }
  //配列をスプレッドシートへ書き出し
  sheet.getRange(1,1,2,8).setValues(arr);
}
```
となったとさ。

※二次元配列を変なワンライナーで宣言してるけど、それについては別記事あり。
[【JavaScript】多次元配列を作成するシンプルな方法【ワンライナー】](https://qiita.com/YasumiYasumi/items/2a9d469f1c829df7945e)

スペース込み1500文字以上あったコードも、665文字まで減りスッキリ^^

### おしまい