Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
419
Help us understand the problem. What is going on with this article?
@tomipetit

puppeteerでスクレイピング

More than 1 year has passed since last update.

web上の情報を抽出するスクレイピング技術ですが、いままでphantomJSで行っていましたが、chromeがヘッドレスブラウザに対応したとのことで、そのnodeライブラリであるpuppeteerで実践してみました。

環境構築

とりあえずお試しということで、dockerで構築しました。
構成はnode.jsのdockerイメージにpuppeteerを追加するかたちです。

下記2サイトの手順を大幅に参考にさせていただきました。
Docker コンテナ上で Puppeteer を動かす
Puppeteer をDockerコンテナで利用する

ディレクトリ構成はこんな感じ。
/
 ├ app/
 │ └ script/
 │  └ app.js
 │ └ data/
 ├ docker-compose.yml
 ├ Dockerfile
 └ Package.json

Dockerfile.
FROM node:9.2.0

RUN apt-get update \
 && apt-get install -y \
      gconf-service \
      libasound2 \
      libatk1.0-0 \
      libc6 \
      libcairo2 \
      libcups2 \
      libdbus-1-3 \
      libexpat1 \
      libfontconfig1 \
      libgcc1 \
      libgconf-2-4 \
      libgdk-pixbuf2.0-0 \
      libglib2.0-0 \
      libgtk-3-0 \
      libnspr4 \
      libpango-1.0-0 \
      libpangocairo-1.0-0 \
      libstdc++6 \
      libx11-6 \
      libx11-xcb1 \
      libxcb1 \
      libxcomposite1 \
      libxcursor1 \
      libxdamage1 \
      libxext6 \
      libxfixes3 \
      libxi6 \
      libxrandr2 \
      libxrender1 \
      libxss1 \
      libxtst6 \
      ca-certificates \
      fonts-liberation \
      libappindicator1 \
      libnss3 \
      lsb-release \
      xdg-utils \
      wget

# node関連設定
WORKDIR /usr/src/app 
COPY     package.json /usr/src/app/
RUN npm install

# スクリプト配置用ディレクトリ作成
RUN mkdir -p /usr/src/app/script

# フォント追加
RUN mkdir /noto

ADD https://noto-website.storage.googleapis.com/pkgs/NotoSansCJKjp-hinted.zip /noto 

WORKDIR /noto

RUN apt-get install -y unzip
RUN unzip NotoSansCJKjp-hinted.zip && \
    mkdir -p /usr/share/fonts/noto && \
    cp *.otf /usr/share/fonts/noto && \
    chmod 644 -R /usr/share/fonts/noto/ && \
    fc-cache -fv

WORKDIR /
RUN rm -rf /noto

ENTRYPOINT ["nodejs","/app/script/app.js"]
package.json
{
    "name": "puppeteer_test",
    "version": "0.1.0",
    "description": "puppeteerの動作テスト",
    "dependencies": {
    "puppeteer"   : "*",
    }
}
docker-compose.yml
version: '2'

services:
    main:
        build: "."
        container_name: "puppeteer_test"
        volumes:
         - "./app/script:/app/script"

puppeteerを使う

基本

/app/script/app.js
const puppeteer = require('puppeteer');

(async() => {
    const browser = await puppeteer.launch({
        args: [
          '--no-sandbox',
          '--disable-setuid-sandbox'
        ]
    });
    const page = await browser.newPage();
    await page.goto('http://example.com'); // 表示したいURL

    /*(何か処理)*/

    browser.close();
})();

(何か処理)のところでページの遷移だったり画像キャプチャだったりを書いていく形になります。

デバイスを指定

puppeteer.DeviceDescriptorsメソッドを最初に実行しておくことでデバイスを指定できます。

/app/script/app.js
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');

