Web scrapingのコツ 〜初歩から完全自動化までのガイド〜

初めまして、2023年11月にInsight Edgeにジョインしたデータサイエンティストのヒメネス(Jiménez)です!スペイン出身です。 入社から数ヶ月しか経っていませんが、この短い期間の中で生成AIの案件に携わるうえ、海外案件でDSコンサルタントとしても活動しています。

元々数学者ですが、プログラミングが大好きで、得意とするアルゴリズム構築を仕事にしました。コンサルティング企業にもしばらく参画した結果、ビジネスも少しずつ理解できるようになり、自分の視野や能力が広がりました。

この機会にWeb scrapingについて紹介したいと思います。Web scraping(スクレイピング)は、Webサイトから自動的にデータを収集する技術であり、研究者、マーケター、データアナリストなどにとって貴重な情報源となっています。

この記事では、Web scrapingの基本から、Seleniumを使用した高度なテクニックまでを紹介し、特に知られざるコツとスクレイピングの倫理に焦点を当てます。私たちの目的は、読者がWeb上のデータを効率的かつ倫理的に収集する方法を理解し、実践できるようになることです。このプロセスを通じて、あらゆるレベルの技術者がWeb scrapingの力を最大限に引き出し、新たな知見を得る手助けをすることを目指しています。

目次

Web scrapingとは

Web scraping(スクレイピング)またはweb crawling(クローリング)とは、プログラムを用いてWebサイトからデータを抽出する技術です。このプロセスには、HTTPリクエストを送信してWebページを取得し、必要な情報を解析・抽出するためのコードが含まれます。スクレイピングは、ネット上に転がっている大量のデータを迅速に収集する必要がある研究、マーケティング分析、競合他社の価格監視など、さまざまな用途で利用されます。

Web scrapingの倫理

スクレイピングは便利な技術である一方で、倫理的な懸念も伴います。Webサイトの利用規約に違反すること、サーバーへの過度な負荷、個人情報の不適切な取得といった問題が発生する可能性があります。そのため、スクレイピングを行う際は、対象サイトの利用規約を確認し、必要に応じてサイトの管理者から許可を得ることが重要です。また、アクセス頻度を制限してサーバーへの負担を軽減するよう配慮する必要があります。

Seleniumの紹介

Seleniumは、Webアプリケーションのテスト自動化のためのフレームワークですが、web scrapingにも広く使用されています。ブラウザを自動操作することで、JavaScriptで動的に生成されたコンテンツを含むページからもデータを取得できます。

まず、Seleniumをインストールしましょう。

pip install selenium

そして、webdriver-managerもインストールしましょう。webdriver-managerは、Selenium WebDriverのドライバを簡単に管理するためのライブラリです。このライブラリを使用することで、ブラウザドライバ(ChromeDriverやGeckoDriverなど)の自動ダウンロードと更新が可能になり、Seleniumを使ったテストやスクレイピングをスムーズに実行できます。

pip install webdriver-manager

この手順により、SeleniumでWebブラウザを自動操作する準備が整います。

ブラウザを手動でダウンロードして使用を指定したい場合、次のリンクよりChromeとFirefoxの最新版がダウンロードできます:ChromeDriver(Chrome)、GeckoDriver(Firefox)。

導入

まずは、Seleniumの世界へようこそと言うことから始めます。初心者でもスムーズに取り組めるよう、ブラウザの起動からWebページへの移動、そして基本操作(テキスト入力やクリック動作)まで、丁寧に解説していきます。ここをマスターすることで、あなたもWebサイトからの簡単なデータ抽出が可能になります。

1. ブラウザ起動

まずはSeleniumを使ってブラウザを立ち上げます。このコード例では、webdriver_managerライブラリを使って、適切なChromeDriverを自動的にダウンロードし、インストールします。

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))

これにより、Seleniumは常に最新のChromeDriverを使用してブラウザを操作できるようになります。

ブラウザが規定のサイズで立ち上がりますが、以下のコマンドでウィンドウの最大化が可能です。

browser.maximize_window()

2. Webページに移動

ブラウザが立ち上がったら、duckduckgo.comに移動させます。コマンドが順序良く実行されるため、基本的には前のコマンド(この場合だと、ブラウザ起動)が完了するまで次のコマンドは待機します。

