はじめに
スクレイピングをnode.jsでできるPuppeteer(スペルミス多発)を初心者が使ってみたところ、
私的なハマりどころが複数あったので備忘録として残します。
ハマりポイント(順番に)
- search.jsは2018年に作成されたものなので、一部スクレイピング対象サイトのセレクタが変わっていた。
- 検索窓に入力した際のポップアウトが動的で、developer toolでDOMをelementを見つけるのに苦労した。
- レスポンシブデザインのbreakpointのサイズを考えずに組んでいたので、Headlessで立ち上がっているChromeと挙動の差異があったことに気付いていなかった(Navigation Drawerが出てきてしまっていた)。
解決策
- ちゃんとdeveloper toolでelementを確認する。
- JavaScriptのdebugと同じ要領でBreakpointを設定してポップアウトが出た瞬間に止めて、elementを確認する。
- Headlessモードを解除し、実際にChromeが動いている様子を確認してロジックを組む。
3についてはpuppeteerにより立ち上がるChromeのブラウザサイズを指定することで対応できないかと思ったのですが、
自分が調べた範囲では、スクリーンショットのサイズは変更可能ですが、ブラウザサイズの変更はできないみたいです。
(もしどなたか、やり方を知っていたら教えてください)
###結局のところ
- developer toolsを使って対象サイトの構成をしっかり把握しよう
- Breakpointを駆使しよう(JS書くならデバッグのやり方把握しないとですよね)
- HeadfullでChromeの挙動を確認しよう
対象
- Puppeteerの初心者
- Puppeteerのexampleのsearch.jsを試そうとしたけどそのままでは動かなかった人
私のレベルとしてはDOM操作の一通りの知識があり、バックエンドはnodeとExpressを少し触った程度です。
用語の正確性がボロボロかもしれませんのでよろしければご指摘ください。
開発環境
- puppeteer@3.0.2
- node.js v12.16.1
- npm 6.13.4
- macOS Catalina 10.15.4
一つ一つ見ていきます
Puppeteerの導入については公式のgithubをご覧ください
https://github.com/puppeteer/puppeteer
examplesの実行については以下となります。
Assuming you have a checkout of the Puppeteer repo and have run npm i (or yarn) to install the dependencies, the examples can be run from the root folder like so:
NODE_PATH=../ node examples/search.js
https://github.com/puppeteer/puppeteer/tree/master/examples
こちらからsearch.jsを見てみます。
'use strict';
const puppeteer = require('puppeteer');
(async() => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://developers.google.com/web/');
// Type into search box.
await page.type('#searchbox input', 'Headless Chrome');
// Wait for suggest overlay to appear and click "show all results".
const allResultsSelector = '.devsite-suggest-all-results';
await page.waitForSelector(allResultsSelector);
await page.click(allResultsSelector);
// Wait for the results page to load and display the results.
const resultsSelector = '.gsc-results .gsc-thumbnail-inside a.gs-title';
await page.waitForSelector(resultsSelector);
// Extract the results from the page.
const links = await page.evaluate(resultsSelector => {
const anchors = Array.from(document.querySelectorAll(resultsSelector));
return anchors.map(anchor => {
const title = anchor.textContent.split('|')[0].trim();
return `${title} - ${anchor.href}`;
});
}, resultsSelector);
console.log(links.join('\n'));
await browser.close();
})();
コードの説明は省きますが、コメントアウトで書かれている通りの流れとなっています。
https://developers.google.com/web にアクセスして検索窓に入力し、出てくるポップアウトの中のボタンを押し、遷移したページの結果を取得するというものです。
1. スクレイピング対象サイトのHTML Elementが変わっていた問題への対応
さて、このまま実行すると
UnhandledPromiseRejectionWarning: Error: No node found for selector: #searchbox input
といったエラーが出ます。2018年段階ではsearchboxというIDで選択できたのですが、2020/4/29時点では変更されています。
修正しなければいけない部分は以下になります。検索窓を見つけてpage.type()
で入力する処理です。
'use strict';
const puppeteer = require('puppeteer');
(async() => {
// 省略
// Type into search box.
await page.type('#searchbox input', 'Headless Chrome'); //<- ここ!
//省略
})();
ChromeのDeveloper toolsを開き、検索窓をinspectします。
以下の画像のようになっています。赤い部分が検索窓に対応するHTML Element
です。
ここで注意することとしては、複数のElementと被らないIDやclassをセレクタすることです。
私が試したパターンは以下の三つとなります。
// await page.type('input[name="q"]', 'Headless Chrome'); //OK
// await page.type('.devsite-search-field', 'Headless Chrome'); //OK
await page.type('.devsite-search-query', 'Headless Chrome'); //OK
以上のどれかに変更することで第一関門は突破できました。
この時は以下のコードをpage.type()
の下に記述して確認しておりました。
await console.log('type success')
もしくはスクリーンショットを以下のように撮ることで確認しておりました。
await page.screenshot({path: 'search.png', fullPage: true});
しかし、後述しますが、デバッグはHeadlessを解除して行えば一発です。
##2. 検索窓入力後のポップアウトのHTML Elementの取得
developer toolsで普通にやるとポップアウト部分のHTML Elementを見ようとするとポップアウトが消えてしまいます。
そこで、以下の記事を参考にしてBreak inを設定します。そうすることによって、JavaScriptが中断され、ポップアウトが開いたままになります。
How to Inspect Dynamic HTML Elements (that keep disappearing!) in Chrome
ここでは結果的に、以下のexampleコードのままで良いことが判明しました。
classが2018年と変更なしということです。
'use strict';
const puppeteer = require('puppeteer');
(async() => {
//省略
// Wait for suggest overlay to appear and click "show all results".
const allResultsSelector = '.devsite-suggest-all-results';
await page.waitForSelector(allResultsSelector);
await page.click(allResultsSelector);
//省略
})();
BreakpointsはDOM Breakpoints
から削除しておきます。もしくは更新すれば消えます。
3.Headlessを解除してHeadfullでデバッグを行う
ここまでクリアすれば問題ないと思っていたのですが、以下のエラーが出てきてしまいました。
UnhandledPromiseRejectionWarning: TimeoutError: waiting for selector ".devsite-suggest-all-results" failed: timeout 30000ms exceeded
結果的に解決策としては、puppeteer.launch()
でpuppteer.launch({slowMo:100})
として、一つ一つの実行に100ms挟むか、
もしくは、以下のようにtype
する前の部分で、検索アイコンをクリックして検索窓を開かせることで解決しました。
'use strict';
const puppeteer = require('puppeteer');
(async() => {
// 省略
const searchIconSelector = '.devsite-search-button'
await page.waitForSelector(searchIconSelector);
await page.click(searchIconSelector);
// Type into search box.
await page.type('.devsite-search-query', 'Headless Chrome');
//省略
})();
####重要なのはHeadfull
にして、実際に動いているChromeの挙動を確認することでした。
Headlessを解除してHeadfullにするには以下のようにします。
puppeteer.launch({headless:false})
そうすると、Headfullで立ち上がり、挙動を確認することができます。
このブラウザのサイズは800px × 600pxで固定とのことです。
私が調べた範囲ではスクリーンショットサイズは変更できましたが、ブラウザサイズは変えられませんでした。
レスポンシブデザインの場合は横幅が800pxの状態でどう表示されるか確認する必要があると思います。
また、他の私のハマったところとしては、検索アイコンをクリックして検索窓を開かせる場合に、Navigation Drawerにも適用されているclassを指定してしまったことです。
const searchIconSelector = '.devsite-search-button' //OK
// const searchIconSelector = '.devsite-header-icon-button' // NG
上のNGの方を使うと、以下のようにNavigation Drawerを開いてしまい、入力はできても、サジェストの全結果を開くボタンを押すことができませんでした。
###最終結果は以下のように出力されたら成功です。
Getting Started with Headless Chrome - https://developers.google.com/web/updates/2017/04/headless-chrome
Automated testing with Headless Chrome - https://developers.google.com/web/updates/2017/06/headless-karma-mocha-chai
Headless Chrome: an answer to server-side rendering JS sites - https://developers.google.com/web/tools/puppeteer/articles/ssr
Puppeteer - https://developers.google.com/web/tools/puppeteer
Chrome DevTools Protocol - https://developers.google.com/chrome-developer-tools/docs/debugger-protocol
Examples - https://developers.google.com/web/tools/puppeteer/examples
New in Chrome 59 - https://developers.google.com/web/updates/2017/05/nic59
All Updates tagged: headless - https://developers.google.com/web/updates/tags/headless?hl=ja
Running the examples - https://developers.google.com/web/tools/puppeteer/examples?hl=ja
Troubleshooting - https://developers.google.com/web/tools/puppeteer/troubleshooting?hl=ja
##この後の調査予定
- 他のexampleも試し、詰まりながら仕様を理解していく。
長くなりましたが以上となります。お疲れ様でした。
#参考記事まとめ