[Googleスプレッドシート][GAS]引越し候補物件をスマートに整理しよう Sprint1

# はじめに

続編: 

https://qiita.com/toro-osushi/items/91d2f3e48e2108f565c9

注意書き: 

※ 本ページ記載内容には、Suumoに対するスクレイピング処理が含まれます。商用利用は禁止されておりますので、個人利用の範囲で参考にしていただければと思います。

## 背景

* 最近引越し予定で家探しをすることになったものの、お気に入り物件をざっと眺められる場所が欲しい
* スプレッドシートにまとめていきたい
* 物件探しはSuumoでやっていく予定

## 課題

### 課題①: 必要な情報すべてを手で入力すると、めんどくさい

* URLだけ貼っていても、いちいち覗きにいかなければいけないため、だるい
* かといって、URLに加えて、家賃・築年数・面積... などなどをSuumoから手で転記するのは、もっとだるい

### 課題②: 通勤時間は、毎回Google Mapで検索しないと不明であり、めんどくさい

* 「通勤にどれくらい移動時間がかかるか？」は、家探しで結構重要なパラメータ
* しかし、物件情報として取得できるのは以下であり、家⇔オフィスの合計通勤時間は物件情報から取得不可能
    * 物件から最寄り駅までの移動時間 (**←しかも、ガバガバで信用ならない!!!**)
    * 最寄り駅からオフィス最寄り駅までの電車移動時間
* 重要な情報であるにもかかわらず、毎回Google Mapで検索するのは、めちゃくちゃだるい

## 目的

Suumo URLを入力すると、物件を比較するために必要な家賃・通勤時間などの情報を、自動で取得・計算するスプレッドシートを作ろう！

<br>

# 進め方

**Quick and Dirty!**

ちゃんと考えるのはだるいので、とっとと動くものを作る。課題に対しては適宜アップデートをかけてつぶしていく。

# 実装

## 実装のイメージ

1. 入力する値はSuumoのURLのみ
    * Suumoの機能のお気に入り登録をしたSuumo URLをいれる前提
2. Suumo URLを使って、以下の値をスクレイピングする
    * 物件名
    * 家賃 (管理費・共益費込)
    * 築年数
    * 最寄り駅と最寄り駅からのアクセス
    * 住所
3. ②で取得した住所とオフィス住所を利用し、Google Maps APIで通勤時間を計算する


## 使い方イメージ

A1にSuumoのURLが入っているとしたら、以下をスプレッドシートのセルに書けば、家賃が取得できる、といったイメージ

    =getRent(A1)

通勤時間に関しては、以下の計算式で計算するイメージ

    =getCommuteTime(<物件住所>, <オフィス住所>)

