puppeteerちゃんでSNSチェック


概要

Typescriptでpuppeteerを使って、駒場祭で企画を出展したい人々が入力したSNSアカウントやウェブサイトのページを自動でチェックするツールを作りました。

※なおこの記事の趣旨はpuppeteerの紹介です。あと、もしこの記事を見てpuppeteerちゃんに興味を持っていただけたなら、たぶんこの記事を読むだけじゃなくてちゃんと自分で調べてみた方がいいです。

課題

学園祭のウェブサイトって、やっぱり出展している企画についての情報も載せたいですよね。
駒場祭では、多様な学生(団体)による数百に及ぶ企画全てに、ウェブサイトに載せる企画・団体紹介情報をいろいろ入力してもらっています。
この情報の中には団体のウェブサイトやSNSのアカウントなども含まれていて、駒場祭ウェブサイトには実際にそれらへのリンクが貼られたりします。

ここで、もしそのリンクを踏んだら404に飛ぶとか、R18サイトに飛ぶとか、過激派のサイトに飛ぶとか、そういうことがあると渋さMAXですよね。
ということで、入力されたウェブサイトやSNSアカウントが問題ないかをチェックする、「SNS査定」というお仕事が発生するわけです。

できたらいいな

1. ウェブサイトやSNSのアカウントが404とかにならずに存在することを確かめたい
ちなみに、SNSはTwitter、Facebook、Instagram、Youtubeの4つです。

2. ウェブサイトやSNSアカウントが本当に企画や団体と関係のあるものかどうかを判定したい
SNSにはとりあえずアカウント名やアカウントの説明文みたいなのがあるのでそれを企画の人たちが入力する団体名や団体説明文と比べてみようかな、という感じです。
ただ、アカウント名と団体名が必ずしも一致すると言い切れるかは微妙、だと思ったので単純に同じ文字列かどうか確かめるのは良くないのかなぁって思ったり…

ウェブサイトは…どうしましょうか、表示されている文字列をめちゃ集めて分析とかできたらいいですよね(白目)

3. 公序良俗に反するサイトではないかをチェック
表示されている文字列をめちゃ集めて分析する…って感じでしょうか
SNSの場合は投稿された文字列をひたすら集めてがんばる、っていう話になってくるのかなぁ…

やったこと詳細

使ったライブラリ

"puppeteer"ちゃんです。従順でかわいい操り人形ちゃん。
指定したURLのサイトに行かせて、ページのスクショを撮ったり、表示されている文字列を取得してきたり、ボタンをぽちぽちさせたりフォームに文字を入力したりと、かなりいろいろなことができます。中身はChromeという噂。
マクロを書くような感じ、と言うと伝わる人が多いのかもしれません。

とりあえずSNSのアカウント名をとってみる

まずは指定したURLまでお人形ちゃんを移動させましょう。

const puppeteer = require("puppeteer");

//以下のJSコードは全てasync関数の中
const browser = await puppeteer.launch();
const puppeteerPage = await browser.newPage();
await puppeteerPage.goto(見たいページのURL);

本物のブラウザで例えるなら、launchメソッドはブラウザの起動、newPageメソッドは新しいタブの作成、gotoメソッドはURLを入力してENTERを押すこと、という感じです。

例えば、こんなページに移動したとして、

<html>
    <head>
        <!--省略-->
    </head>
    <body>
        <div id="hoge">
            <h1>いぬ</h1>
        </div>
        <div id="fuga">
            <span>ねこ</span>
        </div>
        <div id="abya">
            <p class="alpha">あるふぁー</p>
            <p class="brabo">ぶらぼー</p>
            <p class="charlie">ちゃーりー</p>
        </div>
    </body>
</html>

取ってこさせたい文字列がある部分を、セレクタとかって呼ばれる文字列を作って指定します。
例えば「ねこ」って書いてある部分なら"body > #fuga > span"みたいな感じ。
idではなくclassの場合も"body > #abya > .alpha"みたいな感じで指定できます。

//変数nekoに"ねこ"って入る
let neko = await puppeteerPage.$eval("body > #fuga > span", (element)=>{
    return element.textContent
});

