GoogleHomeに話しかけて検索結果をブラウザに表示させる　❻スクレイピングして検索結果のタイトルとURLを登録する

GoogleHome(mini)を購入したので、聞き取ったキーワードから
PCのブラウザに**Googleの検索結果1位のサイトを表示**して、そのサイトの**タイトルを読み上げる**アプリを作成しました。検索したキーワードがDBに無かった場合はキーワードとURLとタイトルを登録します。
##**最終的に出来たアプリ**
![1_ぴーぷるの大盛はまだ食べたことがありません.png](https://qiita-image-store.s3.amazonaws.com/0/237418/6738b748-4790-776c-0ec9-02393a09ceb1.png "ぴーぷるの盛りはヤバいのです!!普通サイズでお腹いっぱいになります※日曜はお休み")

##**ワタシがやったこと**
極力Googleさんのサービスだけで完結したいなーということで、DialogflowとFirebaseを使用
Firebaseがホスティングサーバーとデータベースと関数の管理と色々サービスを担ってくれているので
全部お任せしました。
![2_ワタシがやったことjpg.png](https://qiita-image-store.s3.amazonaws.com/0/237418/ad8611c7-628d-7d79-3d09-d8805a0472cb.png "本当Firebaseって素敵なサービスです💛")


:one:　**[Dialogflowを使用して簡単なチャットボットを作成](https://qiita.com/tedkuma/items/9ceb17936c714a205a74)**
:two:　**[FirebaseのFunctions(cloud functions)を使用して、:one:で作ったボットが適切な返事するように設定](https://qiita.com/tedkuma/items/d892ec31c3cdaf35753b)**
:three:　**:two: [を実行する際に**FirebaseのDatabase**にデータを追加するように設定](https://qiita.com/tedkuma/items/38326f509e6a9f595eac)**
:four:　**:three: [のデータ追加をトリガーに**FirebaseのHosting**で作成したページ(htmlファイル)にパラメータを渡し、
　　　windowopenで対象のページを開くように設定](https://qiita.com/tedkuma/items/f35d3c7cd02778c800b6)**
:five:  　**[Google検索で一番上に表示されてるページを一発で表示させてみる(I'm feeling luckey!)](https://qiita.com/tedkuma/items/e6e2bcec1a540bf17b19)**
:six: 　データ登録時にページタイトルとサイトURLはオリジナルのものを保存する
　　　(webスクレイピングには**[cheerio-httpcli](https://www.npmjs.com/package/cheerio-httpcli)**を使用しました )
:seven:　 フリーのMP3データを**FirebaseのStorage**に入れてレスポンスに音声データを返す(SSML)

---
![3_stage4.png](https://qiita-image-store.s3.amazonaws.com/0/237418/880bd997-4310-5aec-6700-313ff9e7de1d.png)

---

##**:six: スクレイピングして検索結果のタイトルとURLを登録する**##
前回までで、○○を表示してって言ったら、そのページを検索してページが一発で表示されるようになりました。しかーし！せっかくDBに登録しているのに登録したURLが全部`http://www.google.com/search?btnI=I%27m+Feeling+Lucky&lr=lang_ja&ie=UTF-8&oe=UTF-8&q=○○`って、検索ワードを保存しているのにURLでも二重で登録してるみたいで嫌ですよね。それにいつまでも○○の検索結果を表示します なんて、全然知的じゃないので、ページのタイトルを読み上げてもらったらいいんじゃないかと思います。ということで、
#####**今回はwebスクレイピングをします。**#####

**webスクレイピングって??**　Webページのデータを収集情報を抽出することです。

到底、ワタシにはそんな難しいのは書けないので、Node.js用のWEBスクレイピングモジュールというのを使用しました。当初、別のモジュールで作成してましたが諸事情で躓いてcheerio-httpcliを使ってみたら、すんなり出来たので使いやすさは抜群だと思います。

**[cheerio-httpcli](https://www.npmjs.com/package/cheerio-httpcli)**のページにも書いてますが、念のためこちらでも説明します。

今回はNode.jsのモジュールを追加するので
functionsディレクトリに移動してから下記のコマンドを実行します。

~~~shell
npm install cheerio-httpcli
~~~

<details><summary>はい、パッケージが追加されました。</summary>
![4_functionsに移動してからですよ.png](https://qiita-image-store.s3.amazonaws.com/0/237418/2584519e-00a7-5e06-9b5a-57b5e3851890.png)
</details>

functionsのnode_modulesにcheerioとcheerio-httpcliが追加されています。
![5_追加されましたね.png](https://qiita-image-store.s3.amazonaws.com/0/237418/5c08950b-7cbf-8c30-986a-dd1c290cbd1f.png)

丁寧にcheerio-httpcliのディレクトリの中にサンプルファイルを置いて下さっているので
それで早速スクレイピングを体感してみます。
![6_スクレイピングを体感しよう.png](https://qiita-image-store.s3.amazonaws.com/0/237418/5dc3fc17-f1c9-049a-f082-320f9ba525c1.png)

google.jsの中身はこんなことが書いてあります。検索ワードがqに入ってて、
その検索結果一覧からリンクやらタイトルやらを取得してくれるようです。
![7_こんなん入ってます.png](https://qiita-image-store.s3.amazonaws.com/0/237418/37ac322f-bece-e91c-938c-4357aad9a963.png)

まずは実行してみましょう。cheerio-httpcli/exampleディレクトリに移動してからgoogle.jsを実行します

~~~shell
　cd functions/node_modules/cheerio-httpcli/example

　node google.js
~~~

すると、うわー！いっぱい出てきた!!これを使えばいけそうですね。
![8_うわーいっぱい出てきた！.png](https://qiita-image-store.s3.amazonaws.com/0/237418/d0fbb623-1433-7b77-d66c-af1dd1542b1c.png)

cheerio-httpcliの概要はこんな感じです。特にこのコールバックの部分が肝です。
![9_cheerio-httpcliの概要.png](https://qiita-image-store.s3.amazonaws.com/0/237418/ca6a7775-e880-a346-e984-313d89f24044.png)

#####**では、さっそくindex.jsにコードを追加していきます。**#####

<details><summary>前回までのindex.js</summary><div>

~~~javascript
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp(functions.config().firebase);

exports.hoyatalk = functions.https.onRequest((request, response) => {
    const dialogflow_param = request.body.queryResult.parameters.keyword; 
    const dialogflow_user_say = request.body.queryResult.queryText; 
    const keyword = dialogflow_param.join(" ");

    const nd = new Date();
    nd.setTime(nd.getTime() + 1000*60*60*9);
    const year = nd.getFullYear(),month = nd.getMonth()+1,day =nd.getDate(),hour = nd.getHours(),minute = nd.getMinutes();
    const time = year + "年" + month + "月" + day + "日" + hour +  "時" + minute + "分" ;

    const hoyapath = "/hoya/boya"
    const musubipath = "/musubi/maru"

    admin.database().ref(hoyapath).once("value", function(snapshot) { 
        response.setHeader("Content-Type", "application/json"); 
        response.send(JSON.stringify({"fulfillmentText": "はい、"+keyword+"の検索結果を表示します"}));
        admin.database().ref(hoyapath).push({url: "http://www.google.com/search?btnI=I%27m+Feeling+Lucky&lr=lang_ja&ie=UTF-8&oe=UTF-8&q=" + keyword, param_value:keyword , timestamp: time });
        admin.database().ref(musubipath).set({timestamp: time, tx:keyword , url: "http://www.google.com/search?btnI=I%27m+Feeling+Lucky&lr=lang_ja&ie=UTF-8&oe=UTF-8&q=" + keyword });
    });
});
~~~
</div></details>

コメントで★追加となっている箇所が追加したところです。
コールバックの引数(err, $, res)で上の1、2、3の引数を受け取っています。
ログはまた後で解説するために書いているだけなので実行時に不要であれば消してくださいね。

~~~javascript:index.js
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const client = require('cheerio-httpcli');//★追加
admin.initializeApp(functions.config().firebase);

exports.hoyatalk = functions.https.onRequest((request, response) => {
    const dialogflow_param = request.body.queryResult.parameters.keyword;  
    const dialogflow_user_say = request.body.queryResult.queryText; 
    const keyword = dialogflow_param.join(" ");

    const nd = new Date();
    nd.setTime(nd.getTime() + 1000*60*60*9);
    const year = nd.getFullYear(),month = nd.getMonth()+1,day =nd.getDate(),hour = nd.getHours(),minute = nd.getMinutes();
    const time = year + "年" + month + "月" + day + "日" + hour +  "時" + minute + "分" ;

    const hoyapath = "/hoya/boya"
    const musubipath = "/musubi/maru"

    admin.database().ref(hoyapath).once("value", function(snapshot) {   
        
        client.fetch('http://www.google.com/search?btnI=I%27m+Feeling+Lucky&lr=lang_ja&ie=UTF-8&oe=UTF-8&q=', { q: keyword }, function (err, $, res){ //★追加        
            var pagetitle = $('title').text(); //★追加        
            var pageurl = res.request.uri.href;//★追加
            console.log(pagetitle);//★追加
            console.log(pageurl);//★追加

            response.setHeader("Content-Type", "application/json"); 
            response.send(JSON.stringify({"fulfillmentText": "はい、"+keyword+"の検索結果を表示します"}));
            admin.database().ref(hoyapath).push({url: "http://www.google.com/search?btnI=I%27m+Feeling+Lucky&lr=lang_ja&ie=UTF-8&oe=UTF-8&q=" + keyword, param_value:keyword , timestamp: time });
            admin.database().ref(musubipath).set({timestamp: time, tx:keyword , url: "http://www.google.com/search?btnI=I%27m+Feeling+Lucky&lr=lang_ja&ie=UTF-8&oe=UTF-8&q=" + keyword });
        });//★追加
    });
});
~~~

さて、ではdeployしていつものようにテストします。えー！うそでしょー？返信がこなくなっちゃった!!:scream:　
firebaseのログを確認すると…請求先アカウントを設定して下さいと…
請求なんて嫌だねー！無視無視!!:money_mouth:とかやってるといつまでも解決しないのです。
![10_大変です。エラー出ました.png](https://qiita-image-store.s3.amazonaws.com/0/237418/9b02226e-a831-aa2f-757c-9d83be5a3761.png)


#####**Googleの請求先アカウントを設定します**#####
firebaseの左下にSpark　無料$0円/月　と書いてあるところのアップグレードをクリック
立ち上がったFirebaseの料金プランの画面でプランの詳細を参照をクリック
![11_料金プラン見てみる.png](https://qiita-image-store.s3.amazonaws.com/0/237418/676ecda7-1397-7f17-f7a6-a928a4c4c709.png)

無料プランのアウトバウンド ネットワーキングがGoogle専用になっています。
この設定をされているが故にGoogle以外で提供されているAPIが使えない状態になっているのです。
だから便利なことしようと思ったらプランの変更は必須
![12_無料プランではGoogle提供のものしか使えないのです.png](https://qiita-image-store.s3.amazonaws.com/0/237418/86706954-f858-5253-8a70-b1e771742700.png)

観念して有料プランにアップグレードします。ワタシはBlazeプランにしました。プランを選択…と
![13_致し方ありませんプランを選択.png](https://qiita-image-store.s3.amazonaws.com/0/237418/a9e267b8-e0e4-85ad-c52a-46b194486aa2.png)

プロジェクトを選んで
![14_払います.png](https://qiita-image-store.s3.amazonaws.com/0/237418/b9ed4eb2-ef52-0c5f-4402-61ddaab32c81.png)

次へを押します。
![15_払います.png](https://qiita-image-store.s3.amazonaws.com/0/237418/ce69aa51-a182-7c51-ffc9-6870979beeab.png)

利用規約を順守するのチェックをはいにして同意するをクリック
![16_払います.png](https://qiita-image-store.s3.amazonaws.com/0/237418/4d522c82-63ff-94ce-24a3-bbe46b6a6a36.png)

確認を押します。
![17_払います.png](https://qiita-image-store.s3.amazonaws.com/0/237418/88242c7e-a90e-d446-784b-7023f185d546.png)

ちょっと抵抗ありますがクレジットカードの情報も必要です。入力したら購入を確定ボタンを押します。
![18_お支払い.png](https://qiita-image-store.s3.amazonaws.com/0/237418/bce5361e-159d-154e-07ab-0ff4b1efbafd.png)

アップグレードされました。んじゃ再度テストしてみますー！
![19_変更されました.png](https://qiita-image-store.s3.amazonaws.com/0/237418/d64d04ff-8180-6dec-41b6-8d0901560ab6.png)

おー！今度は返事してくれましたー。じゃあfirebaseのログも確認しますよー。
やったー！ページのタイトルとオリジナルのURLが出力されていますー:v_tone2:
![20_ちゃんと出ていますー！.png](https://qiita-image-store.s3.amazonaws.com/0/237418/1d23dcf4-597d-2775-9057-6239b2faf827.png)

ここまでくればもう出来あがったも同然ですね。DialogflowへのresponseとDBへの登録につけかえるだけ★
ワタシはちょっとアレンジして、今まで検索されたことがない場合は「はい、〇〇を表示します」
検索されたことがある場合は「はいはーい、〇〇を表示します」ということにしました。
☟最終的なコードはこんな感じです。

~~~javascript:index.js
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const client = require('cheerio-httpcli');
admin.initializeApp(functions.config().firebase);

exports.hoyatalk = functions.https.onRequest((request, response) => {
    const dialogflow_param = request.body.queryResult.parameters.keyword;  
    const dialogflow_user_say = request.body.queryResult.queryText; 
    const keyword = dialogflow_param.join(" ");

    const nd = new Date();
    nd.setTime(nd.getTime() + 1000*60*60*9);
    const year = nd.getFullYear(),month = nd.getMonth()+1,day =nd.getDate(),hour = nd.getHours(),minute = nd.getMinutes();
    const time = year + "年" + month + "月" + day + "日" + hour +  "時" + minute + "分" ;

    const hoyapath = "/hoya/boya"
    const musubipath = "/musubi/maru"

    admin.database().ref(hoyapath).orderByChild('param_value').startAt(keyword).endAt(keyword).once("value", function(snapshot) { 
        
        if(snapshot.val()==null){//同じワードで検索したことがない場合　(DBのhoya/boyaのparam_valueにkeywordがnullなら）
            console.log(keyword);
            client.fetch('http://www.google.com/search?btnI=I%27m+Feeling+Lucky&lr=lang_ja&ie=UTF-8&oe=UTF-8&q=', { q: keyword }, function (err, $, res){ 
              var pagetitle = $('title').text();            
              var pageurl = res.request.uri.href;

              response.setHeader("Content-Type", "application/json")  
              response.send(JSON.stringify({"fulfillmentText": "はい、"+ pagetitle +"を表示します"}));
              admin.database().ref(hoyapath).push({url: pageurl, param_value:keyword , timestamp: time,title : pagetitle  });
              admin.database().ref(musubipath).set({timestamp: time, param_value:keyword , url: pageurl ,title : pagetitle});
            });
        }else{//同じワードで検索したことがある場合　(DBのparam_valueにkeywordが既にある）
            admin.database().ref(hoyapath).orderByChild('param_value').startAt(keyword).endAt(keyword).once("child_added", function(snapshot, prevChildKey) {
                var db =snapshot.val();
                response.setHeader("Content-Type", "application/json"); 
                response.send(JSON.stringify({"fulfillmentText": "はいはーい、"+ db.title +"を表示します"}));//DBのtitleを参照している
                admin.database().ref(musubipath).set({timestamp: time, tx:keyword , url: db.url ,title : db.title}); 
            });
        }
    });
});
~~~

ではテストしてみます。1回目～
![21_1回目です.png](https://qiita-image-store.s3.amazonaws.com/0/237418/f622a62e-48f6-4c12-0e7b-e74ea82bb0d4.png)

同じワードで2回目～　はいはーい と言ってます。
そしてDBのmusubi/maruにはhoya/boya以下から参照した情報がセットされています。
![22_2回目.png](https://qiita-image-store.s3.amazonaws.com/0/237418/fd7c07d5-9b85-79f6-0191-c0eca30b3075.png)

以上です。今回も長めでしたが最後まで読んで頂きありがとうございました。
次回はオマケ的な機能なのですが、レスポンスに音声ファイルとかも再生できるよーというのをやります。

---

　　　**次回の記事：[❼レスポンスで音声データを返す](https://qiita.com/tedkuma/items/f794d08b4f64b0a5ce73)**

　　　**以前の記事：[❶Dialogflowで簡単なチャットボットを作成](https://qiita.com/tedkuma/items/d892ec31c3cdaf35753b)**
　　　　　　　　　**[❷FirebaseのFunctionsでボットに返事をさせる(Dialogflow V2 API)](https://qiita.com/tedkuma/items/9ceb17936c714a205a74)**
　　　　　　　　　**[❸FirebaseのFunctions実行時 Databaseにデータを追加する](https://qiita.com/tedkuma/items/38326f509e6a9f595eac)**
　　　　　　　　　**[❹Databaseへのデータ追加をトリガーに特定のページへ遷移させる](https://qiita.com/tedkuma/items/f35d3c7cd02778c800b6)**
　　　　　　　　　**[❺Google検索で一番上のページを表示させる](https://qiita.com/tedkuma/items/e6e2bcec1a540bf17b19)**