※後ほど、この実装はまずかったことが判明。[改善すべきポイント](#改善すべきポイント)になります。



## 実装

スプレッドシート > 拡張機能 > App Script でエディタを開き、以下を実装。

```javascript:コード.gs
function getCommuteTime(src, dest) {
 const target_date = '2024/YY/MM HH:mm:ss' // 想定する通勤日時で到着したい時刻を入力
 const datetime = new Date(target_date); 
 const arrival = new Date(datetime.getTime());

 let finder = Maps.newDirectionFinder()
 .setOrigin(src)
 .setDestination(dest)
 .setLanguage("ja")
 .setArrive(arrival)
 .setMode(Maps.DirectionFinder.Mode.TRANSIT); // 公共交通機関

 let route = finder.getDirections().routes[0];
 let value = route.legs[0].duration.value/60; //minutes

 return value;
}

function checkError(return_value){
  let exceptional_strings = '<'
  if (return_value.includes(exceptional_strings)){
    return_value = 'WARNING: 掲載終了の可能性、URLを参照してください。'
  }
  return return_value
}

function getSuumoInfo(url, start, end) {
 let response = UrlFetchApp.fetch(url);
 let content = response.getContentText("utf-8");
 let info = Parser.data(content).from(start).to(end).iterate();
 
 return info
}

function getName(url) {
 const start = '<h1 class="section_h1-header-title">';
 const end = '</h1>';

 let info = getSuumoInfo(url, start, end);
 let name_tmp = info.pop().trim();

 if (name_tmp.includes('-')){
  name = name_tmp.slice(0, name_tmp.indexOf("-"))
 }else{
  name = name_tmp
 }
 
 name = checkError(name)
 return name
}

function getAge(url) {
 const start = '<div class="property_data-body">';
 const end = '</div>';

 let info = getSuumoInfo(url, start, end);
 let age = info[8].trim();
 
 return age
}

function getArea(url) {
 const start = '<div class="property_data-body">';
 const end = '</div>';

 let info = getSuumoInfo(url, start, end);
 let area_tmp = info[5].trim();
 let area = Number(area_tmp.slice(0, area_tmp.indexOf("m")));

 return area
}


function getRent(url) {
 const start_rent = '<div class="property_view_main-emphasis">';
 const start_management_fee = '<div class="property_data-body">';
 const end = '</div>';

 let info_rent = getSuumoInfo(url, start_rent, end);
 let rent_str = info_rent[0].trim().replace('万円', '');
 let rent = Number(rent_str) * 10000;

 let info_management_fee = getSuumoInfo(url, start_management_fee, end);
 let management_fee_str = info_management_fee[0].trim().replace('円', '');
 if (management_fee_str=="-"){ // 管理費・共益費が0の場合
  management_fee_str="0"
 };
 let management_fee = Number(management_fee_str);

 let total_fee = (rent + management_fee)/10000;

 return total_fee
}

function getAddress(url) {
 const start = '<div class="property_view_detail-text">';
 const end = '</div>';

 let info = getSuumoInfo(url, start, end);
 let address = info.pop().trim();

 address = checkError(address)
 return address
}

function getAccessInfo(url) {
 const start = '<div class="property_view_detail-text">';
 const end = '</div>';

 let info = getSuumoInfo(url, start, end);
 let access = info.shift().trim();

 access = checkError(access)
 return access
}
```

# 結果

## よかったポイント

* 「これが欲しかったんや！」という良いPoCになった
* URLを貼るだけでズバズバ情報が入力されるのは、やっぱり気持ちいい！快適！！！
* 通勤時間は結構重要な情報だった、物件一覧と通勤時間を横並びに見れるのは大変便利。

## 改善すべきポイント

1. **セル上で実施する計算式として実装しているため、スプレッドシートのリロードや、行の挿入・削除等で毎回計算が走ってしまう (優先度: Urgent)**
1. **Suumoでお気に入りしていない状態のURLを入力すると、エラーになる (優先度: Medium)**

課題については、しっかり記載していきます。

### ①毎回計算が走る問題 (Urgent)

セルに直書きしている実装となっているため、以下の状況で毎回計算が走ります。

* シートを開く、リロードする場合
* 行の挿入や削除を行った場合

計算が走ることで、以下の問題が発生します。結構クリティカル。

* Google Maps APIの「短時間にAPI叩きすぎじゃボケ」エラー
* 毎回計算が走るので、処理が遅い

-> 以下によって解決可能そう

* スプレッドシート上でトリガーを設定し、トリガーによって計算を実行する
* URL入力断面の情報をスプレッドシート上の別シートに保持・蓄積し、比較するシートは参照のみを行う

次の記事で対応予定

### ②お気に入りじゃないURL、エラーになる問題 (Medium)

Suumoの物件ページは、以下のようにお気に入り系のページと非お気に入りのページでURLが異なるそうです。

* お気に入り: `https://suumo.jp/chintai/bc_xxxxxxxxx/`  
* 非お気に入り: `https://suumo.jp/chintai/jnc_xxxxxxxxxx/`

※細かく言えば他にもあるけど、上記2つで大別できる、程度の認識

URLが異なる上、ページ構造も完全に一致するわけではないので、単一のスクレイピングロジックだとエラーになってしまう、という状態。

想定している通常の使い方だと問題ないのですが、「お気に入り登録しないと、物件URLを入力できない」はダサいので、そのうち対応しようかなと。

# 終わりに

* JavaScript系の言語触るの初めてなので、手こずる部分もあったけど、ドキュメントとか見れば別に普通に書けた。うれしい。
* 動くものを作って、「すげえw」って一緒に喜べる体験自体がとても楽しかった。
* せっかくなので、もう少し便利な仕様にアップデートしたい。

# 参考情報

通勤時間計算周り

https://developers.google.com/apps-script/reference/maps/direction-finder?hl=ja

https://note.com/sozolab/n/n0ad343392a56

スクレイピング周り

https://ex-ture.com/blog/2023/01/24/gas%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%A6web%E3%82%B9%E3%82%AF%E3%83%AC%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0%E3%82%92%E3%82%84%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%88%E3%81%86/