ウェブスクレイピングを高速化する方法:完全ガイド

高度な技術でウェブスクレイピングプロセスを最適化し、データ取得を高速化する方法を学びましょう。
10 分読
How to Make Web Scraping Faster

この記事では以下の内容を確認できます:

  • ウェブスクレイピング処理が遅くなる主な原因
  • ウェブスクレイピングを高速化する様々な手法
  • データ取得を高速化するPythonスクレイピングスクリプトの最適化方法

それでは始めましょう!

スクレイピング処理が遅くなる理由

ウェブスクレイピング処理が遅くなる主な原因を探ります。

理由 #1: サーバー応答の遅延

ウェブスクレイピングの速度に影響を与える最も顕著な要因の一つは、サーバーの応答時間です。ウェブサイトにリクエストを送信すると、サーバーはそのリクエストを処理して応答します。サーバーの応答が遅い場合、リクエストの完了に時間がかかります。サーバーが遅くなる原因としては、トラフィックの集中、リソースの制限、ネットワークの遅延などが考えられます。

残念ながら、対象サーバーの速度を上げることはほとんど不可能です。これは制御不能な領域です。ただし、遅延の原因が自サイトからの過剰なリクエストにある場合は別です。この場合、リクエスト間にランダムな遅延を追加し、リクエストを長期間に分散させてください。

理由 #2: CPU処理速度の低下

CPU処理速度は、スクレイピングスクリプトの実行速度に大きく影響します。スクリプトを順次実行する場合、CPUは各操作を1つずつ処理するため時間がかかります。これは特に、スクリプトが複雑な計算やデータ変換を含む場合に顕著です。

さらに、HTMLパースには時間がかかり、スクレイピングプロセスを大幅に遅延させる可能性があります。詳細はHTMLウェブスクレイピングに関する記事をご覧ください。

理由 #3: 制限された入出力操作

入出力(I/O)操作は、スクレイピング作業のボトルネックになりやすいものです。特に、対象サイトが複数のページで構成されている場合に顕著です。スクリプトが外部リソースからの応答を待機してから処理を続行するように設計されている場合、大幅な遅延が生じる可能性があります。

リクエストを送信し、サーバーの応答を待ち、それを処理してから次のリクエストに移るという方法は、ウェブスクレイピングを効率的に行う方法とは言えません。

その他の理由

ウェブスクレイピングスクリプトの速度低下を引き起こすその他の要因は以下の通りです:

  • 非効率なコード:最適化されていないスクレイピングロジックはプロセス全体を遅くします。非効率なデータ構造、不要なループ、過剰なロギングを避けてください。
  • レート制限:対象サイトが一定時間内にユーザーが送信できるリクエスト数を制限している場合、自動スクレイパーの速度は低下します。解決策は?プロキシサービスの利用です
  • CAPTCHAやその他の反スクレイピング対策:CAPTCHAやボット対策は、ユーザー操作を要求することでスクレイピングプロセスを中断させます。他の反スクレイピング技術を探求しましょう。

ウェブスクレイピングを高速化するテクニック

このセクションでは、ウェブスクレイピングを高速化する最も一般的な手法を紹介します。基本的なPythonスクレイピングスクリプトから始め、様々な最適化がそのスクリプトに与える影響を実証します。

:ここで紹介する技術は、あらゆるプログラミング言語や技術で機能します。Pythonを使用するのは、簡潔さと、ウェブスクレイピングに最適なプログラミング言語の一つであるためです。

初期のPythonスクレイピングスクリプトは以下の通りです:

import requests
from bs4 import BeautifulSoup
import csv
import time