driver.get('https://duckduckgo.com/')

3. 基本操作

ここからは実際のweb scrapingが始まります。そのために、まずは今立ち上がっているブラウザのエレメント(ボタン、入力など)のないところを右クリックし、最後の選択肢「検証」を選びます。右側に今閲覧しているページのHTMLコードが見えます。(エラーメッセージも下部に表示されているかもしれませんが無視しても大丈夫です)

テキスト入力: send_keys

DuckDuckGoの真ん中にある検索バーにテキストを入力するために、検索バーの中で右クリックし、「検証」をクリックします。

すると、右側の検証画面に青でハイライトされるエレメントが見えます。ハイライトの部分をマウスでホバーすると逆に検索バーがハイライトされます。これで操作したいエレメントがちゃんと選択できているか確認できます。

検索バーのXPathを取得する必要がありますので、右側にハイライトされているエレメントにもう一度右クリックし、「Copy」>「Copy XPath」を選択します。

Seleniumでそのエレメントにテキストを入力するために以下のように書きます。

driver.find_element('xpath', '//*[@id="searchbox_input"]').send_keys('insight edge 会社')

テキストクリア: clear

時々テキストエレメントに既にテキストが記入されています(多くの場合記入例として)。その場合send_keysを使うと入力するテキストが元々のテキストに上書きされず追加されます。内容を消して書き直すためにはclearコマンドを使います。

driver.find_element('xpath', '//*[@id="searchbox_input"]').clear()
driver.find_element('xpath', '//*[@id="searchbox_input"]').send_keys('insight edge 会社')

ボタンクリック: click

検索ボタン(青い🔍)のXPathを上記同様に取得し、今度クリックの命令を書きます。

driver.find_element('xpath', '//*[@id="searchbox_homepage"]/div[1]/div/button[2]').click()

テキスト取得: text

表示された検索結果の一番上にInsight Edgeのホームページが現れます。「insight edge 会社」のテキストを取得しましょう。いつも同様にXPathをコピーします。

driver.find_element('xpath', '//*[@id="r1-0"]/div[2]/h2/a/span').text

ところで、「insight edge 会社」のエレメントはクリックできるエレメントにもなっていますので、前のステップと同様にクリックすることも可能です。ただし、指導のためリンク先のアドレスを取得してからページに移動しましょう。右側のエレメントで「insight edge 会社」のエレメントはh3のラベルがありますが、その一層上のラベルaのもの(リンク)のXPathを取得します。

link_url = driver.find_element('xpath', '//*[@id="r1-0"]/div[2]/h2/a').get_attribute('href')
driver.get(link_url)

Insight EdgeのWebサイトに着いたでしょうか?

ブラウザを閉じる: quit

作業した後に必ずブラウザを閉じましょう。そうしないとメモリが解放されないままお化けのブラウザが生き続けます。

quitでブラウザを終了します。

driver.quit()

これはSeleniumの基礎です。次のセクションでもうちょっと高度なテクニックを共有します。

中級

次のステップでは、Seleniumの使いこなし方を一段階引き上げます。ページの読み込み待機処理、人間のような入力を模倣する方法、そしてヘッドレスブラウザの活用法を学び、より効率的かつ正確にデータを収集する技術を習得します。中級レベルをクリアすることで、あなたのスクレイピングはより洗練されたものへと進化します。

1. 待機

Webページが時々ロードするのに時間がかかります。エレメントがロードされる前にアクセスしようとすると「エレメントが存在しない」エラーが挙がります。エレメントが現れるまで待つには以下のメソッドを使います。先にメソッドをインポートします。

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

そしてもう一度上記の処理を実行します。ただし、今回は待機機能を追加します。

driver.get('https://duckduckgo.com/')
driver.find_element('xpath', '//*[@id="searchbox_input"]').send_keys('insight edge 会社')
driver.find_element('xpath', '//*[@id="searchbox_homepage"]/div[1]/div/button[2]').click()
# 待機。指定したXPathが現れなければ10秒までしか待たないように指定しています。その場合Exceptionを拾うべきです。
element = WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.XPATH, '//*[@id="r1-0"]/div[2]/h2/a/span')))
element.click()