(async() => {
  const browser = await puppeteer.launch({
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox'
    ]
  });
  const page = await browser.newPage();
  const iPhone = devices['iPhone 6']; //デバイスはiPhone6を選択

  await page.emulate(iPhone); // デバイス適用
  await page.goto('http://example.com/');

 ...

puppeteerでの処理は大まかに、
ページ全体に対して処理をするpageのメソッド
と、
pageメソッドで抽出したエレメントに対して処理をするelementHandleのメソッド
を使う形に分かれます。

domの選択

domSelect.js
// page.$(セレクタ)
let elm= await page.$('#login_button');

フォーカスを当てる

domSelect.js
// page.focus(セレクタ)
let elm= await page.focus('#login_button');

フォーム入力

テキストはtypeメソッドで入力できます。

formInputSample1.js
// page.type(セレクタ , 値)
await page.type('input[name="id"]', 'abc');
await page.type('input[name="password"]', '123');
もしくはfocusと組み合わせて、
formInputSample2.js
// let element = page.focus(セレクタ)
// element.type(値)
let idElm = await page.focus('input[name="id"]');
await idElm.type('abc');

let passElm = page.focus('input[name="password"]');
await passElm.type('123');

クリック

clickメソッドでクリックできます。

formSubmit1.js
// page.click(セレクタ)
page.click('#login_button');
もしくは$()メソッドで
formSubmit2.js
// page.$(セレクタ)
let buttomElm = page.$('#login_button');
await buttomElm.click();

domの選択(複数)

通常のjavaScriptでいうquerySelectorAllにあたるものです。

domSelect.js
// page.$$(セレクタ)
let items = await page.$$('#list .item');

domの選択(応用)

セレクタ表記のみで書きづらい要素を指定した場合は、組み合わせで対応できます。

pickup.js
// 3番目のitemクラスのなかのnameクラス(nth-childでもいいですが例として。。)
let items = await page.$$('.item');
let pickup = await items[2].$('.name');

要素の抽出

page.evaluateメソッドを使うことで通常のJavaScript記述で処理できるため、そのなかでinnerText等を使って抽出できます。

scraiping.js
const scrapingData = await page.evaluate(() => {
    const dataList = [];
    const nodeList = document.querySelectorAll("td.date");
    nodeList.forEach(_node => {
        dataList.push(_node.innerText);
    })
    return dataList;
});

スクリーンショットを撮る

screenshotメソッドでスクリーンショットを保存できます。簡単。

screenshotPage.js
await page.screenshot({path: 'screenShotPage.png'});

公式ドキュメントによるとdom単位のスクリーンショットもいけるらしい。すごい。。

screenshotDom.js
let element = page.$('#side_nav');
await element.screenshot({path: 'screenShotDom.png'});

レンダリングを待つ

ページやDOMがレンダリングされるタイミングを待つことができます。

wait.js
// ページの遷移待ち
await page.waitForNavigation();

// DOMのレンダリング待ち
await page.waitForSelector('#contents');

サンプル

ここまでの使い方をまとめたようなサンプル。
(2019/02/18 編集) コメントにてログイン処理の不具合をご指摘頂いた内容を反映しました。ありがとうございました。

sample.js
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const fs = require('fs');

(async() => {
    const browser = await puppeteer.launch({
        args: [
          '--no-sandbox',
          '--disable-setuid-sandbox'
        ]
    });
    const page = await browser.newPage();
    const iPhone = devices['iPhone 6']; // 端末はiPhone6を選択

    await page.emulate(iPhone);
    // login
    await page.goto('http://sample-site.com/login'); //sample-site.comへ接続
    await page.screenshot({
        path: 'data/loginPage.png' // スクリーンショットを撮る
    });

    // ログインしているか判定
    const loginChk = await page.evaluate(()=> {
        const node = document.querySelectorAll("#login_button");
        return node.length ? false : true;
    });

    // ログインしていなかったらログイン処理
    if (!loginChk) {
        await page.type('input[name="login_id"]', 'abc');
        await page.type('input[name="login_password"]', '123');

        page.click('#login_button'); // waitForNavigationがタイムアウトになってしまうばあいがあるため、ここではawaitは付けない(コメント参照)

        await page.waitForNavigation({
            waitUntil: 'domcontentloaded'
        });
    }

    // ログイン後ページトップ->メニューから2番目のボタンをクリック
    const menuItems= await page.$$('#menu .item'); 
    const menuItem = await menuItems[1].$('button[type=submit]');
    await menuItem.click();

    await page.waitForNavigation({
        waitUntil: 'domcontentloaded'
    });

    // 一覧ページ
    // コンテンツ抽出
    const scrapingData = await page.evaluate(() => {
        const dataList = [];
        const nodeList = document.querySelectorAll("td.date");
        nodeList.forEach(_node => {
            dataList.push(_node.innerText);
        })
        return dataList;
    });

    //結果をファイルに出力
    fs.writeFile('data/result.txt', JSON.stringify(scrapingData),(err) => {
        if (err) throw err;
       console.log('done');
    });

    browser.close();
})();

公式ドキュメント

ここに記述した諸々は公式ドキュメントを見ていただくとさらに詳しい使い方が載っていますので参考にしてください。
Puppeteer API

419
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
tomipetit
LINKA ASSOCIATES INC. Web Developper,Engineer. Toyohashi Aichi.

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
419
Help us understand the problem. What is going on with this article?