新しいことにはウェルカム

技術 | 電子工作 | ガジェット | ゲーム のメモ書き

Selenium・puppeteer・Playwrightで無限スクロールする

ここしばらく、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で返すことにより、domJSHandleを取得します。

JSHandleはJavaScriptに渡せますので、前回のdomとの比較が行えるようになります。

Element.scrollTopを使った場合

参考までに、PlaywrightでElement.scrollTopを使った例を記載します。

Selenium・puppeteerの場合も、同様な作業になります。

  • Element.scrollTopは、ウィンドウがコンテンツの上からどの位置にあるかを示します。また、Element.scrollTopに値を代入すると、その位置にウィンドウを移動してくれます。
  • Element.clientHeightはウィンドウの高さを表し、Element.scrollHeightはコンテンツの高さを表します。
  • なので、一番下にスクロールするにはElement.scrollTopElement.scrollHeight - Element.clientHeightとします。
  • そして、ウィンドウが一番下までスクロールしているかは、Element.scrollTop + Element.clientHeightElement.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()を使った方が直感的なので好きですね。

関連カテゴリー記事

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com