Node.js で取得した Web ページを変数に保存して利用する方法


> [ ご注意 ]
> 記事タイトルがあまり適切でないことは認識しています。
> 自分は同期/非同期処理のキーワードに気付かないとこの回答に辿り着けなかったので、
> そのキーワードに気付けなくても検索で辿り着けるようなタイトルにしています。

最近 Node.js を触り始めました。

「Node.js でスクレイピングする方法」みたいな記事をよく見かけてたので簡単にできるものと思ってたけど、タイトルの通りのことができなくて混乱しました。

非同期ではなく、同期的に Web ページの取得をしたい場合の方法です。

やりたいことを他の言語で書くと以下のようになります。

## PHP

``Goutte`` を使うとサクッと書けます。

```bash:Terminal
$ composer require fabpot/goutte
```

```php:request-sample.php
<?php
require 'vendor/autoload.php';

$client = new Goutte\Client();
$crawler = $client->request('GET', 'http://www.yahoo.co.jp');

echo $crawler->filter('title')->text();
// or
echo $crawler->filter('title')->eq(0)->text();
```

```bash:Terminal
$ php request-sample.php
Yahoo! JAPAN
```

- [FriendsOfPHP/Goutte: Goutte, a simple PHP Web Scraper](https://github.com/FriendsOfPHP/Goutte)
- [Goutte(PHP)でスクレイピングしてみる - Qiita](http://qiita.com/zaburo/items/45d748ae3966bf08323f)

## Ruby

``Nokogiri`` を使って以下のようになります。

```bash:Terminal
$ gem install nokogiri
```

```ruby:request-sample.rb
require 'nokogiri'
require 'open-uri'

doc = Nokogiri::HTML(open('http://www.yahoo.co.jp'))
puts doc.css('title')
```

```bash:Terminal
$ ruby request-sample.rb
<title>Yahoo! JAPAN</title>
```

- [Parsing an HTML/XML document - Nokogiri 鋸](http://nokogiri.org/tutorials/parsing_an_html_xml_document.html)
- [Nokogiriを使ったRubyスクレイピング [初心者向けチュートリアル] | 酒と涙とRubyとRailsと](http://morizyun.github.io/blog/ruby-nokogiri-scraping-tutorial/)

## GoogleAppsScript

``UrlFetchApp`` を使います。

```js:コード.gs
function myFunction() {
  var response = UrlFetchApp.fetch('http://www.yahoo.co.jp');

  var myRegexp = /<title>([\s\S]*?)<\/title>/i;
  var match = myRegexp.exec(response);
  var title = match[1];

  title = title.replace(/(^\s+)|(\s+$)/g, '');
  Logger.log(title);
}
```

```ログ
[17-04-XX XX:05:02:350 JST] Yahoo! JAPAN
```

- [Class UrlFetchApp  |  Apps Script  |  Google Developers](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app)
- [Google Apps Scriptでスクレイピングする方法 - Qiita](http://qiita.com/ina6ra/items/23248b7290d8140f53fc)

## Node.js

では Node.js だとどう書くのかと思って調べると、``request`` や ``cheerio-httpcli`` などを使うらしい。

```bash:Terminal
$ npm install request
```

```js:request-sample.js
var request = require('request');

var response = '';

request('http://www.yahoo.co.jp', function(error, response, body) {
  response = body;
});

console.log(response);
```

```bash:Terminal
$ node request-sample.js
$
```

…あれ？

```bash:Terminal
$ npm install cheerio-httpcli
```

```js:request-sample2.js
var client = require('cheerio-httpcli');

var response = '';

client.fetch('http://www.yahoo.co.jp', function (err, $, res) {
  response = $;
});

console.log(response);
```

```bash:Terminal
$ node request-sample2.js
$
```

んんん？？
どちらを使っても結果が出力されない…。

公式サイトや色んな記事を見ても書いてあるのは ``request`` や ``fetch`` の中で処理を続ける方法のみ。

でも自分が本当にやりたいのは、取得した内容を呼び出し元関数に返して処理を続けるようなことなので、``fetch`` の中などに書いて続けるやり方じゃ要件を満たせないのです。。

```js:request-sample3.js
var request = require('request');

function myFunction() {
  var response = getContent('http://www.yahoo.co.jp');

  var myRegexp = /<title>([\s\S]*?)<\/title>/i;
  var match = myRegexp.exec(response);
  var title = match[1];

  title = title.replace(/(^\s+)|(\s+$)/g, '');
  return title;
}

function getContent(url) {
  var response = '';
  request(url, function(error, response, body) {
    response = body;
  });
  return response;
}

console.log(myFunction());
```

```bash:Terminal
$ node request-sample3.js
（getContent からの返り値が空文字なので、正規表現の処理に失敗してエラーになる）
```

## 正解は sync-request

日本語記事でそれらしきものがなくて、公式サイトを見てもよくわからないので Stack Overflow で聞いてみました。

- [node.js - How to save the contents obtained by request in a variable - Stack Overflow](http://stackoverflow.com/questions/43184207/how-to-save-the-contents-obtained-by-request-in-a-variable)

速攻で回答が来て、案の定「``request`` の中に書け」と言われる。。。
そんなことわかってんだよと思いながら「中じゃなくて外で値を利用したい」と返したら「``promises`` と ``async.js`` は見た？」との返事を頂く。

その他寄せられた回答を元に調べ直し、``sync-request`` を利用したら見事にうまくいきました。

```bash:Terminal
$ npm install sync-request
```

```js:request-sample4.js
var request = require('sync-request');

var response = request('GET', 'http://www.yahoo.co.jp');

console.log(response.getBody().toString());
// or
console.log(response.body.toString());
```

```bash:Terminal
$ node request-sample4.js
（Yahoo! のソースコードが出力される）
```

- [sync-request](https://www.npmjs.com/package/sync-request)
- [同期処理でrequestモジュールの戻り値を返す(Node.js)(非コールバック) - designetwork](http://designetwork.hatenablog.com/entry/2016/11/16/node-js-sync-request)
- [Node.jsで同期処理でHTTPリクエストを実行してページを取得する。 - 望月いちろうのREADME.md](http://www.utali.io/entry/2016/10/11/201203)

前述の本当にやりたいこと（``request-sample3.js``）を修正すると以下のようになります。

```js:request-sample3.js
//var request = require('request');
var request = require('sync-request');

function myFunction() {
  var response = getContent('http://www.yahoo.co.jp');

  var myRegexp = /<title>([\s\S]*?)<\/title>/i;
  var match = myRegexp.exec(response);
  var title = match[1];

  title = title.replace(/(^\s+)|(\s+$)/g, '');
  return title;
}

function getContent(url) {
  var response = '';

//  request(url, function(error, response, body) {
//    response = body;
//  });
  response = request('GET', url);

//  return response;
  return response.body;
}

console.log(myFunction());
```

```bash:Terminal
$ node request-sample3.js
Yahoo! JAPAN
```

(^o^)／

## 原因とまとめ

原因というか仕様というか。
Node.js では基本的に同期処理ではなくて非同期処理でリクエストが実行されるようです。
今回のように同期処理で値を取得したい場合はライブラリを使ったりなどコツがいるようです。

``sync-request`` の公式サイトには注意書きとして、利用は非推奨で本番環境では使うなよ！と書いてあります。
自分はテストとして使いたいだけなので問題ないですが、ご利用は計画的にお願いします…。

- [sync-request](https://www.npmjs.com/package/sync-request)
- [Node.jsのHTTP通信はsync-request非推奨でthen-requestを推奨 - designetwork](http://designetwork.hatenablog.com/entry/2017/02/21/node-then-request)

## 最後に

繰り返しになりますが、記事タイトルがあまり適切でないことは認識しています。
自分は同期/非同期処理のキーワードに気付かないとこの回答に辿り着けなかったので、
そのキーワードに気付けなくても検索で辿り着けるようなタイトルにしています。