どうも、大曲です。
3月から子会社から本社に戻ってきました。
最近では開発より組織の整備!?とか何か良く分かんないコトやっています。
良い意味の何でも屋を目指せるように頑張ります。
今回はpuppeteerを使ったスクレピングが便利だったので紹介します。
やったコト
puppeteerを使って実現した処理は以下の通りです。
- スクレイピング先のaタグのリンク取得
- スクレイピング先で発生したリクエストの取得
全体のコードはGitHubにあります。
ブログでは書き方をいくつかピックアップして紹介します。
https://github.com/oomatomo/puppeteer-api
最近のスクレイピング事情(個人的な感想)
昔:HTMLペライチ(裏でPHPとかHTMLを生成していた)
今:AngularとかReactとかVue.js(ブラウザ側でDOMの構築..etc)
昔はHTMLさえ取得できれば、そこから必要な情報をフィルタリングして取得できました。
しかし、今はフロントエンドの技術向上に伴いブラウザ側でDOM構築するコトが多いため
HTMLを取得後にJavaScriptが実行されないと必要な情報の取得が難しくなってきています。
Puppeteerとは
https://github.com/GoogleChrome/puppeteer
ヘッドレスChromeを使うためのAPIを提供するライブラリです。
面白いなと思ったのが、Chromeの開発ツールのAPIまで用意されている点です。
開発ツールのConsoleやNetWorkまで使えるのはありがたい。
サンプルコードは以下の通りです。楽でっせ。
const puppeteer = require('puppeteer'); (async () => { // ブラウザの起動 const browser = await puppeteer.launch(); const page = await browser.newPage(); // ページへのアクセス await page.goto('https://example.com'); // スクショを取る await page.screenshot({path: 'example.png'}); // リソースの解放 await browser.close(); })();
あとはAPIの戻り値の多くが非同期処理のPromiseが返すので、活用すればエラーの管理も楽だなと思いました。
ScalaでFutureとかに慣れていれば活用できると思います。今回のコードではそんなに活用していません。
API一覧ページ
API化までの流れ
デバイスの指定(UserAgentの変更)
DeviceDescriptors
↑ Chromeでデフォルトで選択できる項目があるので便利です。
// 選択するパターン const devices = require('puppeteer/DeviceDescriptors'); await page.emulate(devices['iPhone X']);
page.setUserAgent
また、UserAgentの文字列を設定する事もできます。
// 全部設定するパターン await page.setUserAgent('Mozilla/5.0 ..')
ブラウザ内に対してJavaScriptを実行する
これはスクレイピングではよくやる処理だと思います。
ブラウザ内の要素を取得したり、色々出来ます。
// Promiseなのでawaitが必要 const contentLinks = await page.evaluate(() => { const links = []; // 普通のJavaScriptの処理:aタグでhrefの要素を取得している const nodes = document.querySelectorAll('a[href]'); nodes.forEach(node => { links.push(node.getAttribute('href')); }) return links; });
ちなみにevaluate
でなくても$eval
でも出来ます。
要素の抽出系では$$eval
、$$
、$
とか色々あるみたいです。(詳しくは調べていないです)
page.$eval
const searchValue = await page.$eval('#search', el => el.value);
リクエストの種類ごとに制御する
実際にブラウザが要求したリクエストの制御をします。
これを行うことで特定のドメインにはリクエストしないや画像などの重いリクエストは無視するなどが出来ます。
リクエストのイベントは3種類あります。
request
, requestfailed
, requestfinished
です。
今回はリクエスト処理前に制御したいのでrequestを使います。
// requestは通常読み込みのみなので書き換えることを許可する設定をします await page.setRequestInterception(true); page.on('request', request => { const reqUrl = request.url(); // example.com以外のリクエストの場合は無視する if (reqUrl.includes('example.com')) { // 実際にリクエストを要求する // 引数のoverrideを使えば、リクエス内容の上書きも可能みたいです // https://github.com/GoogleChrome/puppeteer/blob/v1.5.0/docs/api.md#requestcontinueoverrides request.continue(); } else { // リクエスト結果を勝手に定義する request.respond({ status: 200, body: 'not match domain'}); } });
Expressを使ってAPI化
Node系ではExpressしか使ったことないので、これを使ってAPI化しました。
Expressのリクエストの処理の中で(async () => {})
を呼びました。
正直この書き方があっているか分かりませんが、動いたのでおkにしました。
app.get('/content', function (req, res) { ... const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']}); ... await browser.close(); res.json({ origin: paramUrl.origin, links: contentLinks }); })(); });
Docker化
Dockerで日本語対応のHeadless Chrome + puppeteerを立ち上げ
Docker化はこちらの記事を参考にしました。
システムでの使い分け
Puppeteer + Express はあくまでスクレイピング用のAPIとしてしか使っていません。
Expressの中でPuppeteerの処理の結果をMySQLにデータを登録したりしていません。
直近、作ったシステムではPythonを使うコトが多いのでPythonからAPIを呼び出すようにしています。
ざっくりとした構成は以下の通りです。
まとめ
Puppeteerは使いやすいですので皆さん使ってみてください。
個人的にはpage.waitFor系のメソッドなど気になるメソッドがいくつかあるので引き続き触ってみたいなと思います。
トリガー的な感じでwaitしてくれるのかな〜ワクワク。
page.waitFor..