iMind Developers Blog

iMind開発者ブログ

puppeteerでWebの画面操作とスクレイピング

概要

puppeteerを使って下記の項目などを実行する。

  • URLを叩いて結果をスクリーンショットとHTML本文の2パターンで保存する
  • ページにテキストを入力してボタンを押す
  • 当該ページに表示されている画像をまとめて保存する
  • ページ遷移しながらスクレイピングしてデータを保存する

バージョン情報

  • node v10.15.1
  • puppeteer@1.20.0

インストール

$ npm i puppeteer

ページを開いて保存する

URLを指定してwebページを開き、スクリーンショットとHTMLの内容を保存するスクリプト。

const puppeteer = require('puppeteer');
const fs = require("fs");

async function saveContent(url) {
    // puppetterで弊社サイトを開く
    const browser = await puppeteer.launch({headless: false});
    const page = await browser.newPage();
    await page.goto(url);

    // スクリーンショットの保存
    await page.screenshot({ path: 'page1.png' });

    // HTMLの保存
    const html = await page.content();
    fs.writeFileSync("page1.html", html);

    await browser.close();
}

saveContent('https://blog.imind.jp/');

headless: falseを指定するとブラウザが立ち上がって指定されたスクリプトの動作を実演してくれる。デバッグ時にはfalseを指定しておくと便利。

これで当該サイトのHTMLとスクリーンショットが取れる。

サーバーなど環境によっては上記コードを動かすとエラーが出る。

(node:4731) UnhandledPromiseRejectionWarning: Error: Failed to launch chrome!
[0926/081125.586304:FATAL:zygote_host_impl_linux.cc(116)] No usable sandbox! Update your kernel or see https://chromium.googlesource.com/chromium/src/+/master/docs/linux_suid_sandbox_development.md for more information on developing with the SUID sandbox. If you want to live dangerously and need an immediate workaround, you can try using --no-sandbox.

この場合は下記を指定すると動作した。

    const browser = await puppeteer.launch({
        args: ['--no-sandbox', '--disable-setuid-sandbox']})

入力フォームに文字を入力してボタンを押す

続いて画面の操作。

動作がわかりやすいようにslowMoでゆっくり動く指定にする。

const puppeteer = require('puppeteer');

async function saveContent(url) {
    // puppetterで弊社サイトを開く
    // slowMoを指定してゆっくり実演する
    const browser = await puppeteer.launch({
        headless: false, slowMo: 1000});
    const page = await browser.newPage();
    await page.goto(url);

    // 文字を入力
    await page.type('form.search-form input[name=q]', 'スクレイピング', { delay: 100 });

    // 検索ボタンを押して遷移するまで待つ
    await Promise.all([
        page.waitForNavigation(), 
        page.click('form.search-form input[type="submit"]'),
    ]);

    // スクリーンショットの保存
    await page.screenshot({ path: 'page2.png' });

    await browser.close();
}

saveContent('https://blog.imind.jp/');

これを実行するとブラウザが立ち上がり、ゆっくり検索ボックスに「スクレイピング」と入力して検索し、終了する。

画面サイズの指定

上記の処理ではスクリーンショットの画像サイズは800x600になっている。

下記のようにViewportを指定するともっと大きなサイズで画面キャプチャを取れる。

window-sizeも一緒に指定しているが、こちらは立ち上がるWindowのサイズでキャプチャのサイズには影響しない。

const puppeteer = require('puppeteer');
const fs = require("fs");

async function saveContent(url) {
    // window-sizeを指定して
    const browser = await puppeteer.launch({
        headless: false,
        args: ['--window-size=1920,1080']});
    const page = await browser.newPage();

    // width, heightの指定
    await page.setViewport({
        width: 1920,
        height: 1080,
    });
    await page.goto(url);

    // スクリーンショットの保存
    await page.screenshot({ path: 'page3.png' });

    await browser.close();
}

saveContent('https://www.yahoo.co.jp/');

画像の保存

ページに表示されている画像を保存する。