ちなみにtextContentinnerHTMLって書いたりしていい感じにすることもできます。

let hogeHTML = await puppeteerPage.$eval("body > #hoge", (element)=>{
    return element.innerHTML
});

また、$$evalっていうelementの部分がelementの配列になるメソッドもあったりします。
あとはそれぞれのSNSの適当なアカウントのページをChromeで開いて検証してみてみて頑張るだけですね。

なおFacebook等、見た目が同じであるにもかかわらずHTMLの構造がアカウントによって若干違う、ということもあるので注意が必要です。
ついでにFacebookって個人アカウント用のページと団体等用っぽいページがあるんですよね…
個人ページの例:https://www.facebook.com/people/%E9%96%A2%E6%9C%AC%E6%8B%93/100019307564542
団体っぽいページの例:https://www.facebook.com/OguraYuiILove/
まあ基本的には自明に後者の方だと思って扱えばいいんですかね…

そういう感じでやるとアカウント名とアカウントの自己紹介くらいは割となんとか取ってこれます。
ウェブサイトは…もう知らない子ですね。

…puppeteer遅くね?

アカウント名をとってくるこの機能を作って数百個のデータについて処理をさせていくと、私はこうなりました。
処理が次に進まない…永遠に終わらない…ってなるんですよね。
調べてみるとどうやらlaunchする時の設定や、ページを読み込む際の設定を頑張ることでまぁ使える程度の速さにすることができます。

1. launch時の設定
いろいろ調べてみると1launchメソッドの引数にいい感じのオブジェクトを入れると使える速さになるっぽいです。
私は以下のものを使いました。

{
    headless: true,
    args: [
        '--disable-gpu',
        '--disable-dev-shm-usage',
        '--disable-setuid-sandbox',
        '--disable-infobars',
        '--disable-3d-apis',
        '--no-first-run',
        '--no-sandbox',
        '--no-zygote',
        '--single-process'
    ]
}

2. 不要なデータへのリクエストをしない設定にする
ウェブページを読み込む時に、HTMLのデータだけでなくCSSやJavaScript、画像その他などのデータを読み込むリクエストも行われます。しかし私たちが文字列データを取るために必要なのはHTMLのデータだけ。というわけで不要なデータへのリクエストを行わせなければ少しは早く処理を済ませることができます。

puppetPage.setRequestInterception(true);
puppetPage.on("request", (request)=>{
    if(["image", "stylesheet", "font", "script"].indexOf(request.resourceType()) !== -1){
        request.abort();
    }else{
        request.continue();
    }
});

request.resourceType()がHTMLと関係ない、画像やスタイルシートやフォントやスクリプトならリクエストを中止させる、という感じです。

ただしこれではうまくいかない場合もあります。FacebookやInstagramあたりでscriptやstylesheetへのリクエストを中止させるとエラーになる感じでした。
私の場合は4つのSNS全てで"image""font"だけをabortしました。

ついでに以下のようなコードも追加してダイアログとかで止まらないようにするのもしました。

puppeteerPage.on("dialog", (dialog)=>{
    await dialog.dismiss();
});

開こうとしてエラーになるかどうかの判定

このままだと404とかが判定できないんですよね
でもさっきから使っているpuppeteerPageonメソッドを使うとあっさりできます。

puppeteerPage.on("response", (response)=>{
    if(response.status() >= 400){
        //ページを開くとエラーになる場合の処理
    }
});

できなかったこと

このあと、検索機能の実装とかでも用いられているMeCabとtfidfを使って、とってきたアカウント名やアカウントの説明文から団体が申請してきた説明と合致するかどうかを判定する、みたいなことを本当はしたかったのですが、
結局そのあたりのライブラリを私が使えるようになるだけの時間的余裕が無かったので、それに伴ってアカウント名やアカウント説明文を取得する機能は作るだけ作って結局使わずじまいでした。

お祭りはまた発生するので今度は回収できたらいいですね…


  1. GitHub上のディスカッションやらStack Overflowやらかなりいろいろな場所を見てまわった記憶がありますが、結局大事なことはQiitaのこの記事にだいたい集約されていたという…