GASでクローラ作る際に使った機能

# 経緯

社内でクローラ作ってデータ収集してBI(tableau)でレポート作りたいという要望があって、初めGAE+BQで作ろうかと思ったがデータ数大したことないのでGAS+Spreadsheetでいいや、って思ってGASでクローラ作ることになった.

## クローラ作る上で使った処理

### URLを指定してコンテンツ取得

[Class UrlFetchApp  |  Apps Script  |  Google Developers](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app#fetch(String))

```js:urlfetch.gs
function urlfetch() {
  const url = 'https://www.google.com/';
  const postheader = {
    "accept":"gzip, */*",
    "timeout":"20000"
  }  
  
  const parameters = {
    "method": "get",
    "muteHttpExceptions": true,
    "headers": postheader
  }
  
  Logger.log(UrlFetchApp.fetch(url, parameters).getContentText('UTF-8'));
}
```

parametersは必須ではないが、サイトによってはデフォルトの設定ではブロックされるので指定したほうがいい。

<hr>

#### 20181211 追記

@alma2 さんのご指摘でheadersにuseragent指定しても`Mozilla/5.0 (compatible; Google-Apps-Script)`のままで変更されないことがわかりましたので、コード修正させていただきました。今の所は変更する手段はなさそうです。

<hr>

クロールしたいページが多い場合はfetchAllを使ったほうが良い.

```js:urlfetchall.gs
function urlfetchall() {
  const urls = ['https://www.google.com/', 'https://www.google.com/'];
  const postheader = {
    "useragent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36",
    "accept":"gzip, */*",
    "timeout":"20000"
  }  
  
  var requests = [];
  urls.forEach(function(url) {
    requests.push({
      "method": "get",
      "muteHttpExceptions": true,
      "headers": postheader,
      "url": url
    });
  });
  
  var responses = UrlFetchApp.fetchAll(requests);
  
  responses.forEach(function(r) {
    Logger.log(r.getContentText('UTF-8'));  
  });
  
}
```

### コンテンツの中の特定の部分を抽出

xpathで取得したい部分を抽出したかったが、ちょうどいいHtmlパーサーがなかったので正規表現で頑張って取ってきた.

```js:regexp.gs
function regexp() {
  const content = UrlFetchApp.fetch('https://www.oreilly.co.jp/books/prog/').getContentText('UTF-8');
  
  const m = content.match(/<td id="\d+" class="title">\s*<a [^>]+>([^<]+)<\/a>/m);
  Logger.log(m[1]);
}
```

一覧とかでglobalに取ってきたい場合はこんな感じ.

```js:regexpglobal.gs
function regexpglobal() {
  const content = UrlFetchApp.fetch('https://www.oreilly.co.jp/books/prog/').getContentText('UTF-8');
  
  while ((m = /<td id="\d+" class="title">\s*<a [^>]+>([^<]+)<\/a>/gm.exec(content)) !== null) {
    Logger.log(m[1]);
  }
}
```

XmlServiceのパーサーはあるが、htmlページをパースするとだいたいエラーになるのでこちらは不採用.

Json形式のレスポンスの場合はJSON.parse使うと良さそう.

### 設定をプロパティから読み取る

gasのエディタからファイル→プロジェクトのプロパティからユーザープロパティとスクリプトプロパティが設定できる.

ハードコードしたくない設定などはこちら使った。

```js:properties.gs
function properties() {
  Logger.log(PropertiesService.getScriptProperties().getProperty('SpreadsheetId'));
}
```

### ログを記録する

ログ出力は主に2つあり、Loggerを用いたものはエディタ上での表示→ログで確認できるログを出力します.こちらは記録用というよりはデバッグ用の一時的なログとして使った.

記録に残したい場合はconsoleの方を使う.こちら使うとStackdriver Loggingの方に出力されるため、そのままでも一定期間保持される.永続化したい場合はsink使ってbqにエクスポートするとかもできる.

```js:log.gs
function log() {
  Logger.log('Hello World!');
  console.log('Hello World!');
}
```

### 一時停止する

クロールする際に先方に負荷かけないために一定時間間開けたいとかの場合に使った.

```js:sleep.gs
function sleep() {
  Utilities.sleep(1 * 1000);
}
```

### SpreadSheetに追記する

クロールした結果を記録するのに使った.

```js:writeSpreadsheet.gs
function writeSpreadsheet() {
  // IDはSpreadsheetのurlのdの次の値 https://docs.google.com/spreadsheets/d/xxxxxxxxxxxxxxxxxxxxx
  const spreadsheet = SpreadsheetApp.openById('xxxxxxxxxxxxxxxxxxxxx');
  const sheet = spreadsheet.getSheetByName('sheet1');
  
  // 末行に追記
  spreadsheet.appendRow(['a', 'i', 'u', 'e', 'o']);
}
```

### GoogleDriveにファイル保存

ページレイアウト変更などでクロールが失敗した場合に後からクロールし直せるようにGoogleDriverにhtmlを保存するために使った.

```js:saveHtml.gs
function saveHtml() {
  const content = UrlFetchApp.fetch('https://www.oreilly.co.jp/books/prog/').getContentText('UTF-8');
  
  // IDはGoogleDriveのurlのfoldersの次の値 https://drive.google.com/drive/folders/xxxxxxxxxxxxxxxxxxxxx
  var errorContentFolder = DriveApp.getFolderById('xxxxxxxxxxxxxxxxxxxxx');
  errorContentFolder.createFile('orielly.html', content, MimeType.HTML);
}
```

## gasを使った上での感想

- エディタと実行環境がブラウザ上で完結しているので書き出しが楽
  - ローカルに環境作る方法もあったり、ES2015で書く方法もあったりしたが、うまくいく場合と行かない場合があったので結局採用しなかった
- 定期実行やAPIの公開がクッソ楽
- デバッガの挙動が微妙
  - 複数ファイルに分けてコード書いていると、トリガーのファイル以外のブレイクポイントが無視されることがよくあった
- バージョン管理が貧弱
  - バージョン記録することはできるが、バージョン戻したりができなさそう
  - 当然ブランチとかもない

## 参考にさせていただいた記事

- [GASでSpreadsheetを操作する自分的ベストプラクティス - Qiita](https://qiita.com/ryan5500/items/e72eb205fbe006c2eb6f)
- [逆引きGoogle Apps Scriptリファレンス «](http://www.bmoo.net/archives/google-apps-script-refference)
- [Google Apps Scriptでスクレイピングを定期実行して、News Picksのソーシャルデータを取得する方法をまとめてみた|Review of My Life](https://review-of-my-life.blogspot.com/2018/02/google-apps-news-picks-scraper.html)
- [Google Apps ScriptでスクレイピングしてSlackに定期ポストするbotを瞬殺で作った - Qiita](https://qiita.com/fireowl11/items/e703e35073b600528e7c)
- [SpreadSheetでスクレイピング。Importxml他、便利な関数9+1 - Qiita](https://qiita.com/ktmg/items/d53440c913e20f8bb34c)