マイブーム@技術と生活

仕事や生活に関わる技術的なことを記述します。

簡単なウェブ解析アプリを作るための node.js、request、cheerio の使い方

勉強のために日本語訳します。

https://www.digitalocean.com/community/tutorials/how-to-use-node-js-request-and-cheerio-to-set-up-simple-web-scraping

 

はじめに

本文では、ハッカーニュースサイトの、上位ランキングのリンクと、題名、URL、点数、コメント数など他のデータを取得するために、ホームページを走査します。これは、node.js を使ってウェブページからデータを取得するための、いくつかある方法の内の1つです。主に、Matthew Mueller 氏が作成した cheerio と呼ばれるモジュールを使います。cheerio はサーバーサイドで使うように設計された jQuery のサブセットです。

cheerio は軽量で、高速、融通が利き、jQuery に慣れていれば、使い易くできています。また、簡単な HTTP クライアントとして、Mikael Rogers 氏が作成した request モジュールを使います。

 

必要事項

node.js、jQueryVPSSSH の接続方法など基本的な Linux の管理・設定ができることが前提です。もし、node.js を使うことが初めてで、まだインストールしていなければ、お使いのOSへのインストール手順について、上記の Articles & Tutorials の章を参照してください。

 

プログラム

NPM を使って必要なモジュールをインストールします。次のコマンドを実行します。

npm install request cheerio

これは、現在の作業ディレクトリにのみ、モジュールをインストールします。全体にインストールする場合は、 npm install -g request cheerio を実行します。

ファイル scrape.js  を作成し、次の行を追加します。

var request = require('request');
var cheerio = require('cheerio');

これは、必要になるモジュールをすべて読込みます。

まずは request でハッカーニュースサイトのホームページを読込み、ページの HTML コードを表示しましょう。もし、エラーが無ければ、HTTP ステータスは 200 になります。

ファイルに次の行を追加します。

request('https://news.ycombinator.com', function (error, response, html) {
  if (!error && response.statusCode == 200) {
    console.log(html);
  }
});

node scrape.js でスクリプトを実行してみてください。端末画面に HTML コードが表示されるはずです。

目的のデータをどうやって抽出するかを調べるために、HTML コードで要素がどのように構成されているかを知る必要があります。良い方法は、Google Chrome に組み込まれているウェブデベロッパーツールを使って、目的の要素を調査することです。ウェブページで右クリックし「要素を調査する」を選びます。

次の例では、Chromeハッカーニュースサイトを開いて、右クリックし、上位ランク記事の題名を調査しています。

図1.

ウェブデベロッパーコンソールで確認すると、上位ランクの30個すべてのリンクと題名を解釈するためには、 クラス名 comhead が付いた「span」要素を探し出し、jQuery API 関数の prev() を使って、その1つ前の「a」要素を選べば良いことが分かります。

プログラムを次のように変更して、動作を確認します。

request('https://news.ycombinator.com', function (error, response, html) {
  if (!error && response.statusCode == 200) {
    var $ = cheerio.load(html);
    $('span.comhead').each(function(i, element){
      var a = $(this).prev();
      console.log(a.text());
    });
  }
}); 

プログラムを実行すると、期待したように、30個の題名の一覧が得られました。次に、残りの他のデータを解釈するように修正します。

request('https://news.ycombinator.com', function (error, response, html) {
  if (!error && response.statusCode == 200) {
    var $ = cheerio.load(html);
    $('span.comhead').each(function(i, element){
      var a = $(this).prev();
      var rank = a.parent().parent().text();
      var title = a.text();
      var url = a.attr('href');
      var subtext = a.parent().parent().next().children('.subtext').children();
      var points = $(subtext).eq(0).text();
      var username = $(subtext).eq(1).text();
      var comments = $(subtext).eq(2).text();
      // Our parsed meta data object
      var metadata = {
        rank: parseInt(rank),
        title: title,
        url: url,
        points: parseInt(points),
        username: username,
        comments: parseInt(comments)
      };
      console.log(metadata);
    });
  }
}); 

 

追加したプログラムの説明

1つ前要素の選択

var a = $(this).prev();

a要素より2つ上位の要素を解釈して、ランクを取得する

var rank = a.parent().parent().text();

リンクの題名を解釈する

var title = a.text();

a要素から href パラメータを解釈する

var url = a.attr('href');

HTML の表の次行から、subtext 子要素を取得する

var subtext = a.parent().parent().next().children('.subtext').children();

子要素から実際のデータを取得する

var points = $(subtext).eq(0).text();
var username = $(subtext).eq(1).text();
var comments = $(subtext).eq(2).text();

修正したスクリプトを実行すると、オブジェクトの配列が次のように出力されます。

[ { rank: 1,
    title: 'The Meteoric Rise of DigitalOcean ',
    url: 'http://news.netcraft.com/archives/2013/06/13/the-meteoric-rise-of-digitalocean.html',
    points: 240,
    username: 'beigeotter',
    comments: 163 },
  { rank: 2,
    title: 'Introducing Private Networking',
    url: 'https://www.digitalocean.com/blog_posts/introducing-private-networking',
    points: 172,
    username: 'Goranek',
    comments: 75 },
...

以上です。加えて、詳細な解析を行うために、抽出したデータを、MongoDB や Redis のようなデータベースに保存できます。scrape.js ファイルのプログラム全体を以下に示します。

var request = require('request');
var cheerio = require('cheerio');

request('https://news.ycombinator.com', function (error, response, html) {
  if (!error && response.statusCode == 200) {
    var $ = cheerio.load(html);
    var parsedResults = [];
    $('span.comhead').each(function(i, element){
      // Select the previous element
      var a = $(this).prev();
      // Get the rank by parsing the element two levels above the "a" element
      var rank = a.parent().parent().text();
      // Parse the link title
      var title = a.text();
      // Parse the href attribute from the "a" element
      var url = a.attr('href');
      // Get the subtext children from the next row in the HTML table.
      var subtext = a.parent().parent().next().children('.subtext').children();
      // Extract the relevant data from the children
      var points = $(subtext).eq(0).text();
      var username = $(subtext).eq(1).text();
      var comments = $(subtext).eq(2).text();
      // Our parsed meta data object
      var metadata = {
        rank: parseInt(rank),
        title: title,
        url: url,
        points: parseInt(points),
        username: username,
        comments: parseInt(comments)
      };
      // Push meta-data into parsedResults array
      parsedResults.push(metadata);
    });
    // Log our finished parse results in the terminal
    console.log(parsedResults);
  }
});