def scrape_quotes_to_scrape():
    # スクレイピング対象ページのURL配列
    urls = [
        "http://quotes.toscrape.com/",
        "https://quotes.toscrape.com/page/2/",
        "https://quotes.toscrape.com/page/3/",
        "https://quotes.toscrape.com/page/4/",
        "https://quotes.toscrape.com/page/5/",
        "https://quotes.toscrape.com/page/6/",
        "https://quotes.toscrape.com/page/7/",
        "https://quotes.toscrape.com/page/8/",
        "https://quotes.toscrape.com/page/9/",
        "https://quotes.toscrape.com/page/10/"
    ]

    # スクレイピングしたデータの保存先
    quotes = []

    # ページを順次スクレイピング
    for url in urls:
        print(f"ページ '{url}' をスクレイピング中")

        # GETリクエストを送信してページHTMLを取得
        response = requests.get(url)
        # BeautifulSoupでページHTMLを解析
        soup = BeautifulSoup(response.content, "html.parser")

        # ページ上の引用要素をすべて選択
        quote_html_elements = soup.select(".quote")

        # 引用要素を反復処理し内容をスクレイピング
        for quote_html_element in quote_html_elements:
            # 引用文のテキストを抽出
            text = quote_html_element.select_one(".text").get_text()
            # 引用文の作者を抽出
            author = quote_html_element.select_one(".author").get_text()
            # 引用文に関連付けられたタグを抽出
            tags = [tag.get_text() for tag in quote_html_element.select(".tag")]

            # 新しい引用文オブジェクトを作成しリストに追加
            quote = {
                "text": text,
                "author": author,
                "tags": ", ".join(tags)
            }
            quotes.append(quote)

        print(f"ページ '{url}' のスクレイピングに成功しましたn")

    print("スクレイピングしたデータをCSVにエクスポート中")

    # スクレイピングした引用をCSVファイルにエクスポート
    with open("quotes.csv", "w", newline="", encoding="utf-8") as csvfile:
        fieldnames = ["text", "author", "tags"]
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)

        writer.writeheader()
        writer.writerows(quotes)

    print("引用文をCSVにエクスポートしましたn")

# 実行時間の計測
start_time = time.time()
scrape_quotes_to_scrape()
end_time = time.time()

execution_time = end_time - start_time
print(f"実行時間: {execution_time:.2f} 秒")

上記のスクレイパーは、Quotes to Scrape ウェブサイトの10ページ分のURLを対象としています。各URLに対して、スクリプトは以下の操作を実行します:

  1. requests を使用して GET リクエストを送信し、ページの HTML を取得します。
  2. BeautifulSoupでHTMLコンテンツをパース。
  3. ページ上の各引用文要素から、引用文テキスト、著者、タグを抽出します。
  4. スクレイピングしたデータを辞書リストに格納します。

最後に、取得したデータをquotes.csvという名前のCSVファイルにエクスポートします。

スクリプトを実行するには、必要なライブラリを以下でインストールします:

pip install requests beautifulsoup4

scrape_quotes_to_scrape()関数の呼び出しは、スクレイピング処理の所要時間を計測するためtime.time()で囲まれています。当環境では、初期スクリプトの実行に約4.51秒を要しました。

スクリプトの実行により、プロジェクトフォルダ内にquotes.csv ファイルが生成されます。さらに、以下のようなログが表示されます:

Scraping page: 'http://quotes.toscrape.com/'
Page 'http://quotes.toscrape.com/' scraped successfully

Scraping page: 'https://quotes.toscrape.com/page/2/'
Page 'https://quotes.toscrape.com/page/2/' scraped successfully

ページ 'https://quotes.toscrape.com/page/3/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/3/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/4/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/4/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/5/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/5/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/6/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/6/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/7/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/7/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/8/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/8/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/9/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/9/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/10/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/10/' のスクレイピングに成功しました

スクレイピングしたデータをCSVにエクスポート中
引用文をCSVにエクスポートしました

実行時間: 4.63秒

この出力は、スクリプトが「Quotes to Scrape」から各ページネーションされたウェブページを順次スクレイピングしていることを明確に示しています。これからご覧いただくように、いくつかの最適化によりこのプロセスの流れと速度は大きく変化します。

