MENU

【実践】Pythonでスクレイピング|playwrightで攻略

当ページのリンクには広告(PR)が含まれていることがあります。
【実践】Pythonでスクレイピング|playwrightで攻略
お悩み女子

SeleniumじゃなくてPlaywrightを使ってみたいんだけど、使いやすいのかな?

なべくん

Playwrightの情報は少なめですが処理速度も速くておすすめです。

これまで動的なサイトのスクレイピングといえばSelenium一択でしたが、ここ最近ではMicrosoftが提供するPlaywrightが注目されています。

項目Playwrightがおすすめな人Seleniumがおすすめな人
要約これから始める人、モダンで高速な開発をしたい人既存の知見や豊富な情報を活かしたい人
処理速度速い重い
情報量少ない豊富
詳細新規プロジェクトで、速度と安定性を重視し、最新のデバッグ機能(動画録画など)を活用したい場合に最適です。巨大なコミュニティと膨大な日本語情報が魅力。既存のプロジェクトやフレームワークとの連携を重視する場合に堅実な選択肢です。

本記事は、スクレイピングを推奨・助長する目的で執筆しておりませんが、教育目的としてPlaywrightを使ってスクレイピング処理する流れを基本から実践までカバーしています。

Seleniumよりも処理が軽くて速いツールを使ってみたい人やSeleniumでは満足できない人は、ぜひ本記事を参考にしてください。

細かい前置きはいいから早く実践に行きたいという方は以下のリンクから実践のフェーズに移動できます。
>>Playwrightのスクレイピング実践へ移動する

目次

Webスクレイピングの常識を覆すPlaywrightの威力

お困り女子

Playwrightってなんで人気が出てきているの?

なべくん

Seleniumの弱点をほぼカバーする後発ツールだから人気になっているんですよ。

Playwrightの魅力
  • モダンなWeb環境に最適化
  • 主要ブラウザやモバイル対応による安定性
  • SeleniumからPlaywrightへ移行する理由

Playwrightを端的に表現するとSelenium(セレニウム)で実現できなかったスクレイピング処理を解決してくれる後発ツールです。

それぞれの内容について見ていきましょう。

Playwrightが解決するJavaScript駆動サイト(動的サイト)の難しさ

動的サイトは、RequestやScrapyといった従来のスクレイピングツールでは必要なデータが取得できないという「難しさ」があり、解決策としてSeleniumが選ばれていましたがそこに「自動待機」機能を備えたPlaywrightが登場し、新たな選択肢として注目されています。

これまでSeleniumを使用した場合、time.sleep()を使用して明示的にコンテンツの読み込みを待たなくてはなりませんでしたが、Playwrightの場合、特定の要素が表示されたり、読み込まれたりするまでライブラリが自動的に待機します。

お悩み女子

JavaScriptで後から表示されるデータ、Playwrightでもちゃんと取れるのですか?

なべくん

Playwrightの自動待機機能なら、JavaScriptで遅延表示される要素も確実に捕捉できます

このようにPlaywrightは、JavaScriptによって動的に生成されるコンテンツを確実に捉えるため、ユーザー側が複雑な待機処理を手動で記述する必要が減り、スクレイピングの安定性が飛躍的に向上します。

主要ブラウザやモバイル対応による安定性

PlaywrightとSeleinumの大きな違いの一つとして、単一API対応というブラウザごとにコードを書き分けなくても共通の一つのコード記述で操作できるにあります。

項目PlayWrightSelenium
主要ブラウザChromium
Firefox
Safari
Chromium
Firefox
Safari
モバイル対応 標準機能として内蔵し、iPhoneやPixelなどのデバイスを非常に高い精度でエミュレート可能。機能はあるが限定的なエミュレートは可能だがPlaywrightほど手軽で高精度ではない。
特徴エンジンベースでの対応。ほぼ全てのモダンブラウザ環境を再現できるが、レガシーブラウザーは非対応。ブラウザ製品ごとに対応。WebDriverを介して操作するためレガシーブラウザーまで対応。
互換性ほぼ発生しない頻繁に発生しうる

Playwrightを使えば、上記の表の通りに主要ブラウザだけでなく、モバイルへの対応も高精度なエミュレートができ、ブラウザ間の挙動の違いに悩まされることが少なくなりますよ。

レガシーブラウザーの対応を除けば、Playwright自身のライブラリとブラウザエンジンをセットで管理しているため、バージョン不整合によるトラブルが極めて少なく安定したスクレイピング環境を構築できます。

なべくん

バージョン地獄に苦しまなくていいのが神ってます。

SeleniumからPlaywrightへ移行する理由

Seleniumは長年、Webブラウザ自動化ツールの定番でしたが、最近のWebサイトはJavaScriptを多用した複雑な構成が多く、データ収集が不安定になったり、タイムアウトエラーが頻発したりする場面が増えています。

一方、Playwrightは現在のモダンなWeb環境に合わせて開発されたツールだから、Seleniumが実装していないテスト実行の動画自動録画・詳細な操作ログなどデバッグ機能が非常に豊富。

