Sansan Builders Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

【Techの道も一歩から】第45回「pyppeteerを使いヘッドレスブラウザでログインして情報を取得する」

f:id:kanjirz50:20190104142720j:plain

こんにちは。 DSOC R&D グループの高橋寛治です。

ログイン後に動的にレンダリングされたページから必要な情報を抽出する、いわゆるスクレイピングを最近行いました。

スクレイピングを行うためにはヘッドレスブラウザ*1の操作が必要です。 pyppeteerを少し踏み込んで使ったため、その備忘録として取り組みを紹介します。

そもそもスクレイピングについては、こちらで書籍の紹介をしていますので、本稿では割愛します。

やりたいことと対象の概要

やりたいことは、あるWebアプリケーション中の所定のページから情報を抽出することです。 前提として、抽出対象となるページのURLは事前にわかっています。

対象とするWebアプリケーションは、ログインが必要です。 抽出したい情報は、抽出対象となるページ中にある「クリップボードにコピーする」ボタンを押下することで、クリップボードに保存されます。

pyppeteer

pyppeteerは、JacaScriptによるヘッドレスブラウザの自動操作ライブラリpuppeteerのPython移植版です。

Python上でヘッドレスブラウザの操作が可能となります。

こちらを用いて、ヘッドレスブラウザを操作し、描画されている項目から情報抽出を行います。

ログイン操作

まずはログインの自動操作を実装します。 本来はログイン結果のCookieセッションを使い回したかったのですが、うまく実装することができませんでした。 今回は、ログイン後にスクレイピングを行う実装となります。

これ以降のサンプルコード中での https://example.com/ を始めとしたURLやHTML要素は、適宜置き換えてください。

import asyncio

from pyppeteer import launch


async def fetch_data(
    urls: list[str],
    login_id: str,
    login_password: str,
    headless: bool = False,
):
    # ブラウザの起動
    browser = await launch(headless=headless)
    page = await browser.newPage()
    # ブラウザの幅と高さは適当に決める
    await page.setViewport({"width": 1280, "height": 960})

    # ログインページに移動
    await page.goto('https://example.com/login')
    # テキストボックスにログイン情報を入力
    await page.type('input[name=login_id]', login_id)
    await page.type('input[name=login_password]', login_password)
    # ログインボタンを押して移動を待つ
    await asyncio.gather(
        page.waitForNavigation({'waitUntil': 'networkidle0'}),
        page.click('button[role=button]')
    )
    # ログイン後に所定のページに移動する
    await page.goto('https://example.com/app')

    # 続く

クリップボードへのアクセス

次にクリップボードへのアクセスの許可です。 2021年12月時点のpyppeteerには実装されていないようです。 直接ブラウザを制御して、権限を取得します。

async def fetch_data(
    ...

    # クリップボードアクセスを許可する
    await browser._connection.send('Browser.grantPermissions', {
        'origin': 'https://example.com/',
        'permissions': ['clipboardRead'],
    })

なお、クリップボードからの情報の取得は次のように行います。

text = await page.evaluate("navigator.clipboard.readText()")

スクレイピング

あらかじめ与えられたURLリストにアクセスし、クリップボードにコピーボタンを押下した上で、クリップボードの内容を取得します。

async def fetch_data(
    ...
    
    fetched_data = []
    for url in urls:
        try:
            await page.goto(url)
            await page.waitForNavigation({'waitUntil': 'networkidle0'})
            await page.waitFor(200)
            # クリックする項目は適宜変更する
            await page.click('button[name=copy-to-clipboard]')
            await page.waitFor(200)
            text = await page.evaluate("navigator.clipboard.readText()")
        except Exception:
            text = ""

        fetched_data.append({
            "url": url,
            "text": text
        })

        # 次の処理の前に待ち時間を入れる
        await page.waitFor(1000 + random.randint(50, 500))

    await browser.close()
    return fetched_data

最後にブラウザを閉じて終了します。 ここでは、情報がうまく取得できない際に空文字列を与えています。

適宜、待ち時間を入れてサーバに優しいスクレイピングを行いましょう。

pyppeteerとgokartを組み合わせてスクレイピング

Pythonのパイプラインフレームワークであるgokart上の一要素として利用できる状態にします。 具体的には、gokartのタスク化を行います。

gokart化しておくことで、バッチとしての利用がしやすくなります。 スクレイピング先のURLリストを取得するタスクをパラメータとすることで、例えば日ごとに異なるパラメータでスクレイピングするということが容易になります。

gokartについては以下の記事で紹介しています。

buildersbox.corp-sansan.com

タスクとしてスクレイピングコードを記述します。

import asyncio
import os

import gokart
import luigi
import pandas as pd
# GokartTaskはgokart.TaskOnKartを継承し、task_namespaceを指定したクラス
from .template import GokartTask


class ScrapingTask(GokartTask):
    data_task = gokart.TaskInstanceParameter()
    login_id_name = luigi.Parameter(default="LOGIN_ID")
    login_password_name = luigi.Parameter(default="LOGIN_PASSWORD")

    def requires(self):
        return self.data_task

    def run(self):
        df = self.load_data_frame()
        urls = list(df["url"])

        fetched_data = asyncio.get_event_loop().run_until_complete(
            fetch_data(
                urls,
                os.environ.get(self.login_id_name),
                os.environ.get(self.login_password_name),
                headless=True,
            )
        )
        df_fetched = pd.DataFrame(fetched_data)

        self.dump(df_fetched)

ログインIDとログインパスワードは、環境変数から取得します。 gokartの設定ファイルとしては、環境変数名を指定します。 これは秘匿情報がログ出力されることを防ぐためです。 gokart実行時にはパラメータがログに出力されますので、これを回避します。

本来、セッションが使い回せるのであれば、セッションを取得するタスクとスクレイピングするタスクで別々にしたいところです。 そうすることで、スクレイピングが途中で失敗しても再開できますし、無駄なログインアクセスも無くなります。

gokartによるスクレイピングバッチは、あくまでパラメータに紐付いてスクレイピングが行われます。 ページの変更を追いかけたい場合は、rerun=Trueのように再実行フラグを立てるか、パラメータを変更する必要があります。

おわりに

pyppeteerを用いてヘッドレスブラウザを操作し、自動ログインおよびクリップボードアクセスを行うことで、Python言語のみで目的の情報が抽出できました。

慣れているPython言語でスクレイピングできることは便利ですし、ブラウザに直接命令を送ることも可能なのでできることは多いです*2

また、本家のpuppeteerをまだ使ったことがないので、試してみたいと思います。

執筆者プロフィール

高橋寛治 Sansan株式会社 DSOC (Data Strategy & Operation Center) R&Dグループ研究員

阿南工業高等専門学校卒業後に、長岡技術科学大学に編入学。同大学大学院電気電子情報工学専攻修了。在学中は、自然言語処理の研究に取り組み、解析ツールの開発や機械翻訳に関連する研究を行う。大学院を卒業後、2017年にSansan株式会社に入社。キーワード抽出など自然言語処理を生かした研究に取り組む。

▼執筆者による連載記事はこちら

buildersbox.corp-sansan.com

*1:GUIのないブラウザのこと。

*2:プルリクエストを出さないとという気持ちになりました。

© Sansan, Inc.