それでは、ウェブスクレイピングを高速化する方法を探ってみましょう!

1. 高速なHTMLパースライブラリを使用する

データパースには時間とリソースがかかり、HTMLパーサーによってその手法は異なります。豊富な機能と自己記述型APIを提供するパーサーもあれば、パフォーマンスを優先するパーサーもあります。詳細は、最適なHTMLパーサーに関するガイドをご覧ください。

PythonではBeautiful Soupが最も人気のあるHTMLパーサーですが、必ずしも最速とは限りません。ベンチマーク結果を参照すると背景がわかります

実際、Beautiful Soupは異なる基盤パーサーをラッピングする役割しか果たしていません。初期化時に第二引数で利用するパーサーを指定できます:

soup = BeautifulSoup(response.content, "html.parser")

一般的に、Beautiful SoupはPython標準ライブラリの組み込みパーサーであるhtml.parserと組み合わせて使用されます。ただし、速度を重視する場合はlxmlを検討すべきです。これはC言語実装を基盤としているため、Pythonで利用可能な最速のHTMLパーサーの一つです。

lxmlをインストールするには、次のコマンドを実行します:

pip install lxml

インストール後、Beautiful Soupと組み合わせて以下のように使用できます:

soup = BeautifulSoup(response.content, "lxml")

これでPythonスクレイピングスクリプトを再実行すると、以下の出力が確認できるはずです:

# 簡略化のため省略...

実行時間: 4.35 秒

実行時間が4.61秒から4.35秒に短縮されました。この変更は小さく見えるかもしれませんが、この最適化の効果は、パース対象のHTMLページのサイズや複雑さ、選択される要素の数に大きく依存します。

この例では、対象サイトのページはシンプルで短く、浅いDOM構造を持っています。それでも、わずかなコード変更で約6%の速度向上を達成できたことは、十分に価値のある成果です!

👍 長所:

  • Beautiful Soupでの実装が容易

👎 デメリット:

  • 効果が小さい
  • 複雑なDOM構造を持つページでは機能しない
  • 高速なHTMLパーサーはより複雑なAPIを持つ可能性がある

2. マルチプロセッシングスクレイピングの実装

マルチプロセッシングとは、プログラムが複数のプロセスを生成する並列実行手法です。各プロセスはCPUコア上で並列かつ独立して動作し、タスクを順次ではなく同時に実行します。

この手法は、ウェブスクレイピングのようなI/Oに制約される操作に特に有効です。その理由は、主なボトルネックがウェブサーバーからの応答待ち時間であることが多いからです。複数のプロセスを利用することで、複数のページに同時にリクエストを送信でき、スクレイピング全体の時間を短縮できます。

スクレイピングスクリプトをマルチプロセッシングに対応させるには、実行ロジックに重要な変更を加える必要があります。以下の手順に従い、Pythonスクレイパーを順次処理からマルチプロセッシング方式へ変換してください。

Pythonでマルチプロセッシングを始めるには、まずmultiprocessingモジュール からPool とcpu_countをインポートします:

from multiprocessing import Pool, cpu_count

Poolはワーカープロセスのプール管理に必要な機能を提供します。一方cpu_countは、並列処理に利用可能なCPUコア数を特定するのに役立ちます。

次に、単一URLをスクレイピングするロジックを関数内に分離します:

def scrape_page(url):
    print(f"Scraping page: '{url}'")

    response = requests.get(url)
    soup = BeautifulSoup(response.content, "html.parser")
    quote_html_elements = soup.select(".quote")

    quotes = []
    for quote_html_element in quote_html_elements:
        text = quote_html_element.select_one(".text").get_text()
        author = quote_html_element.select_one(".author").get_text()
        tags = [tag.get_text() for tag in quote_html_element.select(".tag")]
        quotes.append({
            "text": text,
            "author": author,
            "tags": ", ".join(tags)
        })

    print(f"ページ '{url}' のスクレイピングに成功しましたn")

    return quotes