特にCodegenという非常に強力な開発支援ツールがあるため、開発者ツールを開いて要素を…としなくても直感的に要素の取得などができ、爆速でコードの骨格が作れます

お悩み女子

Seleniumは作り込んだサイトだとデータ収集が不安定になりがち。

なべくん

Playwrightは現在のWeb環境に最適化されているため、安定してデータを取得できます。

Playwrightは、Seleniumの後発ツールとしてJavaScriptがふんだんに使われた動的サイトの特性を考慮して設計されているため、情報量を除けばこれからWebスクレイピングを始めるならPlaywrightがおすすめです。

Playwrightで始めるスクレイピングの基本

お悩み女子

Playwrightって難しいのかな?

なべくん

Seleniumよりも簡単に導入~運用できますよ。

Playwright導入の流れ
  1. Playwrightの環境構築
  2. 使用できるコマンドの確認
  3. サンプルコードの確認
  4. よくあるトラブルシューティング

Playwright導入~運用の流れをそれぞれ見ていきましょう。

Python環境へのPlaywright導入手順(ローカル環境)

お悩み女子

Pythonの環境構築っていつも複雑なんだよね。

なべくん

Playwrightは、わずか2つのコマンドで導入が完了するので安心してください!

# ターミナルまたはコマンドプロンプトで実行
pip install playwright
# Chromium, Firefox, WebKitの3つまとめてインストール
playwright install

Playwrightは、Pythonのパッケージ管理システムであるpipを使ってたったの2ステップで終わるので、簡単に環境構築が完了します。

特定のブラウザだけインストールしたいときは、以下のコマンドを使用してください。

# Chromiumだけをインストール
playwright install chromium

# Firefoxだけをインストール
playwright install firefox

# WebKit (Safariのエンジン) だけをインストール
playwright install webkit
スマホをエミュレートする方法
#====================================================================
# iPhone 13 Proとしてアクセスする例
#====================================================================

import asyncio
from playwright.async_api import async_playwright, expect

async def main():
    async with async_playwright() as p:
        # p.devicesリストからエミュレートしたいデバイスを選ぶ
        iphone_13_pro = p.devices["iPhone 13 Pro"]

        browser = await p.webkit.launch(headless=False)
        
        # デバイス情報をコンテキストに設定
        context = await browser.new_context(**iphone_13_pro)
        
        page = await context.new_page()

        # この時点で、このpageはiPhone 13 Proとして振る舞う
        await page.goto("https://www.yahoo.co.jp")

        print(f"User Agent: {await page.evaluate('navigator.userAgent')}")
        print(f"Viewport Size: {page.viewport_size}")
        
        # スクリーンショットを撮るとiPhoneの画面サイズで保存される
        await page.screenshot(path="screenshot_iphone.png")

        await browser.close()

if __name__ == "__main__":
    asyncio.run(main())

#====================================================================
# エミュレート可能なデバイス一覧の確認方法
# 'iPhone 13', 'Pixel 5', 'Galaxy S9+', 'iPad Pro 11'などが表示されます。
#====================================================================

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    # p.devices辞書のキー(デバイス名)をすべて表示
    print("===== 利用可能なデバイス一覧 =====")
    for device_name in p.devices:
        print(device_name)

次の項目では、Playwrightで使用できるコマンドについて見ていきましょう。

Playwrightで使用できるおもなコマンドと記述例

PythonのPlaywrightで使用できる主要なコマンド(メソッド)について、以下の表にまとめました。

