🐒

クックパッドから複数ページをSeleniumでスクレイピング!!

2023/05/17に公開

1.はじめに

今回は、自然言語処理の自己学習の一環としてレシピサイトの言語情報が必要でスクレイピングを行った際に個人的に詰まった点や注意点を備忘録として記事を書いていきます。
 あと、規約的におそらくスクレイピング自体はサーバーに負荷がかからないよう配慮すれば大丈夫だとは思っているのですがもしこの認識が間違っているようであればご指摘よろしくお願いします。

2.Seleniumについて

今回用いたSeleniumとは、Webアプリケーションのテストを自動化するオープンソースのテストフレームワークで、ジェイソン・ハギンズ氏によって2004年に開発されました。
 PythonによるスクレイピングではBeautifulSoupに並んでよく用いられます。
 今回のスクレイピングでSeleniumを用いた理由としては、スクレイピング対象がページを跨いだり検索を行ったりと動的なため用いました。
また、Seleniumを動かす際の注意点として以下のものを用意する必要があります。

  • ChromeをはじめとしたWebブラウザ
  • ブラウザを動かすために必要なWebDriver

これらを作業する環境にインストールする必要があります。

3.Seleniumによるスクレイピングの流れ

今回のスクレイピングの流れは以下のようになります。

  1. 取得したいレシピに関する検索キーワードを入力
  2. 検索結果から表示されているレシピのタイトルと概要の言語情報を取得
  3. ページ内のレシピに関して全て取得したら次のページへ移動
  4. 2,3を次のページへ移るためのURLが取得できなくなるまで続ける
  5. JSONファイルとして保存する
scraping_for_cockpad.py
option = Options()   
#画面非表示の設定
option.add_argument('--headless') 
driver_path = "./chromedriver"
driver = webdriver.Chrome(executable_path=driver_path, options=option)
#クックパッドのURLの取得
driver.get('https://cookpad.com/')
#サーバーの負荷の回避
time.sleep(INTERVAL)

各種手順に移る前に最初の設定をします。
ここでは

  • Windowの非表示
  • DriverのPathの指定
  • クックパッドのURLの取得
  • サーバーに負荷がからないよう待機の設定

を行っています。
 また、binaryについては「chromedriver_binary」というライブラリをインポートするだけで大丈夫です。(要インストール)

3-1:検索キーワードの入力と検索

普段検索を行う際には、検索フォームを見つけそこに検索したいキーワードを入力します。
この動作をPython上でSeleniumを用いて行っていきます。

scraping_for_cockpad.py
# 検索フォームにキーワードを入力して送信
search_form = driver.find_element(By.XPATH, '//*[@id="global_search_form"]')
search_form.find_element(By.NAME, 'keyword').send_keys(keyword)
search_form.submit()
# レシピのタイトルが表示されるまで待機
wait = WebDriverWait(driver, 20)
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.recipe-title')))

waitの時間指定が20とかなり大きい値にしている理由としては、自分が動かしている環境だとDrive操作がかなり重くページの表示にかなり時間を要したためなのでこの時間指定については個々で調整してください。

3-2:検索結果から表示されているレシピのタイトルと概要の言語情報を取得

1の操作で取得したいデータが存在するWebページを表示できたのでそこから言語情報(レシピのタイトル,概要)を取得していきます。

scraping_for_cockpad.py
# 結果の格納先の準備
recipes = []
num_do = 1
#レシピ情報をリスト型で取得
recipe_list = driver.find_elements(By.XPATH, '//li[@class ="recipe-preview"]')
#次へボタンの情報を取得する(初回ver)
if num_do == 1:
    bottun = driver.find_element(By.XPATH, '//*[@id="main"]/div[1]/section[1]/main/div[1]/div/a')
#二回目以降
else:
    bottun = driver.find_element(By.XPATH, '//*[@id="main"]/div[1]/section[1]/main/div[1]/div/a[2]')
#レシピ情報を一つずつ取得する
for n in range(len(recipe_list)):
    recipe = recipe_list[n] 
    #レシピのタイトル情報を取得する
    title = recipe.find_element(By.CSS_SELECTOR, '.recipe-title').text
    #レシピの概要を取得する
    description = recipe.find_elements(By.XPATH, '//div[@class = "recipe-text" or @class = "recipe-text recipe-text-noimage"]/div[@class= "recipe_description"]')[n].text
    print(f'{num_do}: {title}:{description}')
    recipes.append({'title': title, 'description': description})
num_do+=1

"num_do"を入れている理由としては、クックパッドのページにおいて1ページ目の「次へ」ボタンのXPATHが異なっているため条件分岐のトリガーにするために用いました。

3-3:ページ内のレシピに関して全て取得したら次のページへ移動

2で表示されたページのレシピ情報を取得し終えたら、次のページに移っていきます。

scraping_for_cockpad.py
#2で取得した次へボタンをクリックする
bottun.click()
#次のページのレシピのタイトルが表示されるまで待機
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.recipe-title')))
#遷移先のURLのdriverに取得させる
cur_url = driver.current_url
driver.get(cur_url)    
#さらに次のページのURLを取得する
next_page_url = driver.find_elements(By.XPATH, '//*[@id="main"]/div[1]/section[1]/main/div[1]/div/a[2]')

この段階において自分は都度driverにURLを取得させることを抜けていたためかなり頭を抱えました、、、(よくよく考えれば必要不可欠ですよね、、)

3-4: 2,3を次のページへ移るためのURLが取得できなくなるまで続ける

2で情報を取得し、3で次のページに遷移するということを最後のページになるまで繰り返していきます。