visibility_of_element_located(対象エレメントの表示)の他、presence_of_element_located(対象エレメントの存在)やelement_to_be_clickable(対象エレメントのクリック可能性)など、ご自身のニーズに合ったメソッドを使うことができます。

2. 人間らしい入力

スクレイピングを悪用する人がいるため、Webサイトによっては機械らしい行為に対してアクセスをブロックすることがあります。しかし、我々のような良い使い方をする人にとっては不公平なハードルとして感じられるかもしれません。その場合、人間らしい入力を再現するとてもシンプルな関数を用いてハードルを超えることができます。

def fill_slowly(driver, xpath, keys):
    import time, random
    for key in keys:
        driver.find_element('xpath', xpath).send_keys(key)
        time.sleep(random.uniform(0.2, 1)) # 平均0.2秒の正規分布に従う休憩時間を寝かせます

driver.get('https://duckduckgo.com/')
fill_slowly(driver, '//*[@id="searchbox_input"]', 'insight edge 会社')
driver.find_element('xpath', '//*[@id="searchbox_homepage"]/div[1]/div/button[2]').click()

人間らしい入力イメージ:

3. ヘッドレス

自動化を高速化するために、一度実装したコードをヘッドレス(非表示)モードにします。例えば、テキスト取得に使ったコードを再利用しますが、今回はブラウザを非表示にし、結果をprintで出力します。ブラウザ表示の場合とブラウザ非表示の場合の速さを比較します。

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
import time

def compare_speed(headless=False):
    options = Options()
    if headless:
        options.add_argument('--headless')
    driver = webdriver.Chrome(service=Service(), options=options)
    driver.get('https://duckduckgo.com/')
    driver.find_element('xpath', '//*[@id="searchbox_input"]').send_keys('insight edge 会社')
    driver.find_element('xpath', '//*[@id="searchbox_homepage"]/div[1]/div/button[2]').click()
    text = driver.find_element('xpath', '//*[@id="r1-0"]/div[2]/h2/a/span').text
    driver.quit()
    return text

# 表示
start_time = time.time()
text = compare_speed(headless=False)
print(f'取得したテキスト: {text}')
print(f'経過時間: {time.time() - start_time:.2f} 秒')

# 非表示
start_time = time.time()
text = compare_speed(headless=True)
print(f'取得したテキスト: {text}')
print(f'経過時間: {time.time() - start_time:.2f} 秒')

ヘッドレスモードの方が速かったでしょうか?

上級

この章では、複雑なWebページからのデータ抽出に挑むための、高度なテクニックを学びます。タブの操作からJavaScriptの実行、HTMLの直接取得に至るまで、さらにはマルチスレッディングを駆使した高速化テクニックまで、上級者向けの知識とスキルを身につけ、より深いスクレイピングの世界を探求します。

1. タブ移動

場合によって、クリックする際に新しいタブが開かれます。タブを移動することも可能です。

driver.get('https://dirdam.github.io/apps.html#squadro')
driver.find_element('xpath', '//*[@id="squadro_description"]/a').click()
driver.switch_to.window(driver.window_handles[1])
element = WebDriverWait(driver, 30).until(EC.presence_of_element_located((By.XPATH, '//*[@id="welcome_bga_text"]')))
print(element.text)

driver.window_handlesがタブを管理するリストになりますので、最初のタブの番号は0で、新しく開かれたタブの番号は1です。

タブを閉じる: close

driver.quit()を使うとブラウザ自体が閉じてしまいます。タブだけを閉じるのにdriver.close()を使います。

注意しなければならないことは、タブを閉じると再びアクセスできないので、引き続きスクレイピング作業をする場合、開いているタブに切り替える必要があります。例えば、規定のタブに戻るのに0番目のタブに戻ります。

driver.close()
driver.switch_to.window(driver.window_handles[0])

2. JavaScript実行

Webサイトに直接JavaScriptを投げることが可能です。便利な使用例がいくつか考えられます。

新規タブを開く(そのまま)

ブラウザ上で新しいタブを開きます。(内容は空です)

driver.execute_script("window.open('','_blank');")

新規タブを開く(特定のURL)