上記の関数は各ワーカープロセスによって呼び出され、CPUコア上で順次実行されます。

次に、順次スクレイピングのフローをマルチプロセッシングロジックに置き換えます:

def scrape_quotes():
    urls = [
        "http://quotes.toscrape.com/",
        "https://quotes.toscrape.com/page/2/",
        "https://quotes.toscrape.com/page/3/",
        "https://quotes.toscrape.com/page/4/",
        "https://quotes.toscrape.com/page/5/",
        "https://quotes.toscrape.com/page/6/",
        "https://quotes.toscrape.com/page/7/",
        "https://quotes.toscrape.com/page/8/",
        "https://quotes.toscrape.com/page/9/",
        "https://quotes.toscrape.com/page/10/"
    ]

    # プロセスプールを作成
    with Pool(processes=cpu_count()) as pool:
        results = pool.map(scrape_page, urls)

    # 結果リストをフラット化
    quotes = [quote for sublist in results for quote in sublist]

    print("スクレイピングしたデータをCSVにエクスポート中")

    with open("quotes_multiprocessing.csv", "w", newline="", encoding="utf-8") as csvfile:
        fieldnames = ["text", "author", "tags"]
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(quotes)

    print("引用をCSVにエクスポートしましたn")

最後に、実行時間を計測しながらscrape_quotes()関数を実行します:

if __name__ == "__main__":
    start_time = time.time()
    scrape_quotes()
    end_time = time.time()

    execution_time = end_time - start_time
    print(f"実行時間: {execution_time:.2f} 秒")

if __name__ == "__main__":構文は、モジュールインポート時に特定の部分が実行されないようにするために必須です。このチェックがないと、特にWindows環境で、multiprocessingモジュールが予期せぬ動作を引き起こす可能性のある新規プロセスを生成しようとする場合があります。

すべてをまとめると次のようになります:

from multiprocessing import Pool, cpu_count
import requests
from bs4 import BeautifulSoup
import csv
import time

def scrape_page(url):
    print(f"Scraping page: '{url}'")

    response = requests.get(url)
    soup = BeautifulSoup(response.content, "html.parser")
    quote_html_elements = soup.select(".quote")

    quotes = []
    for quote_html_element in quote_html_elements:
        text = quote_html_element.select_one(".text").get_text()
        author = quote_html_element.select_one(".author").get_text()
        tags = [tag.get_text() for tag in quote_html_element.select(".tag")]
        quotes.append({
            "text": text,
            "author": author,
            "tags": ", ".join(tags)
        })

    print(f"ページ '{url}' のスクレイピングに成功しましたn")

    return quotes

def scrape_quotes():
    urls = [
        "http://quotes.toscrape.com/",
        "https://quotes.toscrape.com/page/2/",
        "https://quotes.toscrape.com/page/3/",
        "https://quotes.toscrape.com/page/4/",
        "https://quotes.toscrape.com/page/5/",
        "https://quotes.toscrape.com/page/6/",
        "https://quotes.toscrape.com/page/7/",
        "https://quotes.toscrape.com/page/8/",
        "https://quotes.toscrape.com/page/9/",
        "https://quotes.toscrape.com/page/10/"
    ]

    # プロセスプールを作成
    with Pool(processes=cpu_count()) as pool:
        results = pool.map(scrape_page, urls)

    # 結果リストをフラット化
    quotes = [quote for sublist in results for quote in sublist]

    print("スクレイピングしたデータをCSVにエクスポート中")

    with open("quotes_multiprocessing.csv", "w", newline="", encoding="utf-8") as csvfile:
        fieldnames = ["text", "author", "tags"]
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(quotes)

    print("引用をCSVにエクスポートしました")

