カテゴリー
【puppeteerでスクレイピング】Alpine Dockerコンテナからpuppeteerで動的サイトのスクレイピングを試してみる
※ 当ページには【広告/PR】を含む場合があります。
2020/11/13
銀行口座や証券口座のウェブサイトなどでは認証用のjsリソースなどをログイン時に読み込ませる仕様になっていることが多く、ブラウザが適切にロード完了時に処理をしてクライアント側からの操作を受け付ける仕組みです。
puppeteer登場以前のブラウザなしAjax操作だけでは動的ウェブサイトからのスクレイピングは非常に困難だったのですが、puppeteerを利用することでコマンドラインからでも複雑なスクレイピングがいとも簡単に可能となります。
今回はヘッドレスブラウザから操作するモダンな仕組みのスクレイピング・プログラムをDocker Alpineコンテナで開発環境を整えてみたときの記事になります。
なお、Puppeteerについて詳しく解説してそうな書籍は、『
Alpine Linuxからdockerコンテナを作る
alpine:edge
chromium
FROM alpine:edge
RUN apk update && apk upgrade && \
apk add --no-cache bash openssh expect
#Dependancies of puppeteer in alpine container
RUN apk add --no-cache \
chromium \
nss \
freetype \
freetype-dev \
harfbuzz \
ca-certificates \
ttf-freefont \
nodejs \
nodejs-npm \
yarn
RUN npm i -g typescript @babel/core @babel/node ts-node
RUN addgroup -g 1000 -S pptruser && \
adduser -D -u 1000 -S -G pptruser pptruser
RUN echo '%pptruser ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
CMD ["bash"]
今回はインタラクティブモードでコンテナの中から操作しながら開発を試みるので、ユーザーにリソースを編集する権限が必要です。
そのため後述しますがpuppeteerのオプションも
--no-sandbox
なお今回は
babel-node
次にdocker-compose.ymlを作成し、以下の内容で編集します。
version: '3'
services:
app:
image: puppeteer-alpine:edge
build: .
user: "pptruser:pptruser"
container_name: puppeteer-alpine
environment:
NODE_ENV: "development"
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: "true"
PUPPETEER_EXECUTABLE_PATH: "/usr/bin/chromium-browser"
volumes:
- ./:/usr/src/app
working_dir: "/usr/src/app"
ひとまずはこのコンテナがビルド&インタラクティブモードで起動するかを確認します。
ちなみにプロジェクトのルートディレクトリは
/usr/src/app
早速pupperteer開発環境用のDockerコンテナをビルドして立ち上げてみます。
$ docker-compose build
$ docker-compose run --rm app bash
[node@d4f30154b2f7:/usr/src/app]$
インタラクティブモードに入ったら、このコンテナ内で
npm --init
{
"name": "puppeteer_ajax",
"version": "0.1.0",
"description": "To learn puppetter with docker alpine",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"tap": "babel-node dist/index.js",
"start": "yarn build && yarn tap"
},
"devDependencies": {
"@types/node": "^13.7.1",
"@types/puppeteer": "^5.4.0"
},
"dependencies": {
"puppeteer": "^5.4.1"
}
}
そこから
yarn install
puppeteerを使う(準備編)
この記事ではデフォルトではtypescriptのソースコードを利用します。
まずsrcフォルダを作成し、そこにindex.tsを追加します。
プロジェクトのフォルダ構造(※node_modulesの中は除く)は以下のようになっていると思います。
$ tree -I node_modules
.
├── Dockerfile
├── docker-compose.yml
├── index.ts
├── package.json
├── src
│ └── index.ts
├── tsconfig.json
└── yarn.lock
後付ですが、手元の環境の
tsconfig.json
tsc --init
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"declaration": true,
"sourceMap": true,
"outDir": "./dist",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true
},
"include": [
"./src/index.ts",
],
"exclude": [
"./test/*.spec.ts"
],
"compileOnSave": false
}
さてまず簡単なスクリーンショットを撮って画像を保存してみます。
import puppeteer from 'puppeteer';
(async () => {
try {
const browser = await puppeteer.launch({
executablePath: '/usr/bin/chromium-browser',
args: ['--disable-dev-shm-usage', '--no-sandbox']
});
const page = await browser.newPage();
await page.goto('https://www.google.com/');
await page.screenshot({
path: '/usr/src/app/result.png'
});
await browser.close();
} catch (e) {
console.error(e);
}
})();
編集しおえたら、先程のpackage.jsonの中にあったスクリプトを利用して、ビルド&実行してみます。。
$ yarn start
正常に処理が通るとルートに以下のような画像が保存されていると思います。

見ての通りで、chrome単体で日本語フォントを導入しなかった場合には日本語を含むページは文字化けしてしまいます。
chromeの日本語対応
説明が前後して恐縮ですが、
alpine:edge
FROM alpine:edge
#...中略
RUN apk add --no-cache font-noto-cjk unifont
これらのパッケージを
docker-compose build

