Gasを用いたスクレイピングのやり方

### 今回のお題
今回のお題はGasを用いたWebスクレイピングです。
スクレイピングといえばPythonという方も多いと思いますが、環境構築などの手間を考慮してまずは一度Gasでやってみたいと思います。

### 目次

- Gasとは何か
- 今回の実装内容
- スプレッドシート・プロジェクトの作成
- ライブラリの導入
- コードの編集
- 補足：うまくいかない場合に

### Gasとは何か
まず最初にGasという言語について軽く説明をします。
GasとはGoogle Apps Scriptの略で、その名の通りGoogle社が開発したプログラミング言語です。
同社のサービスであるGoogle SpreadSheetなどとの親和性が高い、文法がJavaScript準拠である、環境構築が不要、などの特徴があります。

今回はGas自体についてはあまり詳しく説明するつもりはありませんのでご了承ください。
### 今回の実装内容
例にとるものはなんでも良かったのですが、今回は試しに「auの公式サイトのスクレイピング」を行ってみたいと思います。

具体的には、サイトに載っているiPhoneの機種名をスプレッドシートに転記していきたいと思います。
### スプレッドシート・プロジェクトの作成
まずは、下準備です。
スクレイピングした内容を転記するためのスプレッドシートと、Gasのコードを記述するためのスクリプトエディタを作成してください。

スクリプトエディタに関しては、コンテナバインドでもスタンドアローンでもどちらでも大丈夫です。

### ライブラリの導入
スクリプトエディタが作成できたら、まずはライブラリを導入します。
導入するライブラリは、"Parse"と呼ばれるものになります。

以下のスクリプトIDを用いて検索をかけ、ライブラリを導入してください。

```JavaScript:ParseのスクリプトID
1Mc8BthYthXx6CoIz90-JiSzSafVnT6U3t0z_W3hLTAX5ek4w0G_EIrNw
```
これで、ライブラリの導入が完了しました。
### コードの編集
次に、コードを編集していきます。

最初に記述されているコードを全て削除し、以下のコードを貼り付けてください。

```javascript
// ①対象のURLから機種のリストを抽出する関数（au/iphone）
function getModelListAuIPhone() {
  //スクレイピングしたいWebページのURLを変数で定義する
  let url = "abc";
  //URLに対しフェッチを行ってHTMLデータを取得する
  let html = UrlFetchApp.fetch(url).getContentText("UTF-8");
  //Parserライブラリを使用して条件を満たしたHTML要素を抽出する
  let modelList = Parser.data(html)
  .from('  txtColor-base txtW-bold">')
  .to("</h3>")
  .iterate();
  console.log(modelList);
  return modelList;
}

// ②結果をスプレッドシートに反映する関数（au/iPhone）
function printModelListAuIPhone(){
  let modelList = getModelListAuIPhone();
  let url = "xxx"; //スクレイピング結果を出力するスプレッドシートのurl
  let spreadsheet = SpreadsheetApp.openByUrl(url)
  let name = "yyy";// 出力先のシート名
  let sheet = spreadsheet.getSheetByName(name);
  // 取得した要素を一つづつ出力
  for(let i = 2; i <= (modelList.length + 1); i++){
    let range = sheet.getRange(i, 1);
    // <span>などがネストされていた場合は取り除く 
    var model = modelList[(i - 2)].replace('<span class="txtSize-smaller">',"");
    model = model.replace("</span>", "");
    model = model.replace('<span class="txtSize-smaller">',"");
    model = model.replace("</span>", "");
    console.log(model);
    range.setValue(model);
  }
}
```
関数が2つ用意されていますね。
①は指定したサイトから目的の情報をスクレイピングするための関数で、取得した情報がmodelListという変数に代入されています。

②は①の結果を元にスプレッドシート出力を行う関数で、実際に呼び出すのはこちらになります。

いくつか注意点があるので、順番に解説をしていきます。

#### HTML情報の取得について

```javascript
let html = UrlFetchApp.fetch(url).getContentText("UTF-8");
```
HTML情報の取得は、上記のコードで行っています。
fetchメソッドの引数がスクレイピング対象のurlで、getContentTextメソッドの引数がそのパージの文字コードですね。
もちろんこの辺りはページごとに変更が必要になります。
#### 条件を満たしたHTML要素の抽出
スクレイピングとは、条件にあった文字列を出力する行為です。

例えば、誰かに資料をまとめてもらう場合でも、「そのプリントに載ってる地名だけ全部リストアップしといて」などと条件を指定しますよね。

しかし、機械相手ではその文字列が地名かどうかなどの気の利いた判断は望めません。
そこで、「ページの中でこの文字列やこのHTML要素で囲まれた部分の文字列をリストアップして」などと条件設定することが一般的です。

以下の例を見てください。

```javascript
//Parserライブラリを使用して条件を満たしたHTML要素を抽出する
  let modelList = Parser.data(html)
  .from('  txtColor-base txtW-bold">')
  .to("</h3>")
  .iterate();
```
元々のHTML要素の中から、'  txtColor-base txtW-bold">'と"</h3>"にあるもののみを取得しています。
iterate()メソッドは、取得したものを配列にするためのものです。

参考までに、元々のHTMLコードも載せておきます。

```HTML
<h3 class="heading-ttl margin-t-10  margin-r-0  margin-b27  margin-l-0   text-center  txtColor-base txtW-bold">iPhoneXX</h3>
```
欲しかったのはiPhoneXXの部分なので、それに合わせてfrom()とto()の引数も決定しています。

②の部分に関しては、スクレイピングというよりはGas自体の説明に近くなるので今回は割愛します。

### 補足：うまくいかないときに
ここからは補足です。
スクレイピングの処理自体は上記のコードでできるようになっているので、うまくいっている方には不要な内容かと思います。
必要に応じてお読みください。

エラーが出る・目的の情報が得られないなどといった場合、多くの場合はコードのミスが原因です（タイプミス、変数の誤りなど）。

しかし、まれにGasでのスクレイピングならではの注意点もあるので、ここではそれについてまとめます。

#### HTMLクラスの半角スペースについて
個人的には１番の落とし穴ですね。
先程お見せしたHTMLのソースコードを再度記載します。

```HTML
<h3 class="heading-ttl margin-t-10  margin-r-0  margin-b27  margin-l-0   text-center  txtColor-base txtW-bold">iPhoneXX</h3>
```
一つ目のheading-ttlの後は半角スペース一つですが、よくよく見ると二つ目のmargin-t-10の後は半角スペース2つになっています。

当然、スペースの数が違えば異なる文字列として認識されるので、from()あるいはto()でスクレイピングする範囲を指定する際にここを誤ると目的のものが取得できなくなります。

クラス部分に関しては検証ツールを用いてのコピペも可能なので、手打ちよりはそちらの方がミスが減るかなと思います。
#### from()/to()の引数について

理由はわからないのですが、from()やto()の引数を長くしすぎると全く条件に合っていない部分(bodyではなくheadの中身など）が抽出されるようになります。

上記の例でも、最初はh3タグの中身全てをfrom()の引数に当てていたのですが、それだとうまくいかなかったので、後半部分のみに変更しました。

もちろん、うまくいっているのであれば、なるべく長くした方が関係のない部分が紛れ込む可能性は低くなります。
#### replaceメソッドについて
以下のコードにご注目ください。
抽出対象の文字列の中にネストしているspanタグを取り除いているのですが、一見すると冗長とも言える書き方になっています。

```javascript
    // <span>などがネストされていた場合は取り除く 
    var model = modelList[(i - 2)].replace('<span class="txtSize-smaller">',"");
    model = model.replace("</span>", "");
    // replace2回目
    model = model.replace('<span class="txtSize-smaller">',"");
    model = model.replace("</span>", "");
```
同じ内容でのreplace()処理が2回繰り返されています。

JavaScriptであればreplaceメソッドは第一引数の文字列全てを一度に置換できるはずなのですが、今回は最初のものしか置換できなかったので、渋々2回繰り返すことにしました。

もちろん、削除対象がspanタグを3つ、4つと含んでいればその分だけ繰り返すことになります。
### 終わりに
以上が、Gasを用いたスクレイピングの実装方法になります。
複数のURLを経由したり、条件に合ったものだけを出力したりといったより高度な方法については、機会があれば取り上げます。