if __name__ == "__main__":
    start_time = time.time()
    scrape_quotes()
    end_time = time.time()

    execution_time = end_time - start_time
    print(f"実行時間: {execution_time:.2f} 秒")

スクリプトを再度実行します。今回は以下のようなログが生成されます:

ページをスクレイピング中: 'http://quotes.toscrape.com/'
ページをスクレイピング中: 'https://quotes.toscrape.com/page/2/'
ページをスクレイピング中: 'https://quotes.toscrape.com/page/3/'
ページをスクレイピング中: 'https://quotes.toscrape.com/page/4/'
ページをスクレイピング中: 'https://quotes.toscrape.com/page/5/'
ページをスクレイピング中: 'https://quotes.toscrape.com/page/6/'
ページ 'https://quotes.toscrape.com/page/7/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/8/' をスクレイピング中
ページ 'http://quotes.toscrape.com/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/9/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/3/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/10/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/4/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/5/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/6/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/7/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/2/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/8/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/9/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/10/' のスクレイピングに成功しました

スクレイピングしたデータをCSVにエクスポート中
引用文をCSVにエクスポートしました

実行時間: 1.87秒

ご覧の通り、実行順序はもはや順次ではありません。スクリプトは複数のページを同時にスクレイピングできるようになりました。具体的には、CPUで利用可能なコア数(本例では8)までスクレイピング可能です。

並列処理により約145%の時間短縮が実現され、実行時間が4.61秒から1.87秒に短縮されました。これは素晴らしい成果です!

👍 長所:

  • 大幅な実行時間短縮
  • ほとんどのプログラミング言語でネイティブサポート

👎 デメリット:

  • 利用可能なコア数に制限される
  • URLリスト内の順序を尊重しない
  • コードに大幅な変更が必要

3. マルチスレッドスクレイピングの実装

マルチスレッドとは、単一プロセス内で複数のスレッドを同時に実行するプログラミング技術です。これにより、スクリプトは複数のタスクを同時に実行でき、各タスクは専用のスレッドで処理されます。

マルチプロセッシングと似ていますが、マルチスレッドは必ずしも複数のCPUコアを必要としません。これは、単一のCPUコアが同じメモリ空間を共有しながら多数のスレッドを同時に実行できるためです。この概念については、並行処理と並列処理の比較ガイドで詳しく解説しています。

スクレイピングスクリプトを順次処理からマルチスレッド化するには、前章で説明した変更と同様の作業が必要であることを留意してください。

本実装ではPythonのconcurrent.futuresモジュールからThreadPoolExecutorを使用します。以下の通りインポート可能です:

from concurrent.futures import ThreadPoolExecutor

ThreadPoolExecutorはスレッドプールを管理し、それらを並行実行するための高レベルなインターフェースを提供します。

前回と同様に、まず単一URLのスクラッピングロジックを関数に分離します。主な違いは、ThreadPoolExecutorを利用して関数を複数スレッドで実行する必要がある点です:

quotes = []

# 最大10ワーカーのスレッドプールを作成
with ThreadPoolExecutor(max_workers=10) as executor:
    # mapを使用してscrape_page関数を各URLに適用
    results = executor.map(scrape_page, urls)

# 全スレッドの結果を結合
for result in results:
    quotes.extend(result)

デフォルトでは、max_workersが Noneまたは未指定の場合、マシンのプロセッサ数に5を掛けた値が使用されます。今回は10ページのみなので、10に設定すれば問題ありません。ただし、スレッドを過剰に開くとシステムが遅くなり、パフォーマンスが低下する可能性がある点に注意してください。

スクレイピングスクリプト全体は以下のコードで構成されます:

from concurrent.futures import ThreadPoolExecutor
import requests
from bs4 import BeautifulSoup
import csv
import time