ブラウザ上で新しいタブを開きます。(内容は特定したURL)

driver.execute_script("window.open('https://duckduckgo.com');")

メタデータ取得

例えば、ページタイトルの取得。

driver.execute_script('return document.title;')

3. HTML取得

全HTML取得

Webページのソースコード(HTML)を取得することも可能です。ページ全体のソースコードを取得するには以下のコードを使用します。

page_source = driver.page_source
print(page_source)

一部HTML取得

一方、特定のXPathのエレメント配下のHTMLのみ抽出したい場合、excute_scriptメソッドを活用して以下のコードで取得可能です。

driver.get('https://duckduckgo.com/')
parent_element = driver.find_element('xpath', '//*[@id="__next"]/main/article/div[1]/div[1]/div/section[2]')
html_code = driver.execute_script("return arguments[0].outerHTML;", parent_element)

読み込んだHTMLを保存したい場合、HTMLファイルとして保存(再生)できます。

with open("temp.html", "w", encoding="utf-8") as file:
    file.write(html_code)

また、そのファイルを開いて、そのHTMLに対して改めてSelenium経由でスクレイピングすることも可能です。URLではなく、ローカルHTMLを開くのに以下使います。

import os
driver.get("file://" + os.path.abspath("temp.html"))

4. マルチスレッディング

異なるURLから何度もデータを取得する場合、一つ一つ順番に処理を行う方法もありますが、効率を考えるとマルチスレッディングを使用することがより効果的です。マルチスレッディングを使うことで、複数の処理を並行して実行できるため、全体の処理時間を大幅に短縮することが可能になります。

例えば、Wikipediaの複数のオリンピックの記事から、それぞれの参加人数の情報を取得したい場合、以下のようなコードで取得することができます。まずはオリンピック記事から参加人数を抽出する関数を用意します。

def find_participants(url):
    driver = webdriver.Chrome(service=Service())
    driver.get(url)
    for i in range(1, 2 + 1): # テーブル番号は1か2のどちらか
        for j in range(6, 7 + 1): # 行番号は6から7のどちらか
            try:
                element = driver.find_element('xpath', f'//*[@id="mw-content-text"]/div[1]/table[{i}]/tbody/tr[{j}]/th')
                if element.text == '参加人数': # 参加人数の行を見つけたら、隣の列のテキストを返す
                    participants = driver.find_element('xpath', f'//*[@id="mw-content-text"]/div[1]/table[{i}]/tbody/tr[{j}]/td').text
                    driver.quit()
                    return participants
            except:
                continue
    return '見つかりませんでした'

以下のコードで情報を順番に取得します。

titles = ['2020年東京オリンピック', '2016年リオデジャネイロオリンピック', '2012年ロンドンオリンピック', '2008年北京オリンピック']
participants = {}
start_time = time.time()
for title in titles:
    participants[title] = find_participants(driver, f'https://ja.wikipedia.org/wiki/{title}')
print(f'経過時間: {time.time() - start_time:.2f} 秒')
print(participants)

ただし、それぞれの記事へのアクセスは独立しているので、それぞれのデータを並行して取得するようにします。

from multiprocessing.pool import ThreadPool

start_time = time.time()
titles = ['2020年東京オリンピック', '2016年リオデジャネイロオリンピック', '2012年ロンドンオリンピック', '2008年北京オリンピック']
participants = {}
pool = ThreadPool(processes=len(titles)) # 同時に実行するスレッド数を指定
threads = [] # スレッドとタイトルの対応を保存するリスト
for title in titles:
    t = pool.apply_async(find_participants, (f'https://ja.wikipedia.org/wiki/{title}',))
    threads.append((title, t))

pool.close() # プールを閉じる。これ以降新しいスレッドは作成しない
pool.join() # 全てのスレッドが終了するまで待つ

for title, thread in threads:
    try:
        participants[title] = thread.get() # コールの結果を取得
    except Exception as e:
        print(f'エラー: {e}')
        
print(f'経過時間: {time.time() - start_time:.2f} 秒')
print(participants)

私の実験環境で順番に処理すると処理が約30秒かかり、マルチスレッディングだと10秒以内に終了します。