分類メソッド名 / 機能Python記述例効果・目的
起動・終了launch()browser = await p.chromium.launch(headless=False)ブラウザ(Chromium, Firefox, WebKit)を起動する。
起動・終了new_context()context = await browser.new_context()新しいブラウザコンテキスト(独立したセッション)を作成する。
起動・終了new_page()page = await context.new_page()新しいページ(タブ)を開く。
ページ操作goto()await page.goto(“https://example.com”)指定したURLに移動する。
要素の選択locator()button = page.locator(“button.btn-primary”)要素を特定するためのロケータオブジェクトを作成する。
要素の選択getByRole()
getByText()など
login_button = page.get_by_role(“button”, name=”ログイン”)
element = page.get_by_text(“ようこそ”)
ユーザーの視点に近い方法で要素を特定する(役割、表示テキストなど)。
アクションclick()await page.get_by_role(“button”).click()要素をクリックする。
アクションfill()await page.get_by_label(“名前”).fill(“山田 太郎”)入力フィールドにテキストを入力する。input()より推奨。
アクションscreenshot()await page.screenshot(path=”screenshot.png”, full_page=True)ページのスクリーンショットを撮る。
待機処理自動待機
(Auto-Waiting)
(メソッドに内蔵)click, fill, textContentなど多くのアクションが、対象要素が適切な状態になるまで自動で待機する。
待機処理wait_for_selector()await page.wait_for_selector(“#dynamic-content”)指定したセレクタの要素が出現するまで待機する。
待機処理wait_for_load_state()await page.wait_for_load_state(“networkidle”)ネットワーク通信が落ち着くまで待機する。(”domcontentloaded”なども指定可)
ネットワークroute()await page.route(“**/images/*.jpg”, lambda route: route.abort())特定のネットワークリクエストを傍受して処理(中断、改変など)する。
ネットワークwait_for_response()async with page.expect_response(“**/api/data”) as response_info:
await page.get_by_text(“更新”).click()
特定のアクション(クリックなど)によって発生するAPIレスポンスを待機する。
デバッグcodegenplaywright codegen https://example.comブラウザ操作を記録し、自動でコードを生成する。
デバッグtracingawait context.tracing.start(screenshots=True, snapshots=True)
await context.tracing.stop(path=”trace.zip”)
実行した全操作の詳細な記録(動画、DOMスナップショット、コンソールログ等)を1つのファイルに保存する。

Playwrightは強力な非同期処理が強みなのでawaitasyncといった記述が目立ちますが、Seleniumで使用される一般的な同期処理コードも使えます。

サンプルコードについては次の項目で見ていきましょう。

Playwrightでスクレイピングするサンプルコード

PlaywrightはモダンなブラウザーツールですがSeleniumと比較して、以下の特徴を持っています。

  1. 非同期処理
  2. 強力な待機機能
  3. 高度なデバッグ・ネットワーク機能

以下にyahooにアクセスしてクリックするなどの基本的な動作を盛り込んだサンプルコードは以下のとおりです。

from playwright.sync_api import sync_playwright
import time

def run():
    with sync_playwright() as p:
        # 1. ブラウザを起動 (Chromiumを使用)
        # headless=Falseにすると、ブラウザの動きを実際に見ることができます。
        browser = p.chromium.launch(headless=False)
        page = browser.new_page()

        # 2. URLにアクセス
        print("Yahoo! JAPANにアクセスします...")
        page.goto("https://www.yahoo.co.jp/")

        # 3. 任意の場所をクリック (「ニュース」のリンクをクリック)
        # get_by_roleを使うと、人間が見つけるのと同じように要素を探せて安定します。
        print("「ニュース」のリンクをクリックします...")
        page.get_by_role("link", name="ニュース").click()
        
        # ページ遷移を待つ (通常は自動で待機しますが、念のため)
        page.wait_for_load_state("domcontentloaded")
        print(f"現在のページタイトル: {page.title()}")

        # 4. テキストを入力 (検索バーに「Playwright」と入力)
        # placeholderテキストを指定して検索バーを特定
        print("検索バーに「Playwright」と入力します...")
        search_bar = page.get_by_placeholder("トピックス、キーワードで検索")
        search_bar.fill("Playwright")
        
        # 見やすいように少し待機
        time.sleep(5) 
        
        # Enterキーを押して検索を実行
        search_bar.press("Enter")
        page.wait_for_load_state("domcontentloaded")
        print("検索を実行しました。")

        # 5. ページ最下部までスクロール
        print("ページの最下部までスクロールします...")
        # JavaScriptを直接実行してスクロールさせる確実な方法
        page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
        
        # スクロールしたことが分かるように少し待機
        time.sleep(5)
        
        # 6. 結果のスクリーンショットを撮る
        screenshot_path = "result_screenshot_sync.png"
        page.screenshot(path=screenshot_path, full_page=True)
        print(f"'{screenshot_path}' にスクリーンショットを保存しました。")

        # 7. ブラウザを閉じる
        browser.close()

# スクリプトを実行
run()

Playwright公式ドキュメントでは非同期(Async) APIを中心に紹介しているのは、パフォーマンスと拡張性の高さを重視しているためです。しかし、個人の学習や小規模なツールであれば、可読性の高い同期処理でも問題ありません。

Python Playwrightのトラブルシューティング例

Playwrightは非常に安定したツールですが、実際の多様な操作を含むWebスクレイピングの過程で予期せぬエラーに遭遇することがあります。

お悩み女子

Playwrightはどんなエラーに遭遇しやすいの?

なべくん

Playwrightでは、同期的処理を非同期処理で実装すると読み込みエラーが頻発しています。

特にWebサイトの構造変更や読み込みのタイミングによるエラーは頻繁に発生しがちです。

Playwrightは公式ドキュメントが非常に充実しており、問題解決の糸口が見つからない場合は、まず公式ドキュメントを参照することをおすすめします。
参考:Playwrightの公式ドキュメント

筆者が遭遇したエラー
  • 例外処理の未実装
    よくエラーが出ている部分を細分化し、例外処理を実装する
  • 同期的処理の実装
    同期的処理を想定している非同期処理を同期処理に修正する
  • 長時間実行によるメモリリーク(解放漏れ)
    並列処理やバックグラウンドブラウザをこまめに閉じる

エラーの数だけ技術は上達するので、トライアンドエラーを恐れずにPlaywrightをガシガシ使い込んでいきましょう。

Playwrightを使ってPython公式サイトをスクレイピングしてみる

なべくん

実際にPlaywrightを使ってスクレイピングしてみましょう。

喜ぶ女子

待ってました。

サンプルサイト
  • サイト名:Python公式サイト
  • サイトURL:https://www.python.org/
  • 利用規約:https://www.python.org/about/legal/
  • robots.txt:https://www.python.org/robots.txt

Seleniumのときと同様にPythonの公式サイトをサンプルとして使わせていただき、実際にスクレイピングをしてみましょう。
参考:【実践】SeleniumでPythonスクレイピング!サンプルコードあり

  1. robots.txt・利用規約を確認する
  2. 取得する要素を確認する
  3. スクリプトを書き、検証する

各スクレイピング手順について、それぞれ見ていきましょう。

実践手順1. robots.txt・利用規約を確認する

まずは、スクレイピングが可能かどうか利用規約robots.txtを確認します。

# Directions for robots.  See this URL:
# http://www.robotstxt.org/robotstxt.html
# for a description of the file format.

User-agent: HTTrack
User-agent: puf
User-agent: MSIECrawler
Disallow: /

# The Krugle web crawler (though based on Nutch) is OK.
User-agent: Krugle
Allow: /
Disallow: /~guido/orlijn/
Disallow: /webstats/

# No one should be crawling us with Nutch.
User-agent: Nutch
Disallow: /

# Hide old versions of the documentation and various large sets of files.
User-agent: *
Disallow: /~guido/orlijn/
Disallow: /webstats/

以上のrobots.txtと利用規約から以下のことが読み取れます。

  • /~guido/orlijn/ と /webstats/ はスクレイピング禁止
  • 以下の名前を持つクローラーはすべてのページへアクセス拒否
    HTTrack, puf, MSIECrawler, Nutch
  • 利用規約に明示的なスクレイピング禁止は記載されていない

特定のページを除けば、常識の範囲内でスクレイピングができることが確認できます。

実践手順2. 取得する要素を確認する

今回はPythonでPlaywrightを使用してスクレイピングするということなので、Pythonの公式サイトから投稿されている記事のタイトルと投稿日を取得していく流れは、以下のとおりです。

取得の流れ
  • Python公式サイトにアクセスする
  • Newsのナビメニューをクリックする
  • 新着記事一覧からmoreをクリックする
  • 記事一覧の投稿日と記事タイトルを取得する
  • CSVに保存してダウンロードする

要素を確認する際に注意しておきたいのがPCとスマホやモバイルなどの端末による見え方やページ遷移先が異なる場合があります。(今回はその典型例)

お悩み女子

いちいち開発者ツールを閉じるの面倒くさい。

なべくん

PCとモバイルで取得要素の名称自体が変わることもあるので、面倒でも開発者ツールを閉じましょう。

ローカル環境の場合、Codegenを使えば開発者ツールを開閉したりせずに要素の確認からコードの生成までおこなえます。

実践手順3. Pythonスクリプトを書き、検証する

今回は、ウェブスクレイピングをすることが目的であるため、開発環境に関しては言及しません。したがって、誰でもほぼ同じ動作環境が再現できるGoogle Colaboratory(通称:Colab)でコードの実装を行います。

Python公式にアクセスして新着ニュースを取得するコードは下記の通り。

# ==============================================================================
# セクション1: 環境構築
# ==============================================================================
import logging
import os
import sys

for handler in logging.root.handlers[:]:
    logging.root.removeHandler(handler)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

IS_COLAB = "google.colab" in sys.modules

try:
    if IS_COLAB:
        logging.info(">>> Google Colab環境を検出しました。Playwrightの環境構築を開始します。")
        get_ipython().system('pip install playwright pandas beautifulsoup4 lxml -q')
        get_ipython().system('playwright install chromium')
        logging.info(">>> 環境構築が正常に完了しました。")
    else:
        logging.info(">>> ローカル環境とみなし、環境構築をスキップします。")
except Exception as e:
    logging.error(f"--- 環境構築中にエラーが発生しました: {e} ---")
    raise SystemExit("環境構築に失敗したため、処理を中断します。")


# ==============================================================================
# セクション2: ライブラリのインポート
# ==============================================================================
import time
import asyncio
import pandas as pd
from bs4 import BeautifulSoup
from datetime import datetime
from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeoutError

if IS_COLAB:
    from google.colab import files


# ==============================================================================
# セクション3: メイン処理
# ==============================================================================

async def main():
    async with async_playwright() as p:
        browser = None
        page = None
        try:
            browser = await p.chromium.launch(headless=True, args=["--no-sandbox", "--disable-dev-shm-usage"])
            context = await browser.new_context(
                user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36',
                viewport={'width': 1920, 'height': 1080}
            )
            page = await context.new_page()
            logging.info("Playwrightのセットアップが完了しました。")

            await page.goto("https://www.python.org/", timeout=60000)
            logging.info(f"'{await page.title()}' にアクセスしました。")

            await page.locator("#news > a").click(force=True)
            await page.wait_for_url("**/blogs/")
            logging.info(f"ニュース一覧ページ ({page.url}) に遷移しました。")

            await page.locator('xpath=//*[@id="content"]/div/section/div/div[1]/div/p/a').click()
            await page.wait_for_url(lambda url: "blog.python.org" in url or "pythoninsider.blogspot.com" in url)
            logging.info(f"ブログサイト ({page.url}) に正常に遷移しました。")

            is_mobile_site = 'pythoninsider.blogspot.com' in page.url
            if is_mobile_site:
                logging.info(">>> モバイル版ブログサイトのスクレイピングを開始します。")
            else:
                logging.info(">>> PC版ブログサイトのスクレイピングを開始します。")

            news_data = []
            MAX_PAGES = 10

            for i in range(MAX_PAGES):
                logging.info(f"--- ページ {i + 1} のデータを取得します ---")
                await page.wait_for_selector(".blog-posts")
                html = await page.content()
                soup = BeautifulSoup(html, 'lxml')

                page_articles_found = 0

                def format_date(date_str):
                    try:
                        date_obj = datetime.strptime(date_str, "%A, %B %d, %Y")
                        return date_obj.strftime("%Y-%m-%d")
                    except ValueError:
                        return date_str

                post_containers = soup.select(".blog-posts > div.date-outer")
                for container in post_containers:
                    date_tag = container.select_one("h2.date-header span")
                    date_str = date_tag.get_text(strip=True) if date_tag else "日付不明"
                    formatted_date = format_date(date_str)
                    articles = container.select("div.post-outer")
                    for article in articles:
                        title_tag = article.select_one("h3.post-title a")
                        if title_tag:
                            title = title_tag.get_text(strip=True)
                            if title and not any(d['タイトル'] == title for d in news_data):
                                # logging.info(f"  >> 発見: [投稿日: {formatted_date}] [タイトル: {title}]")
                                news_data.append({'投稿日': formatted_date, 'タイトル': title})
                                page_articles_found += 1
                
                logging.info(f"このページから新たに {page_articles_found} 件の記事を取得しました。")
                if page_articles_found == 0 and i > 0:
                    logging.info("新たな記事がなかったため、最終ページと判断し、処理を終了します。")
                    break

                if i < MAX_PAGES - 1:
                    try:
                        await page.evaluate('window.scrollTo(0, document.body.scrollHeight)')
                        await page.wait_for_timeout(1000)

                        next_button_locator_str = "#blog-pager-older-link" if is_mobile_site else "#Blog1_blog-pager-older-link"
                        next_button = page.locator(next_button_locator_str)

                        if await next_button.count() > 0:
                            await next_button.click()
                            logging.info(f"次のページ({i + 2}ページ目)へ遷移します...")
                            await page.wait_for_load_state('domcontentloaded')
                        else:
                            logging.info("「Older Posts」ボタンがDOM内に見つかりませんでした。ここで取得を終了します。")
                            break
                    except PlaywrightTimeoutError:
                        logging.info("「Older Posts」ボタンの操作でタイムアウトしました。ここで取得を終了します。")
                        break
            
            logging.info(f"合計 {len(news_data)} 件のニュースを取得しました。")
            if news_data:
                df = pd.DataFrame(news_data, columns=['投稿日', 'タイトル'])
                csv_filename = 'python_blog_news_playwright.csv'
                df.to_csv(csv_filename, index=False, encoding='utf-8-sig')
                logging.info(f"取得したデータを'{csv_filename}'に保存しました。")
                if IS_COLAB:
                    files.download(csv_filename)
            else:
                logging.warning("取得できたニュースがなかったため、CSVファイルは作成されませんでした。")

        except Exception as e:
            logging.error(f"処理中に予期せぬエラーが発生しました: {e}", exc_info=True)
            if page:
                error_filename = 'error_screenshot_playwright.png'
                await page.screenshot(path=error_filename)
                if IS_COLAB:
                    files.download(error_filename)
        finally:
            logging.info("処理が終了しました。ブラウザは自動的に閉じられます。")

# asyncioを使ってmain関数を実行
import nest_asyncio
nest_asyncio.apply()
asyncio.run(main())
なべくん

awaitasyncの使い方が重要です。

参考までに以下のリンクにGoogle Colabのサンプルコードを置いておくので参考にしてください。
参考:Google Colab

おまけ:Playwrightでスクレイピング処理を実用する

おまけ編では以下のスクレイピング処理を実施します。

検索順位をスクレイピングしてみる
  1. 任意のキーワードでYahooの検索結果にアクセスする
  2. 1ページ目から5ページ目までの順位を取得する
  3. 以下の要素を取得する
    記事タイトル・記事URL・記事のディスクリプションPAA・関連キーワード
  4. CSVに保存しダウンロードする

最初の構成ではYahooのトップページから検索キーワードを入力して検索ボタンをクリックする流れでしたが、ボット回避がうまくいかずうまく処理できなかったため、検索結果に直接アクセスする方法に変更しました。

# ==============================================================================
# セクション1: 環境構築
# ==============================================================================
import logging
import os
import sys

# 既存のログハンドラをクリア
for handler in logging.root.handlers[:]:
    logging.root.removeHandler(handler)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

IS_COLAB = "google.colab" in sys.modules

try:
    if IS_COLAB:
        logging.info(">>> Google Colab環境を検出しました。Playwrightの環境構築を開始します。")
        get_ipython().system('apt-get update -qq && apt-get install -y fonts-ipafont-gothic -qq')
        get_ipython().system('pip install playwright pandas -q')
        get_ipython().system('playwright install chromium')
        logging.info(">>> 環境構築が正常に完了しました。")
    else:
        logging.info(">>> ローカル環境とみなし、環境構築をスキップします。")
except Exception as e:
    logging.error(f"--- 環境構築中にエラーが発生しました: {e} ---")
    raise SystemExit("環境構築に失敗したため、処理を中断します。")


# ==============================================================================
# セクション2: ライブラリのインポート
# ==============================================================================
import asyncio
import pandas as pd
from playwright.async_api import async_playwright
from urllib.parse import quote_plus

if IS_COLAB:
    from google.colab import files

# ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
# ★ 設定項目 ★
SEARCH_KEYWORD = "ここに検索したいキーワード"
MAX_PAGES_TO_SCRAPE = 5 # 取得したいページ数
# ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★


# ==============================================================================
# セクション3: メイン処理
# ==============================================================================

async def main():
    async with async_playwright() as p:
        browser = None
        page = None
        try:
            browser_args = ["--no-sandbox", "--disable-dev-shm-usage", "--disable-blink-features=AutomationControlled"]
            browser = await p.chromium.launch(headless=True, args=browser_args)
            context = await browser.new_context(
                user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
                viewport={'width': 1920, 'height': 1080},
                locale='ja-JP'
            )
            page = await context.new_page()
            logging.info("Playwrightのセットアップが完了しました。")

            encoded_keyword = quote_plus(SEARCH_KEYWORD)
            start_url = f"https://search.yahoo.co.jp/search?p={encoded_keyword}"
            logging.info(f"検索結果ページに直接アクセスします: {start_url}")
            await page.goto(start_url, wait_until="domcontentloaded", timeout=60000)

            logging.info("ポップアップや同意画面が表示されていないか確認し、処理します...")
            try:
                later_button = page.locator('button:has-text("後で")')
                if await later_button.is_visible(timeout=5000):
                    await later_button.click()
                    logging.info("「アドレスバーを設定」ポップアップの「後で」ボタンをクリックしました。")
                    await later_button.wait_for(state="hidden", timeout=5000)
                    logging.info("「アドレスバーを設定」ポップアップが閉じられたことを確認しました。")

                agree_button = page.locator('button:has-text("同意して閉じる")')
                if await agree_button.is_visible(timeout=3000):
                    await agree_button.click()
                    logging.info("クッキー同意ボタンをクリックしました。")
                    await agree_button.wait_for(state="hidden", timeout=5000)
                    logging.info("クッキー同意画面が閉じられたことを確認しました。")

            except Exception as e:
                logging.warning(f"ポップアップ処理中に軽微なエラー(またはポップアップ無)が発生しましたが、処理を続行します: {e}")

            all_search_results = []
            
            for page_num in range(1, MAX_PAGES_TO_SCRAPE + 1):
                logging.info(f"--- {page_num} ページ目のデータを取得します ---")

                await page.wait_for_selector("#contents", timeout=30000)
                
                #  page.title()が不安定な場合があるため、エラーが発生しても処理を続行する
                try:
                    page_title = await page.title()
                    logging.info(f"検索結果ページ '{page_title}' の読み込み完了。")
                except Exception as e:
                    logging.warning(f"ページのタイトル取得に失敗しましたが、処理を続行します: {e}")
                
                await page.wait_for_timeout(1000) # 動的コンテンツの読み込みを少し待つ

                result_items = await page.locator("div.sw-Card.Algo").all()

                # このページに検索結果がない場合はループを抜ける
                if not result_items:
                    logging.info("このページに検索結果が見つかりませんでした。取得を終了します。")
                    break

                logging.info(f"ページ内の検索結果(div.sw-Card.Algo)を{len(result_items)}件見つけました。")

                rank_offset = len(all_search_results)
                for i, item in enumerate(result_items, 1):
                    rank = rank_offset + i
                    link_loc = item.locator("a:has(h3)").first
                    title_loc = link_loc.locator("h3")
                    desc_loc = item.locator("p.sw-Card__summary")
                    
                    if await title_loc.count() == 0:
                        continue

                    title = await title_loc.inner_text()
                    url = await link_loc.get_attribute("href")
                    description = await desc_loc.inner_text() if await desc_loc.count() > 0 else ""
                    
                    logging.info(f"  [順位 {rank}] {title}")
                    all_search_results.append({"順位": rank, "タイトル": title, "URL": url, "ディスクリプション": description})

                logging.info(f"--- {page_num} ページ目から {len(result_items)} 件の記事を取得しました ---")

                # 次のページへ遷移するか、ループを終了するかを判断
                if page_num < MAX_PAGES_TO_SCRAPE:
                    next_button = page.locator('.Pagenation__next a:has-text("次へ")')
                    if await next_button.count() > 0:
                        logging.info("「次へ」ボタンをクリックして、次のページへ遷移します...")
                        await next_button.click()
                        await page.wait_for_load_state("domcontentloaded", timeout=60000)
                    else:
                        # 「次へ」ボタンがない場合は最終ページなのでループを抜ける
                        logging.info("「次へ」ボタンが見つかりませんでした。最終ページと判断し、取得を終了します。")
                        break
                else:
                    logging.info(f"指定された {MAX_PAGES_TO_SCRAPE} ページの取得が完了したため、ループを終了します。")
            
            logging.info(f"--- 合計 {len(all_search_results)} 件の検索結果を取得しました ---")
            
            related_keywords = []
            paa_questions = []
            try:
                logging.info("--- 関連キーワードとPAAの取得を試みます ---")
                # 関連検索ワードのセレクタ
                related_keywords_loc = page.locator('div.Unit--south li.SouthUnitItem a') 
                paa_loc = page.locator('div.AnswerRelatedQuestions .sw-Accordion__title')
                
                if await related_keywords_loc.count() > 0:
                    related_keywords = [await loc.inner_text() for loc in await related_keywords_loc.all()]
                    logging.info(f"関連キーワード: {related_keywords}")
                else:
                    logging.warning("関連キーワードのセクションが見つかりませんでした。")
                
                if await paa_loc.count() > 0:
                    paa_questions = await paa_loc.all_inner_texts()
                    logging.info(f"PAA: {paa_questions}")
                else:
                    logging.warning("PAAのセクションが見つかりませんでした。")
            except Exception as e:
                logging.warning(f"関連情報(キーワード/PAA)の取得中に軽微なエラーが発生しました: {e}")

            if all_search_results:
                df_results = pd.DataFrame(all_search_results)
                filename_results = f"yahoo_search_results_{SEARCH_KEYWORD.replace(' ', '_')}.csv"
                df_results.to_csv(filename_results, index=False, encoding='utf-8-sig')
                logging.info(f"検索結果を'{filename_results}'に保存しました。")
                if IS_COLAB: files.download(filename_results)
            
            if related_keywords:
                df_related = pd.DataFrame(related_keywords, columns=["関連キーワード"])
                filename_related = f"yahoo_related_keywords_{SEARCH_KEYWORD.replace(' ', '_')}.csv"
                df_related.to_csv(filename_related, index=False, encoding='utf-8-sig')
                logging.info(f"関連キーワードを'{filename_related}'に保存しました。")
                if IS_COLAB: files.download(filename_related)

            if paa_questions:
                df_paa = pd.DataFrame(paa_questions, columns=["他の人はこちらも質問"])
                filename_paa = f"yahoo_paa_{SEARCH_KEYWORD.replace(' ', '_')}.csv"
                df_paa.to_csv(filename_paa, index=False, encoding='utf-8-sig')
                logging.info(f"PAAを'{filename_paa}'に保存しました。")
                if IS_COLAB: files.download(filename_paa)

        except Exception as e:
            logging.error(f"処理中に予期せぬエラーが発生しました: {e}", exc_info=True)
            if page:
                error_filename = f"error_screenshot_{SEARCH_KEYWORD.replace(' ', '_')}.png"
                await page.screenshot(path=error_filename, full_page=True)
                logging.info(f"エラー発生時のスクリーンショットを '{error_filename}' として保存しました。")
                if IS_COLAB:
                    try:
                        files.download(error_filename)
                        html_filename = f"error_page_{SEARCH_KEYWORD.replace(' ', '_')}.html"
                        with open(html_filename, 'w', encoding='utf-8') as f:
                            f.write(await page.content())
                        logging.info(f"エラー発生時のHTMLを '{html_filename}' として保存しました。")
                        files.download(html_filename)
                    except Exception as download_error:
                        logging.error(f"エラーファイルのダウンロード中に問題が発生しました: {download_error}")
        finally:
            if browser: await browser.close()
            logging.info("処理が終了しました。")

# asyncioを使ってmain関数を実行
import nest_asyncio
nest_asyncio.apply()
asyncio.run(main())
なべくん

ぜひ研究材料として活用してください。

参考までに以下のリンクにGoogle Colabのサンプルコードを置いておくので参考にしてください。
参考:Google Colab

よくある質問(FAQ)

Playwrightは初心者でも簡単に学習できますか?

はい、公式ドキュメントも分かりやすく、具体的なコード例が豊富に提供されており、コードを自動生成する「Codegen(コードジェン)」機能を使えば、ブラウザでの操作がそのままコードになるので、手書きの量を大幅に減らしながら実践的に学べます。

SeleniumからPlaywrightへの移行を考えています。具体的なメリットを教えてください。

SeleniumからPlaywrightへの移行には、主に以下のメリットがあります。

  • モダンなWebサイトへの対応: JavaScriptによって動的にコンテンツが読み込まれるサイトでも、安定して要素を特定しデータを取得できます。
  • 強力な自動待機機能: 要素が表示可能になるまでPlaywrightが自動的に待機するため、手動で待機処理を設定する手間が大幅に削減され、スクレイピングの安定性が向上します。
  • 単一APIによる主要ブラウザ対応: Chromium、Firefox、WebKit(Safariのエンジン)といった主要ブラウザすべてを、共通のPythonコードで操作できます。これにより、ブラウザごとの互換性に悩まされることがなくなります。
  • 高い開発効率: Codegen機能でコードを自動生成でき、非同期処理にも最適化されているため、スクリプトの作成時間を大幅に短縮できます。
Playwrightの「自動待機機能」は、具体的にどのような場面で役立ちますか?

Playwrightの自動待機機能は、Webスクレイピングにおける多くの「要素が見つからない」「タイムアウト」といった問題を解決し、以下の場面で役立ちます。

  • JavaScriptで遅延して表示されるコンテンツ: Webページにアクセスした後、JavaScriptが実行されて初めて表示される商品リストやコメント、詳細情報などを確実に捕捉できます。
  • 非同期で読み込まれる要素: ページの一部分が非同期的に更新される場合でも、Playwrightは要素が実際に存在し、操作可能な状態になるまで自動で待ちます。
  • ユーザーアクション後の画面変化: ボタンクリックやフォーム送信後に画面の一部が変化したり、新しい要素が出現したりする場合でも、手動でtime.sleep()などを挿入する必要がありません。
スクレイピングしたデータをファイルに保存する方法もPlaywrightでできますか?

Pythonの標準機能と組み合わせることでCSVやJSONファイルとして保存できます。

PlaywrightでWebスクレイピングをする際、プロキシ設定は可能ですか?

はい、PlaywrightではWebスクレイピングを行う際にプロキシ設定が可能です。

プロキシを利用することでスクレイピング元のサーバーからIPアドレスのブロックを回避したり、地域制限のあるコンテンツにアクセスしたりできる場合があります。

PlaywrightのCodegen機能は、生成されたコードをそのまま使えるのでしょうか?調整は必要ですか?

PlaywrightのCodegen機能は大半がそのまま使用可能ですが、以下の場合は調整が必要です。

  • 複雑な条件分岐: ログイン後のリダイレクト先が複数ある場合や、特定の条件に応じて異なる操作が必要な場合。
  • 繰り返し処理: 複数のページを巡回して同じ情報を収集する場合など、ループ処理が必要です。
  • 高度なデータ抽出: ページ内に多数の要素があり、特定の規則性を持つデータだけを抽出したい場合など。
  • エラーハンドリング: 予期せぬポップアップやエラーページが表示された場合の対応。

まとめ:Playwrightでスクレイピングを効率化しよう

これまでにPython環境へのPlaywright導入から具体的な操作方法、サンプルコードまでを解説してきました。

Playwrightのまとめ
  • 主要ブラウザやモバイル対応で多様な環境を再現できるツール
  • 非同期処理で効率的かつ高速にスクレイピングできる
  • 自動待機機能やネットワーク操作など高機能

Playwrightは「速度」「安定性」「開発体験」の3つの点でSeleniumを大きく上回るモダンなツールです。

喜ぶ女子

Selenium以外の選択肢があるのは良いですね。

なべくん

モダン・高機能・高速と三拍子揃ったツールです。

項目Playwrightがおすすめな人Seleniumがおすすめな人
要約これから始める人、モダンで高速な開発をしたい人既存の知見や豊富な情報を活かしたい人
処理速度速い重い
情報量少ない豊富
詳細新規プロジェクトで、速度と安定性を重視し、最新のデバッグ機能(動画録画など)を活用したい場合に最適です。巨大なコミュニティと膨大な日本語情報が魅力。既存のプロジェクトやフレームワークとの連携を重視する場合に堅実な選択肢です。

参考情報が豊富なSeleniumも使いやすいツールですが、Playwrightは後発ツールながらモダンなWebサイト(動的サイト)の自動化においては、PlaywrightがSeleniumよりもはるかに効率的で安定したスクレイピング成果が得られますよ。

この記事を書いた人

Watanabeのアバター Watanabe サイト運営者

2020年よりブログ開始。
SEOが思いのほか性にあっていたようで現在に至る。
モットーは「勝率の高い選択をする」
AIは活用するが吉、最後は人間が息を吹き込む。
アートと科学を追求し、日々精進。
―――
収益:6~7桁をウゴウゴ。
サイト:ペラサイト~中規模サイトまで運営中。
案件:1000円以上の案件をメインに取組中。
打ち手:ブラックSEO~ホワイトSEOまで
―――

目次