def scrape_page(url):
    print(f"Scraping page: '{url}'")

    response = requests.get(url)
    soup = BeautifulSoup(response.content, "html.parser")
    quote_html_elements = soup.select(".quote")

    quotes = []
    for quote_html_element in quote_html_elements:
        text = quote_html_element.select_one(".text").get_text()
        author = quote_html_element.select_one(".author").get_text()
        tags = [tag.get_text() for tag in quote_html_element.select(".tag")]
        quotes.append({
            "text": text,
            "author": author,
            "tags": ", ".join(tags)
        })

    print(f"ページ '{url}' のスクレイピングに成功しましたn")

    return quotes

def scrape_quotes():
    urls = [
        "http://quotes.toscrape.com/",
        "https://quotes.toscrape.com/page/2/",
        "https://quotes.toscrape.com/page/3/",
        "https://quotes.toscrape.com/page/4/",
        "https://quotes.toscrape.com/page/5/",
        "https://quotes.toscrape.com/page/6/",
        "https://quotes.toscrape.com/page/7/",
        "https://quotes.toscrape.com/page/8/",
        "https://quotes.toscrape.com/page/9/",
        "https://quotes.toscrape.com/page/10/"
    ]
    
    # スクレイピングしたデータの保存先
    quotes = []

    # 最大10ワーカーのスレッドプールを作成
    with ThreadPoolExecutor(max_workers=10) as executor:
        # mapを使用してscrape_page関数を各URLに適用
        results = executor.map(scrape_page, urls)

    # 全スレッドの結果を結合
    for result in results:
        quotes.extend(result)

    print("スクレイピングしたデータをCSVにエクスポート中")

    with open("quotes_multiprocessing.csv", "w", newline="", encoding="utf-8") as csvfile:
        fieldnames = ["text", "author", "tags"]
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(quotes)

    print("引用文をCSVにエクスポートしました")

if __name__ == "__main__":
    start_time = time.time()
    scrape_quotes()
    end_time = time.time()

    execution_time = end_time - start_time
    print(f"実行時間: {execution_time:.2f} 秒")

実行すると、以下のようなメッセージがログに記録されます:

ページをスクレイピング中: 'http://quotes.toscrape.com/'
ページをスクレイピング中: 'https://quotes.toscrape.com/page/2/'
ページをスクレイピング中: 'https://quotes.toscrape.com/page/3/'
ページをスクレイピング中: 'https://quotes.toscrape.com/page/4/'
ページをスクレイピング中: 'https://quotes.toscrape.com/page/5/'
ページをスクレイピング中: 'https://quotes.toscrape.com/page/6/'
ページをスクレイピング中: 'https://quotes.toscrape.com/page/7/'
ページ 'https://quotes.toscrape.com/page/8/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/9/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/10/' をスクレイピング中
ページ 'http://quotes.toscrape.com/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/6/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/7/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/10/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/8/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/5/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/9/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/4/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/3/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/2/' のスクレイピングに成功しました

スクレイピングしたデータをCSVにエクスポート中
引用文をCSVにエクスポートしました

実行時間: 0.52秒

マルチプロセッシングスクレイピングと同様に、ページの実行順序はもはや順次ではありません。今回は、マルチプロセッシング時よりもパフォーマンスの向上がさらに大きくなっています。これは、スクリプトが10リクエストを同時に実行できるようになり、以前の制限(CPUコア数である8リクエスト)を超えたためです。

時間改善は劇的で、4.61秒から0.52秒へ、約885%の削減率を達成!

👍 メリット:

  • 大幅な実行時間短縮
  • ほとんどの技術でネイティブサポート

👎 デメリット:

  • 適切なスレッド数の決定が容易でない
  • URLリスト内の順序を保持しない
  • コードに大幅な変更が必要

4. Async/Awaitを使用したスクレイピング

非同期プログラミングは、ノンブロッキングコードを記述可能にする現代的なプログラミングパラダイムです。マルチスレッドやマルチプロセッシングを明示的に管理することなく、並行処理を扱う能力を開発者に与えることがその目的です。

