Node.js
js
Web
scraping
WebScraping

Node.jsにてGoogle画像検索ページを作成(WebScraping)

今回の投稿は3回目で、Web界隈の投稿は初になります。
Webプログラミングは初心者で、至らないところもたくさんあると思います。
ご指摘いただけると幸いです。

背景

Node.jsへ慣れるための一環として作成しました。
以前から画像スクレイピングは興味があり、今回の課題としました。

開発環境

今回はローカル上のみでの動作を目標とします。
・Windows10
・GoogleChrome
Node.js@8.11.1
npm@5.6.0
Express@4.16.0
ejs@2.6.1
cheerio-httpcli@0.7.3
bower@1.8.4
jquery@2.2.4
Linuxコマンドを使用したいため、GitBashを使用しています。
テンプレートエンジンにはEjsを用いています。

今回の目標

今回は以下の画像に示すようなシステムを目指します。
inputFieldに文字を入力し、Do searchボタンを押したら検索を行い、画像の一覧を表示するシステムです。
scrapingSys.png

事前準備

今回作成するプロジェクトをGitHubにて公開しております。
モジュール関係のディレクトリはignoreしてあるため、事前準備の作業を行って下さい。
NodeScraping(Github)

前提として、Chrome,Node.jsはインストールしてある事とします。

スケルトンコードの作成

//作業用ディレクトリの作成
$ mkdir NodeScraping
$ cd NodeScraping

//npmを使用して、Expressをインストール
$ npm install express --save
$ npm install express-generate -g --save

//expressコマンドでスケルトンコードを作成
$ express --view=ejs myapp  //myappというディレクトリ内にアプリを作成。(テンプレートエンジンはEJS)
$ cd myapp/
$ npm install //必要なモジュールをインストール

この時点でアプリを起動する準備が出来ています。
npm startコマンドを実行して、アプリケーションを実行します。
chromeにてlocalhost:3000と検索すると、Welcome to Expressと表示されるはずです。

次に、クライアント用JSのパッケージ管理ツールbowerとjqueryをインストールしていきます。

bowerとjqueryのインストール

bowerとは、クライアント用のモジュールを管理するパッケージマネージャーです。
主に、jqueryやbootstrap等を取り扱います。
bowerはnpmにてインストールできます。

//bowerのインストール
$ npm install bower -g --save

bowerからインストールされるモジュールを格納するディレクトリを指定します。

$ cd public/
$ mkdir third_party    //ここにbowerからインストールされるコンポーネントを格納します。
$ cd ..
$ touch .bowerrc

.bowerrcの中身は以下のように記述してください。

.bowerrc
{
  "directory": "public/third_party"
}

これで、bowerよりinstallされるパッケージはpublic/third_party内に格納されます。
最後に、jqueryをインストールします。

$ bower install jquery --save

cheerioのインストール

cheerioはWebスクレイピング用のモジュールです。
npmでインストールしていきます。

$ npm install cheerio-httpcli --save

サーバサイドプログラミング

Node.jsを用いて、サーバ側のJSを記述していきます。
今回は面倒なことを省くため、ルーティング設定は変更しません。
全てルートディレクトリ内(index.js)で処理をしていきます。

app.jsの変更

アプリケーションの根幹を担うスクリプトです。
ルーティングの設定や、テンプレートエンジンの指定など基本設定を記述していきます。
expressのスケルトンコードにてほぼ完成されているため、変更箇所はわずかです。
以下の一か所のみを変更します。

app.js
app.use(express.static(path.join(__dirname, 'public')));
                        
app.use('/public', express.static(path.join(__dirname, 'public')));

クライアントサイドのjsやcss,画像,bowerにてインストールしたモジュール等が格納されたディレクトリを指定します。
それを、プログラム内でpublicにて呼び出せるように変更します。

routes/index.jsの変更

このindex.jsにてスクレイピングを行い、その結果をJSONとして、index.ejsのパラメータに引き渡します。

プログラム

routes/index.js
var express = require('express');
var cheerioClient = require('cheerio-httpcli');
var router = express.Router();

//初期訪問時
router.get('/', function(req, res, next) {
    //空パラメータでindex.ejsをレンダリング
    res.render('index', { message: '{}', word: ''});
});

//パラメータ付きURLにて訪問時
router.get('/searchWord/:word', function(req, res, next){
    //Google画像検索
    var request = { q: req.params["word"], tbm: "isch" };
    var promise = searchClearlyByGoogleImages( request );

    //検索結果から、全画像URLをJSONへ変換
    promise.then( function( result ){
        var list = result.clearlyList;
        var i, length = list.length;
        var results = [];
        //画像URL全てを配列へ格納
        for( i=0; i<length; i++ ){
            if(list[i].src){
                results.push( list[i].src );
            }
        }
        //画像URLの配列をJSONへ変更
        var resultJson = JSON.stringify(results);
        //URLのJSONと検索ワードをパラメータに格納して、index.ejsをレンダリング
        res.render('index', {message: resultJson, word: req.params["word"]});
    }, function( error ){
        res.send(error);
    });
});