scraping_for_cockpad.py
#次のページが存在する限り実行
while len(next_page_url) > 0:
	recipe_list = driver.find_elements(By.XPATH, '//li[@class ="recipe-preview"]')
	if num_do == 1:
	    bottun = driver.find_element(By.XPATH, '//*[@id="main"]/div[1]/section[1]/main/div[1]/div/a')
	else:
	    bottun = driver.find_element(By.XPATH, '//*[@id="main"]/div[1]/section[1]/main/div[1]/div/a[2]')
	for n in range(len(recipe_list)):
	    recipe = recipe_list[n] 
	    title = recipe.find_element(By.CSS_SELECTOR, '.recipe-title').text
	    description = recipe.find_elements(By.XPATH, '//div[@class = "recipe-text" or @class = "recipe-text recipe-text-noimage"]/div[@class= "recipe_description"]')[n].text
	    print(f'{num_do}: {title}:{description}')
	    recipes.append({'title': title, 'description': description})
	bottun.click()
	wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.recipe-title')))
	cur_url = driver.current_url
	driver.get(cur_url)    
	next_page_url = driver.find_elements(By.XPATH, '//*[@id="main"]/div[1]/section[1]/main/div[1]/div/a[2]')
	num_do += 1
#終了の合図
else:
   print("\n\n最後のページの処理が終わりました。\n\n")

動的スクレイピング自体は初めてではなかったのですが、ページを跨いでのスクレイピングは初めてだったのでここが一番苦労しました。
 最終的な考え方としては、次のページのURLをSeleniumのfind_elementsで要素をリスト型で取得することで次のページが存在するときはlen(要素)が1で、存在しない時はlen(要素)が0となるようにすることで条件分岐を行いました。

3-5:JSONファイルとして保存する

テキストデータなのでJSONファイルとして保存した方が後々都合が良いので最後にJSONファイルに変換して終わりです。

scraping_for_cockpad.py
# JSONファイルに出力
with open(f'../data/recipes_{keyword}.json', 'w') as f:
	json.dump(recipes, f, ensure_ascii=False, indent=4)

json.dumpのindentを4に指定している理由はなんとなく見やすいからなのでなくても大丈夫です。

4.実行結果

これまでで作成したコードを実行して、取得した結果が以下のようになります。
図1:scraping_for_cockpad.pyの実行結果

5.まとめ

今回は動的にかつページを跨ぐスクレイピングをSeleniumで行いました。
自分的に躓いたポイントをまとめたのですが、もっと効率の良い方法あるよ!って方いらっしゃいましたらご教授いただけると幸いです。

6.コードの全体図

scraping_for_cockpad.py
import time
import json
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# WebDriverの設定
import chromedriver_binary

def Scrayping(keyword:str, limit:int = None):
    '''
    キーワードを指定するとその検索結果のタイトルと概要をスクレイピングしてくれる関数
    '''
    
    INTERVAL = 2
    #Seleniumを使うための設定とgoogleの画面への遷移
    driver_path = "./chromedriver"
    driver = webdriver.Chrome(executable_path=driver_path)
    driver.maximize_window()
    driver.get('https://cookpad.com/')
    time.sleep(INTERVAL)

    # 検索フォームにキーワードを入力して送信
    search_form = driver.find_element(By.XPATH, '//*[@id="global_search_form"]')
    search_form.find_element(By.NAME, 'keyword').send_keys(keyword)
    search_form.submit()
    #レシピのタイトルが表示されるまで待機
    wait = WebDriverWait(driver, 20)
    wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.recipe-title')))
    # レシピのタイトルと概要をスクレイピングして、辞書形式で保存
    next_page_url = driver.find_elements(By.XPATH, '//*[@id="main"]/div[1]/section[1]/main/div[1]/div/a')
    recipes = []
    num_do = 1
    #次のページが存在する限り実行
    while len(next_page_url) > 0:
        #表示されているページのレシピ全て取得
        recipe_list = driver.find_elements(By.XPATH, '//li[@class ="recipe-preview"]')
	#次へボタンの取得(初回ver)
        if num_do == 1:
            bottun = driver.find_element(By.XPATH, '//*[@id="main"]/div[1]/section[1]/main/div[1]/div/a')
	#二回目以降
        else:
            bottun = driver.find_element(By.XPATH, '//*[@id="main"]/div[1]/section[1]/main/div[1]/div/a[2]')
	#レシピを一つずつ取得
        for n in range(len(recipe_list)):
            recipe = recipe_list[n] 
	    #レシピのタイトルを取得 
            title = recipe.find_element(By.CSS_SELECTOR, '.recipe-title').text
	    #レシピの概要を取得
            description = recipe.find_elements(By.XPATH, '//div[@class = "recipe-text" or @class = "recipe-text recipe-text-noimage"]/div[@class= "recipe_description"]')[n].text
            print(f'{num_do}: {title}:{description}')
            recipes.append({'title': title, 'description': description})
	#次へボタンをクリック
        bottun.click()
	#レシピのタイトルが表示されているため
        wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.recipe-title')))
	#新しいページのURLの取得
        cur_url = driver.current_url
        driver.get(cur_url)   
	#さらに次のページを取得
        next_page_url = driver.find_elements(By.XPATH, '//*[@id="main"]/div[1]/section[1]/main/div[1]/div/a[2]')
        num_do += 1
        if num_do == limit:
            break
    #終了した時の合図     
    else:
        print("\n\n最後のページの処理が終わりました。\n\n")

    # JSONファイルに出力
    with open(f'../data/recipes_{keyword}.json', 'w') as f:
        json.dump(recipes, f, ensure_ascii=False, indent=4)

    # WebDriverを終了
    driver.close()

Discussion