従来の同期的なアプローチでは、各操作が終了してから次の操作が開始されます。これは、特にウェブスクレイピングのようなI/Oに制約されるタスクにおいて非効率を招く可能性があります。非同期プログラミングでは、複数のI/O操作を同時に開始し、それらの完了を待機できます。これにより、スクリプトの応答性と効率性が維持されます。

Pythonでは、非同期スクレイピングは通常、標準ライブラリのasyncioモジュールを使用して実装されます。このパッケージは、asyncおよび awaitキーワードを介してコルーチンを使用したシングルスレッドの並行コードを書くための基盤を提供します。

ただし、requestsなどの標準的なHTTPライブラリは非同期操作をサポートしていません。そのため、asyncioとシームレスに連携するよう設計されたAIOHTTPのような非同期HTTPクライアントを使用する必要があります。この組み合わせにより、スクリプトの実行をブロックすることなく複数のHTTPリクエストを並行して送信できます。

以下のコマンドでAIOHTTPをインストールします:

pip install aiohttp

次に、asyncioaiohttpをインポートします:

import asyncio
import aiohttp

これまでの章と同様に、単一URLのスクレイピングロジックを関数にカプセル化します。ただし今回は、関数を非同期にします:

async def scrape_url(session, url):
    async with session.get(url) as response:
        print(f"Scraping page: '{url}'")

        html_content = await response.text()
        soup = BeautifulSoup(html_content, "html.parser")
        # スクレイピングロジック...

ウェブページのHTMLを取得するためにawait関数が使用されている点に注意してください。

関数を並列実行するには、AIOHTTPセッションを作成し複数のスクレイピングタスクを集約します:

# スクレイピングタスクを並行実行
async with aiohttp.ClientSession() as session:
    tasks = [scrape_url(session, url) for url in urls]
    results = await asyncio.gather(*tasks)

# 結果リストをフラット化
quotes = [quote for sublist in results for quote in sublist]

最後に、asyncio.run() を使用して非同期メインスクレイピング関数を実行します:

if __name__ == "__main__":
    start_time = time.time()
    asyncio.run(scrape_quotes())
    end_time = time.time()

    execution_time = end_time - start_time
    print(f"実行時間: {execution_time:.2f} 秒")

Pythonでの非同期スクレイピングスクリプトには以下のコードが含まれます:

import asyncio
import aiohttp
from bs4 import BeautifulSoup
import csv
import time

async def scrape_url(session, url):
    async with session.get(url) as response:
        print(f"Scraping page: '{url}'")

        html_content = await response.text()
        soup = BeautifulSoup(html_content, "html.parser")
        quote_html_elements = soup.select(".quote")

        quotes = []
        for quote_html_element in quote_html_elements:
            text = quote_html_element.select_one(".text").get_text()
            author = quote_html_element.select_one(".author").get_text()
            tags = [tag.get_text() for tag in quote_html_element.select(".tag")]
            quotes.append({
                "text": text,
                "author": author,
                "tags": ", ".join(tags)
            })

        print(f"ページ '{url}' のスクレイピングに成功しましたn")

        return quotes

async def scrape_quotes():
    urls = [
        "http://quotes.toscrape.com/",
        "https://quotes.toscrape.com/page/2/",
        "https://quotes.toscrape.com/page/3/",
        "https://quotes.toscrape.com/page/4/",
        "https://quotes.toscrape.com/page/5/",
        "https://quotes.toscrape.com/page/6/",
        "https://quotes.toscrape.com/page/7/",
        "https://quotes.toscrape.com/page/8/",
        "https://quotes.toscrape.com/page/9/",
        "https://quotes.toscrape.com/page/10/"
    ]

    # スクラッピングタスクを並行実行
    async with aiohttp.ClientSession() as session:
        tasks = [scrape_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks)

    # 結果リストをフラット化
    quotes = [quote for sublist in results for quote in sublist]

    print("スクレイピングしたデータをCSVにエクスポート中")

    with open("quotes_multiprocessing.csv", "w", newline="", encoding="utf-8") as csvfile:
        fieldnames = ["text", "author", "tags"]
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(quotes)

    print("引用文をCSVにエクスポートしました")

