shobylogy

叩けシンプルの杖

Pythonでの複雑なページをスクレイピングする(requests-html + pyppeteer)

Pythonでスクレイピングをする場合、requests-htmlが便利ですが、 JSで構築された複雑なページなどをスクレイピングしたい場合、物足りなくなるケースが出てきます。

今回は、requests-htmlが内部で使用しているpyppeteerと組み合わせて、 複雑なWebページをスクレイピングする方法をご紹介します。

requests-htmlとは

requests、pipenvの作者の方が開発したhtmlの取得・parse用のライブラリです。 HTML Parsing for Humans™と銘打っている通り、非常にhuman readableなコードを書くことができます。

基本的な使い方は以下の通りです。

from requests_html import HTMLSession
session = HTMLSession()
r = session.get('https://python.org/')

about = r.html.find('#about', first=True)

requests-htmlは大変便利なライブラリなのですが、JSで構築された複雑なページをスクレイピングする際には力不足になるケースがあります。 その場合は、headless browserを直接使用したスクレイピングを行うことで、対応することができます。

pyppeteerとは

pyppeteerとは、Chromeをheadlessで操作するライブラリであるPuppeteerのPythonラッパーです。 Seleniumと比べて文法がシンプルであり、async/awaitを用いてオシャレに非同期処理が書けることがメリットです。

github.com

基本的な使い方は以下の通りです。

import asyncio
from pyppeteer import launch

async def main():
    browser = await launch()
    page = await browser.newPage()
    await page.goto('http://example.com')
    await page.screenshot({'path': 'example.png'})
    await browser.close()

asyncio.get_event_loop().run_until_complete(main())

スクレイピングに用いる場合の注意点として、20秒以上の通信をする場合、Chromiumとpyppeteerのコネクションが切れてしまうバグが存在するようです。 そのため、以下のコードでpatchを適用しましょう。

# workaround: https://github.com/miyakogi/pyppeteer/pull/160
def patch_pyppeteer():
    import pyppeteer.connection
    original_method = pyppeteer.connection.websockets.client.connect

    def new_method(*args, **kwargs):
        kwargs['ping_interval'] = None
        kwargs['ping_timeout'] = None
        return original_method(*args, **kwargs)

    pyppeteer.connection.websockets.client.connect = new_method
patch_pyppeteer()

また、Jupyter Notebook上で動かす場合、環境によってはasyncioのevent loopがnestできずにエラーが出るケースがあるので、その場合は以下のworkaround用ライブラリを適用してください。

github.com

import nest_asyncio
nest_asyncio.apply()

pyppeteer + requests-html

pyppeteerはあくまでHeadless Chromeの操作ライブラリであるため、単体ではhtmlのparse処理には不向きです。 そのため、requests-htmlと組み合わせ、fetchをpyppeteer、parseをrequests-htmlを用いることで、 複雑なHPのスクレイピングも短いコードで記述することができるようになります。

例は以下の通りです。

from pyppeteer import launch
import asyncio
from requests_html import HTML

async def fetch():        
    browser = await launch(headless=True)
    page = await browser.newPage()
    
    # 対象ページに移動
    url = 'http://example.com'
    
    # ページ遷移して読み込みが終わるまで待つ
    await asyncio.wait([
        page.goto(url),
        page.waitForNavigation(),
    ])

    # contentを取得し、request-htmlのparserで読み込み
    content = await page.content()
    html = HTML(html=content)

    # 取得したいデータのparse
    target = html.find('#target_id', first=True).text
    
    await browser.close()
    
    return target

target = asyncio.get_event_loop().run_until_complete(fetch())

このように、シンプルなコードで複雑なページもスクレイピングすることができます。

まとめ

requests-htmlとpyppeteerを組み合わせることで、複雑なページもシンプルなコードでスクレイピングを行うことができます。