responseイベントに介入して結果が返るたびに都度bufferからデータを取り出しファイル保存している。またbufferのサイズについても調整している。

const puppeteer = require('puppeteer');
const fs = require("fs");
const path = require("path");

const imageDir = 'images'
if (!fs.existsSync(imageDir)) {
    fs.mkdirSync(imageDir);
}

async function saveContent(url) {
    const browser = await puppeteer.launch({
        headless: true});
    const page = await browser.newPage();

    // bufferの拡張
    // https://github.com/GoogleChrome/puppeteer/issues/1274
    await page._client.send('Network.enable', {
          maxResourceBufferSize: 1024 * 1024 * 10, 
          maxTotalBufferSize: 1024 * 1024 * 30, 
    });

    // 取得対象のcontent-type(svgとか他も必要に応じて足すこと)
    const imageTypes = ['image/png', 'image/jpeg', 'image/gif']
    page.on('response', async (resp) => {
        // status codeやcontent-typeを確認して取得対象のみ保存する
        // content-lengthが小さいspacerやタグ的な画像は無視(>=1024)
        if ([200, 201, 304].includes(resp.status())
                && imageTypes.includes(resp['_headers']['content-type'])
                && resp['_headers']['content-length'] >= 1024) {
            // レスポンスからbufferを取得
            let buffer = await resp.buffer();
            // URLからファイル名を取得
            let baseName = path.basename(resp['_url']);
            // images/ファイル名に画像を保存
            // 同一ファイル名がいたら上書きしてしまうので真面目にやる時は要修正
            fs.writeFileSync(imageDir + "/" + baseName, buffer);
        }
    });

    await page.goto(url, { waitUntil: 'networkidle0' });
    await browser.close();
}

saveContent('https://www.yahoo.co.jp/');

これでimagesディレクトリ配下に画像ファイルがわさわさ取れる。

スクレイピングして情報を取得する

Webページから任意の情報を取得する。

例としてYahooニュースの記事タイトルを1〜2ページ目だけ収集する。

const puppeteer = require('puppeteer');
const fs = require("fs");

async function getArticleLinks(page, pageCount, links) {
    // 記事タイトルの収集
    const items = await page.$$('li.yjnSubTopics_list_item a');
    for(const item of items){
        let title = await page.evaluate(e => e.innerText, item);
        let link = await page.evaluate(e => e.getAttribute('href'), item);
        links.push({'title': title, 'link': link});
    }
    // ページ数が指定未満なら次のページを収集
    if(pageCount < 1) {
        await Promise.all([
            page.waitForNavigation(),
            page.click('li.pagination_item-next a'),
        ]);
        await getArticleLinks(page, pageCount + 1, links)
    }
    return links;
}

async function saveArticleLinks(url) {
    // puppetterで弊社サイトを開く
    const browser = await puppeteer.launch({
        headless: true});
    const page = await browser.newPage();
    await page.goto(url);

    // pageをたどりながらタイトルを収集
    let links = await getArticleLinks(page, 0, []);

    // 取得した情報をJSONファイルで出力
    fs.writeFileSync("news.json", JSON.stringify(links, null , "  "));

    await browser.close()
}

saveArticleLinks('https://news.yahoo.co.jp/topics/top-picks')

実行結果

[
  {
    "title": "日産CEO候補 3人に絞り検討\n9/27(金) 1:02",
    "link": "https://news.yahoo.co.jp/pickup/6337736"
  },
  {
    "title": "由規481日ぶり1軍 最速150km\n9/27(金) 0:30",
    "link": "https://news.yahoo.co.jp/pickup/6337735"
  },
  {
    "title": "JDI、再建の行方再び不透明\n9/26(木) 23:51",
    "link": "https://news.yahoo.co.jp/pickup/6337734"
  },


以下略

Yahooニュースでは記事タイトルの上部はHTMLでリンクが貼ってあるが、下部はJavaScriptで表示されている。こうしたリンクでも取得できるのがpuppeteerのいいところ。

改定履歴

Author: Masato Watanabe, Date: 2019-10-03, 記事投稿