module.exports = router;


//**スクレイピング関数**
var searchClearly = function( url, request, clearly ){
    var promiseCheerio = cheerioClient.fetch( url, request );

    //プロミス形式で検索結果を返す
    return new Promise(function (resolve, reject) {
        promiseCheerio.then( function( cheerioResult ){
            if( cheerioResult.error ){
                reject( cheerioResult.error );
            }else{
                // リンク一覧を生成
                var $ = cheerioResult.$;
                resolve({
                    "clearlyList" : clearly( $ ),
                    "cheerioJQuery" : $
                });
            }
        }, function( error ){
            reject( error );
        });
    });
}

// グーグル検索結果をリスト形式で取得する。
var searchClearlyByGoogleImages = function( request ){
    return searchClearly( "http://www.google.co.jp/search", request, function( $ ){
        var results = [];
        //画像を保持するタグ全てへアクセス
        $("a[class='rg_l']").each( function (idx) {
            var target = $(this);
            var img = target.find('img');

            results.push({

                "src" : img.eq(0).attr("data-src")
            });
        });
        return results;
    });
};
//**========**

Google画像検索ページのHTMLエレメント解説

Googleにて普通に画像検索を行った際の画像一つ一つのエレメントは以下のようになっています。

<a jsname="hSRGPd" href="" class="rg_l" rel="noopener" style="background: rgb(240, 237, 234); width: 263px; height: 179px; left: 0px;">
  <div class="Q98I0e" jsaction="IbE0S"></div>
  <img class="rg_ic rg_i" id="YY3hGeE3AixijM:" jsaction="load:str.tbn" alt="「???」の画像検索結果" onload="typeof google==='object'&amp;&amp;google.aft&amp;&amp;google.aft(this)" src="画像のURL" style="width: 263px; height: 184px; margin-left: 0px; margin-right: 0px; margin-top: -5px;">
  <div class="v08Hob save-button iv_msusc" role="button" tabindex="0" jsaction="lIOYZ" data-ved=""></div>
  <div class="v08Hob save-button iv_mssc" style="display:none" role="button" tabindex="0" jsaction="lIOYZ" data-ved=""></div>
  <div class="rg_ilmbg"> 400×280  - ???.com  </div>
</a>

一部省略しています。
このようにclass="rg_l"のaタグがルートに存在し、imgタグのsrc属性に画像のURLが格納されています。
このURLのみを取り出して処理を行っています。

スクレイピングプログラムの解説

index.jsのタグ指定を行っている部分を抜き出します。

//画像を保持するタグ全てへアクセス
("a[class='rg_l']").each( function (idx) {
var target = $(this);
var img = target.find('img');
results.push({
"src" : img.eq(0).attr("data-src")
});
});

このプログラムの順序は以下のようになっております。


  1. class='rg_1'を持つaタグ全て取得し、イテレーション
  2. aタグ内のimgタグを取得
  3. imgタグ内のsrc属性から画像URLを取得。(なぜかプログラム内では"data-src"と書かなければ動かなかった)
  4. 配列に取得したURLを追加

以上で画像URLのスクレイピング機能が実装できました。

ルーティングの解説

Node.jsのGETパラメータは特殊です。
Webでよく使うGetのパラメータ設定はlocalhost:3000?word=犬ですが、
Node.jsではlocalhost:3000/word/犬となります
index.js上部ではrouter.get('/')router.get('/searchWord/:word')でパラメータ付きURLか区別をしています。
get関数の第一引数にはlocalhost:3000以降の階層を記述ます。

パラメータ付きURL内の関数では
req.params['word'];
で検索ワードを取得しています。
スクレイピング関数に検索ワードを渡して、URLの配列を取得します。
関数の最後では、URLを格納した配列をJSON化します。

次章では以上のパラメータを使用して動作する、クライアントサイドのプログラムを作成します。

クライアントサイドプログラミング

サーバーサイドにて処理されたパラメータをもとに、HTMLとJSを記述していきます。
まずは、routes/index.jsからレンダリングされるview/index.ejsを変更していきます。

index.ejsの変更

routes/index.jsにて以下の記述を目にしたと思います。

routes/index.js
res.render('index', {message: resultJson, word: req.params["word"]});

これは、URLが詰まったJSONテキスト(resultJson)検索ワード(req.params["word"])をパラメータとして渡し、index.ejsのレンダリングを行うという意味です。