ということで、puppeteerの動作確認はこれでOKですので、次は実践的なログインを伴うページを操作してみます。
puppeteerでログイン認証後にページを操作する
あらためまして、Puppeteerは
Puppeteerに関する書籍は少ないですが、手元においておくベスト本としては「
その処理としては主に、ページ全体や抽出したHTML要素を処理したい場合には
page
ではPuppeteerの実力を確認してみましょう。
今回は色んな証券口座のページの中でもログイン時に色んなjsコードのダウンロードと認証用の処理が走るため単純なajaxでは到底不可能で、個人的に諦めていた某M証券のウェブサイトのログインを試してみます。
import puppeteer from 'puppeteer';
(async () => {
try {
const browser = await puppeteer.launch({
executablePath: '/usr/bin/chromium-browser',
args: ['--disable-dev-shm-usage', '--no-sandbox']
});
const page = await browser.newPage();
await page.goto('https://trade.m******.co.jp/mgap/login');
//👇①input要素のname属性を利用したフォームの入力
await page.type('input[name="user"]', 'xxxxxxxx');
await page.type('input[name="password"]', 'xxxxxxxxxxxxx');
//👇②idがsubmit-btnのボタン要素をクリック
page.click('#submit-btn');
//👇③クリック後にページが遷移するまで待機
await page.waitForNavigation({
waitUntil: 'domcontentloaded'
});
await page.screenshot({
path: '/usr/src/app/result.png'
});
await browser.close();
} catch (e) {
console.error(e);
}
})();
項目①が普段はキーボードからタイプしているユーザー名とパスワードです。
入力後に②の部分からヘッドレスchromeをまたいでプログラム的に擬似クリックさせてログインしているので、感覚的な操作ができるところもpuppeteerの凄いところです。
③の部分でクリック後のページの遷移が完了するまで待機した後に、正常に処理されるとログイン状態になったページに移行する、という流れです。
このプログラムを
yarn start

...あれま絶賛読み込み中です。
page.waitForNavigation
waitUntil
今回のページはログイン認証を処理するのに、何回かのページのロードが繰り返し発生しているようです。
そこでコネクション数が0個になった状態から500ミリ秒経過したときを完了とみなす
networkidle0
//...
//👇③クリック後にページが遷移するまで待機
await page.waitForNavigation({
waitUntil: 'networkidle0'
});
//...
すると、難攻不落だったページにもログインが可能となりました。

ついでですが、ログインしたあとのページに存在する任意のDOMが生成された時点を遷移完了とみなす方法も良く利用すると思います。
たとえばログイン後のページによくあるログアウトのボタンやリンクなどのDOMをターゲットに、
page.waitForSelector
たとえば以下のような感じです。
//...
//👇③クリック後にページが遷移するまで待機
await page.waitForSelector('.logout');
//...
しかしながらこの方法も万能ではなくウェブサイトのレンダリング処理方法で変わってしまいます。
今回のサイトではDOM要素を生成したあとにinnerHtmlなどのコンテンツが挿入される形式だったため、この方法はログインに適さなかったようです。

DOMの生成後にvisibleになったり、hiddenされたりする要素を使った処理のあるサイトであったら、
visible: true
hidden: true
//...
//👇③クリック後にページが遷移するまで待機
await page.waitForSelector('.logout', {visible: true});
//...
ただしDOMが全てvisibleの状態になったとしても更に後段のスクリプトが走っている仕様のページもあるので、こちらも必ずしも最良の手段にはならない場合があります。
Puppeteerでスクレイピング
では本題のスクレイピングを行います。
通常puppeteerによるスクレイピングを行う場合、
もちろんpuppeteerでゴリゴリとスクレイピングしても良いのですが、お目当てのページまで遷移できたなら、従来通りjs標準の正規表現のテクニックでhtmlのテキストを捌いていくことも出来ます。
今回は後者の方を採用し、puppeteerでログインした後に正規表現でスクレイピングを行うことを想定して解説してみます。
前節でログイン後に画像をキャプチャしたときのコードを以下のように修正します。
import puppeteer from 'puppeteer';
import { writeFile } from 'fs';
(async () => {
try {
const browser = await puppeteer.launch({
executablePath: '/usr/bin/chromium-browser',
args: ['--disable-dev-shm-usage', '--no-sandbox']
});
const page = await browser.newPage();
await page.goto('https://trade.m******.co.jp/mgap/login');
//👇①input要素のname属性を利用したフォームの入力
await page.type('input[name="user"]', 'xxxxxxxx');
await page.type('input[name="password"]', 'xxxxxxxxxxxxx');
//👇②idがsubmit-btnのボタン要素をクリック
page.click('#submit-btn');
//👇③クリック後にページが遷移するまで待機
await page.waitForNavigation({
waitUntil: 'domcontentloaded'
});
//👇④現在のHTMLをテキストに変換
let bodyHtml = await page.content();
//👇⑤正規表現で生のhtmlを好きなように操作
//例として空白行部分を全て削除し、テキストファイルとして保存
bodyHtml = bodyHtml.replace(/(^\n|^\s*?\n)/gm, '');
console.log(bodyHtml);
writeFile('/usr/src/app/result.txt', bodyHtml, (err) => {
if (err) { throw err }
console.log('Done');
});
await browser.close();
} catch (e) {
console.error(e);
}
})();
前節と違うところは、④で
page.content
このメソッドは文字列として返してくれるので、⑤の部分のように自由に正規表現処理を挿入することができます。
まとめ
以上、puppeteerを利用して動的ウェブサイトからのスクレイピングを行う手順を簡単に解説してみました。
今回スクレイピングに使ったpuppeteerの機能はほんの一部で、アイデア次第でかなり複雑な操作を行うアプリケーションも作成できるようです。
まだpuppeteerを利用したことがないなら是非ともこの機会に一度試してみてはいかがでしょうか。
Puppeteer APIのもっと詳しいコーディング例などは以下の参考サイト内をご参照ください。
参考サイト
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー