ここしばらく、Selenium・puppeteer・Playwrightの使い方をまとめていました。
まとめが長くなってきたので、ある程度の説明や手順が必要な操作は別記事にまとめようと思います。
ここでは、Selenium・puppeteer・Playwrightそれぞれで、無限スクロールする方法をまとめました。
スクロール
無限スクロールの手順に入る前に、まずは基本となるスクロールをする方法です。
JavaScriptのElement.scrollTop
を使って、ウィンドウのdom
のスクロール位置を移動させる方法もあるのですが、ここではElement.scrollIntoView()
を使って、ウィンドウ内の一番下のdom
が見えるようにスクロールさせる方法を使います。
参考として最後にElement.scrollTop
を使った方法も記載しました。
sample html
ウィンドウの中に、記事が同じ形式で並んでいるようなページを想定しています。
一番下の要素に対してElement.scrollIntoView()
を実行します。
<style> .scroll{ width:200px; height: 200px; overflow:scroll; } .in{ height:100px; } </style> <div class="scroll"> <div class="in">test1</div> <div class="in">test2</div> <div class="in">test3</div> </div>
Selenium
ウィンドウ内の1番下の要素を取得してwebelement.location_once_scrolled_into_view
を参照すると、その要素が見えるまでスクロールしてくれます。
webelement.location_once_scrolled_into_view
はプロパティなので参照するだけで大丈夫です。
内部的には要素に対してElement.scrollIntoView()
を呼び出しています。
driver.find_element(By.CSS_SELECTOR, 'div.in:last-child') \ .location_once_scrolled_into_view
puppeteer
JavaScriptでElement.scrollIntoView()
を呼び出します。
let selector = 'div.in:last-child'; await page.waitForSelector(selector); await page.$eval(selector, (dom) => { dom.scrollIntoView() });
Playwright
スクロール用の関数locator..scrollIntoViewIfNeeded()
が用意されているのでそれを使います。
内部的には要素に対してElement.scrollIntoViewIfNeeded()
を呼び出しています。
await page.locator('div.in:last-child').scrollIntoViewIfNeeded();
無限スクロール
よくある、スクロールするとその下に更に要素が追加されていくタイプのやつです。
基本的には上記のスクロールをループさせるのですが、要素が無限ではなく有限の場合もあるので、一番下の要素が前回と同じかをチェックして、同じならばループをストップします。
要素が同じかの比較は、JavaScriptのdom
比較演算子===
で判定します。
Selenium
prevElem = None while True: elem = driver.find_element(By.CSS_SELECTOR, "div.in:last-child") bSame = driver.execute_script( "return arguments[0]===arguments[1]", elem, prevElem) if bSame: break elem.location_once_scrolled_into_view prevElem = elem time.sleep(1)
puppeteer
import { setTimeout } from 'timers/promises'; let prevElem; while (true) { const selector = 'div.in:last-child'; const elem = await page.waitForSelector(selector); const bSame = await page.evaluate((_elem, _prevElem) => { return _elem === _prevElem; }, elem, prevElem); if (bSame) { break; } await page.evaluate((dom) => { dom.scrollIntoView() }, elem); prevElem = elem; await setTimeout(1000); }
Playwright
import { setTimeout } from 'timers/promises'; let prevElem; while (true) { const selector = 'div.in:last-child'; const elem = await page.locator(selector).evaluateHandle((dom: Element) => dom); const bSame = await page.evaluate((arg) => { return arg[0] === arg[1]; }, [elem, prevElem]); if (bSame) { break; } await page.locator(selector).scrollIntoViewIfNeeded(); prevElem = elem; await setTimeout(1000); }
Playwrightは注意が必要です。
同じ要素かをJavaScriptでチェックするのですが、locator
はJavaScriptに渡せません。
そこで、locator.evaluateHandle()
でlocator
が指すdom
をそのままreturn
で返すことにより、dom
のJSHandle
を取得します。
JSHandle
はJavaScriptに渡せますので、前回のdom
との比較が行えるようになります。
Element.scrollTop
を使った場合
参考までに、PlaywrightでElement.scrollTop
を使った例を記載します。
Selenium・puppeteerの場合も、同様な作業になります。
Element.scrollTop
は、ウィンドウがコンテンツの上からどの位置にあるかを示します。また、Element.scrollTop
に値を代入すると、その位置にウィンドウを移動してくれます。Element.clientHeight
はウィンドウの高さを表し、Element.scrollHeight
はコンテンツの高さを表します。- なので、一番下にスクロールするには
Element.scrollTop
をElement.scrollHeight - Element.clientHeight
とします。 - そして、ウィンドウが一番下までスクロールしているかは、
Element.scrollTop + Element.clientHeight
がElement.scrollHeight
に等しいかで判断します。
文だけだとややこしいので、イメージにすると下図のようになります。
import { setTimeout } from 'timers/promises'; let bContinue = true; while (bContinue) { await page.locator('div.scroll').evaluate(dom => { dom.scrollTop = dom.scrollHeight - dom.clientHeight; }); await setTimeout(1000); bContinue = await page.locator('div.scroll').evaluate(dom => { return dom.scrollTop + dom.clientHeight < dom.scrollHeight; }); }
感想など
要素が動的に生成されるので、スクロールした後に次の要素が現れるまでウエイトを入れる必要があります。
Element.scrollTop
を使った方が簡潔に書けるのですが、Element.scrollIntoView()
を使った方が直感的なので好きですね。