EJSとは?

HTML内でこのパラメータを扱うためにテンプレートエンジンのEJSを使用します。
EJSの説明をとあるブログ様より引用させていただきます。

EJSはテンプレートエンジンと呼ばれるツールの1つで、
JavaScriptのような書き方を取り入れつつHTMLが書けるという特徴を持っています。

テンプレートエンジン「EJS」とタスクランナー「Gulp.js」で爆速HTMLコーディング

プログラム

view/index.ejs
<!DOCTYPE html>
<html>
<head>
    <title>Search Image</title>
    <link rel="stylesheet" type="text/css" href="/public/stylesheets/images.css">
    <script type="text/javascript" src="/public/third_party/jquery/dist/jquery.min.js"></script>
    <script type="text/javascript" src="/public/javascripts/index.js"></script>
</head>
<body>
    <input type="hidden" class="image-urls" value="<%= message %>">
    <h1>Image search from google(WEB Scraping)</h1>
    <br>
    <div class="input-form">
        <input type="text" class="search-field">
        <input type="button" class="search-button" value="Do search">
        <h2 class="search-word">Search word : <%= word %></h2>
    </div>
    <br>
    <div class="container"></div>
</body>
</html>

プログラムの解説

プログラムの内容はほぼHTMLですが、<%= パラメータ名 %>でroutes/index.jsから渡されたパラメータを使用できます。

<input type="hidden" class="image-urls" value="<%= message %>">

input type="hidden"で、JSONテキストを埋め込みます。
後述のpublic/javascripts/index.jsにて使用するための準備です。
後は入力フォームと、検索結果を表示するテキストを用意しています。

public/javascripts/index.jsの作成

クライアント側のJSを作成していきます。
まずは、public/javascripts/内にindex.jsを作成してください。
routes/index.jsとは別種ですので、ご注意ください。
内容を記述していきます。

プログラム

※Jqueryを使用しています

public/javascripts/index.js
//ページのレンダリング後に実行
$(function(){
    //1.==イベントリスナーの登録==
    $('.search-button').on('click', function(){
        //URLの整理
        var url = document.URL;
        var pos = url.indexOf("searchWord");
        if(pos > 0){
            url = url.substring(0, pos);
        }

        //パラメータ付きでリダイレクト
        var searchWord = $('.search-field').val();
        location.href =  url + 'searchWord/' + searchWord; 
    });


    //2.==URLの配列から画像一覧を生成==
    //JSONから画像一覧を取得
    var imageUrlJSON = $('.image-urls').val();
    var imageUrls = JSON.parse(imageUrlJSON);

    //バリデーション
    if(imageUrls == null){
        return;
    }

    var rootDiv = $('.container');

    for(var i = 0; i < imageUrls.length; i++){
        //画像を生成し、DIV内に格納
        var img = $('<img>', {src: imageUrls[i]});
        img.appendTo(rootDiv);
    }
})

プログラムの解説

このプログラムは以下のように分けられます。


  1. イベントリスナーの登録
  2. URLの配列から画像一覧を生成

1番では、ボタンを押されたときのイベントを登録しています。
イベントは、以下の手順で成り立っています。

1-1.現在のURLから、余計な部分を消す。→(localhost:3000/searchWord/犬)
1-2.入力された検索ワードを抜き出す。
1-3.パラメータ付きURLでリダイレクトを行う



2番は以下のような手順で成り立っています。

2-1.JSONをHTMLから抜き出す
2-2.JSONからオブジェクトへパース
2-3.オブジェクトがなかった場合のバリデーション
2-4.画像全てをイテレーション
2-5.imgタグを生成し、画像URLをsrc属性として付与する。

アプリケーションの実行・確認

全てのプログラムの記述が終了しました。
では、実行してみましょう。

$ cd myapp/
$ node app.js

Chromeを立ち上げ、localhost:3000で検索しましょう。
以下の画像のようなシンプルな画面が出力されるでしょう。
出力された場合は成功です。
inputFieldに検索したい単語を入れ、Do searchボタンを押せば画像検索されます。

まとめ

途中CROS等様々な所でつまづき、思うようにいかないことも多かったです。
結果としてシンプルな形に収められてよかったです。
間違って掲載している情報など発見しましたら、ご報告いただけると幸いです。

参考資料

★nodeでスクレイピングをしたのでライブラリ「CasperJS」と「cheerio-httpcli」の紹介(Qiita)
Node.js + ExpressでREST API開発を体験しよう[作成編]
Promiseを使う
Express Routing(GETにてパラメータを取得する方法)
★Node.js + Express で作る Webアプリケーション 開発 入門
★が付いているサイトは今回の制作にて大変参考になりました。