if __name__ == "__main__":
    start_time = time.time()
    asyncio.run(scrape_quotes())
    end_time = time.time()

    execution_time = end_time - start_time
    print(f"実行時間: {execution_time:.2f} 秒")

実行すると、以下のような出力が得られます:

ページスクレイピング中: 'http://quotes.toscrape.com/'
ページ 'http://quotes.toscrape.com/' のスクレイピングに成功しました                                                                

ページスクレイピング中: 'https://quotes.toscrape.com/page/3/'
ページ 'https://quotes.toscrape.com/page/7/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/9/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/6/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/8/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/10/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/3/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/5/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/4/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/7/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/9/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/6/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/2/' をスクレイピング中
ページ 'https://quotes.toscrape.com/page/10/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/5/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/4/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/8/' のスクレイピングに成功しました

ページ 'https://quotes.toscrape.com/page/2/' のスクレイピングに成功しました

スクレイピングしたデータをCSVにエクスポート中
引用符がCSVにエクスポートされました

実行時間: 0.51秒

実行時間はマルチスレッド方式と同等ですが、スレッドを手動で管理する必要がないという利点があります。

👍 長所:

  • 大幅な実行時間短縮
  • 現代的なプログラミングは非同期ロジックに基づいている
  • スレッドやプロセスの手動管理が不要

👎 デメリット:

  • 習得が容易ではない
  • URLリスト内の順序を尊重しない
  • 専用の非同期ライブラリが必要

5. その他のスクレイピング高速化の手法とアプローチ

ウェブスクレイピングを高速化するその他の方法は以下の通りです:

  • リクエストレート最適化:リクエスト間隔を微調整し、速度とレート制限回避・禁止回避の最適なバランスを見つける。
  • ローテーションプロキシ: ローテーションプロキシを使用してリクエストを複数のIPアドレスに分散させ、ブロックされる可能性を減らし、より高速なスクレイピングを可能にします。最適なローテーションプロキシを参照してください。
  • 分散システムによる並列スクレイピング:スクレイピングタスクを複数のオンラインマシンに分散させる。
  • JavaScriptレンダリングの削減:ブラウザ自動化ツールの使用を避け、HTMLパーサーとしてHTTPクライアントなどのツールを優先します。ブラウザは多くのリソースを消費し、従来のHTMLパーサーよりもはるかに遅いことを覚えておいてください。

まとめ

本ガイドでは、ウェブスクレイピングを高速化する方法を解説しました。ウェブスクレイピングスクリプトが遅くなる主な原因を明らかにし、サンプルPythonスクリプトを用いてこれらの問題に対処する様々な手法を検討しました。ウェブスクレイピングロジックをわずかに調整するだけで、実行時間を8倍改善できました。

ウェブスクレイピングプロセスの高速化には手動でのウェブスクレイピングロジック最適化が重要ですが、適切なツールの使用も同様に重要です。ブラウザ自動化ソリューションを必要とする動的サイトをターゲットとする場合、ブラウザは遅くリソースを大量に消費する傾向があるため、状況はより複雑になります。

これらの課題を克服するには、スクレイピング専用に設計された完全ホスト型クラウドソリューション「スクレイピングブラウザ」をお試しください。Puppeteer、Selenium、Playwrightなど主要なブラウザ自動化ツールとシームレスに連携します。CAPTCHA自動解決機能を搭載し、1億5000万以上のレジデンシャルIPからなるプロキシネットワークを基盤としており、あらゆるスクレイピングニーズに対応する無制限のスケーラビリティを提供します!

今すぐ登録して無料トライアルを開始しましょう。