ご覧の通り、節約できる実行時間がとても大きいです。取得する情報が多ければ多いほど、このようなアプローチがもっと大事になってきます。並行して実行するブラウザが多ければ多いほどメモリも使いますのでそれとの兼ね合いで調整する必要があります。

おまけ: Selenium + Kaggle API

最後に、SeleniumとKaggle APIを組み合わせて、自動的にデータセットをKaggleにアップロード(更新)する方法について説明します。下記で習うことを活かして、記者がオンラインボードゲームサイトのSquadroというゲームの各々のプレイの情報をスクレイピング経由で収集してデータセットとしてKaggleにアップロードしています。Kaggleは、データサイエンスと機械学習のコミュニティで広く使用されており、自分のデータセットを公開して他の研究者と共有することが可能です。

※ データセットを作成する場合、手動で初回のアップロードを行なってください。こちらで見せるコードはデータセットの自動更新用のコードです。

1. 前提条件

  • Kaggleアカウントが必要です。まだお持ちでない場合は、Kaggleでアカウントを作成してください。
  • Kaggle APIのトークンを取得します。Kaggleのアカウント設定ページから「Create New API Token」をクリックしてダウンロードします。ダウンロードしたkaggle.jsonファイルは、安全な場所に保存してください。

2. Kaggle APIの準備

Kaggle APIを使用するためには、まずkaggleパッケージをインストールする必要があります。

pip install kaggle

次に、ダウンロードしたkaggle.jsonファイルを、Kaggle APIの設定ファイルが読み込まれるディレクトリに配置します。通常、このディレクトリは~/.kaggleです(LinuxおよびMacOSの場合)。Windowsの場合は、C:\Users\<Windows-username>\.kaggle\になります。

mkdir ~/.kaggle
cp path/to/kaggle.json ~/.kaggle/kaggle.json
chmod 600 ~/.kaggle/kaggle.json

3. SeleniumとKaggle APIの組み合わせ

Seleniumを使ってスクレイピングを行います。毎日更新される情報を集めるのに便利です。スクレイピングしたデータをdataset_path配下に保存します。

# スクレイピング
df = scraping_call() # 適当にデータを収集する関数

# データ保存
dataset_path = '/path/to/your/dataset' # データセットのローカルディレクトリを指定
dataset_name = 'my_data.csv' # データファイル名
df.to_csv(f'{dataset_path}/{dataset_name}') # 保存関数

Kaggle APIを呼び出すのに必要なライブラリをインポートします。そして、Kaggle APIに認証します。

from kaggle.api.kaggle_api_extended import KaggleApi

api = KaggleApi()
api.authenticate()

Kaggle APIを使用して、データセットをアップロード(更新)します。

# データセットの新しいバージョンを作成
api.dataset_create_version(folder=f'{dataset_path}/', version_notes="メモ")

このプロセスを使用することで、データセットの管理と更新を効率的に行うことができます。Seleniumを介して追加のWeb操作を自動化することと組み合わせることで、完全に自動化されたデータアップロードのワークフローを構築することが可能です。

重要なのは、自動化プロセスを利用する際にスクレイピングの倫理的な実装を行うこととKaggleの利用規約に違反しないようにすることです。

4. 活用例: Squadroのプレイ歴収集

このセクションの冒頭で述べた通り、上記を実装し、記者がSquadroというボードゲームのデジタル版でプレイされるゲームの情報を2日に1回ぐらい集めて自動的にこちらのデータセットを更新しています。更に、そのデータセットを自動でダウンロードし、読み込んでデータを可視化するStreamlitアプリも開発しました。

まとめ

この記事では、スクレイピングの基本から応用まで幅広くカバーしました。スクレイピングは、Webサイトからデータを抽出するための強力な技術であり、研究、マーケティング分析、競合他社の価格監視など様々な用途で利用されています。しかし、スクレイピングを行う際には、Webサイトの利用規約に違反しないよう注意し、サーバーへの負荷を最小限に抑える必要があります。

スクレイピングは、適切に行えばデータ収集の効率を大幅に向上させることができますが、技術的な面だけでなく、倫理的な観点からも慎重に取り組む必要があることを忘れてはいけません。皆さんも是非良いスクレイピングを実装してみてください!