Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
1749
Help us understand the problem. What are the problem?

実践/現場のPythonスクレイピング

!! ======================== !!
※この記事は2019年の記事です。この記事で紹介している内容は2019年当時の内容である事を理解した上で、実際に設定する際は最新の情報を確認しながら行ってください。
!! ======================== !!

SeleniumはE2Eテストの自動化などで大きな力を出してくれます。

今回の記事では、下記の内容をまとめてみます。

  • 色々なユースケース
  • 抜け漏れ対策のwait.until()関数 => 実務ではとても重要
  • IDやClassが無くても、AltやPlaceholderなどから力技で抽出する技
  • パスワード系
  • 無限スクロール系

必要なツールをまずは揃える

Python3.7

ChromeDriver

https://sites.google.com/a/chromium.org/chromedriver/downloads
(※お使いのchromeと同じバージョンをインストールしてください。2019年3月現在は73が公式最新版です。)
ダウンロードして解凍したファイルを、環境変数が通っているフォルダに保存してください。

ちなみに、下記のコマンドでも可能という記事がありますが、

terminal
pip install chromedriver-binary

この方法でインストールすると、公式版を通り越して、開発版を優先するケースがあります。その場合GUI側のブラウザとバージョンが合わなくて動かないケースがあったりするので使わない方がいいと思っています。

Selenium, BeautifulSoup

Seleniumは、上記ChromeDriverを操作する為のプログラムです。
BeautifulSoupは、サイトのHTMLコードの中から、お目当ての情報を抽出するソフトです。

terminal
pip install selenium
pip install beautifulsoup4

ここまでインストール出来たら十分です。

最初のスクレイピング 一番簡単な基本

試しに下記コードを実行して、ブラウザが立ち上がるか見てみましょう。

ch1_first-scraping.py
import time
from selenium import webdriver

# 仮想ブラウザ起動、URL先のサイトにアクセス
driver = webdriver.Chrome()
driver.get('https://www.google.com/')
time.sleep(2)

# サイト内から検索フォームを探す。
# Googleでは検索フォームのNameが「q」です。
el = driver.find_element_by_name("q")
# 検索フォームに文字を入力
el.send_keys('Qiitaで記事投稿楽しいぞ!!!')
time.sleep(2)

# 検索フォーム送信(Enter)
el.submit()

from bs4 import BeautifulSoup
soup = BeautifulSoup(driver.page_source, features="html.parser")
# タイトルをターミナル上に表示
print(soup.title.string)

無事に下画像のようにタイトルが表示されたでしょうか?
python-selenium-scraping
今あなたは最初のクローリングとスクレイピングに成功したのです。
Google検索ページをクロールして、タイトルをスクレイピングした訳ですね。

発展編 ブラウザを起動しないでスクレイピング

次に、こちらのサンプルを動かしてください。
先程のサンプルに、Optionを載せてブラウザを起動させずにプログラムが動きます。

ch1_hidden-browser.py
import time
from selenium import webdriver

from selenium.webdriver.chrome.options import Options
op = Options()
# --headlessだけではOSによって動かない、プロキシが弾かれる、
# CUI用の省略されたHTMLが帰ってくるなどの障害が出ます。
# 長いですが、これら6行あって最強かつどんな環境でも動きますので、必ず抜かさないようにしてください。
op.add_argument("--disable-gpu");
op.add_argument("--disable-extensions");
op.add_argument("--proxy-server='direct://'");
op.add_argument("--proxy-bypass-list=*");
op.add_argument("--start-maximized");
op.add_argument("--headless");
driver = webdriver.Chrome(chrome_options=op)

#driver = webdriver.Chrome()
driver.get('https://www.google.com/')
time.sleep(2)

el = driver.find_element_by_name("q")
el.send_keys('Qiitaで記事投稿楽しいぞ!!!')
time.sleep(2)

el.submit()

from bs4 import BeautifulSoup
soup = BeautifulSoup(driver.page_source, features="html.parser")
print(soup.title.string)

python-selenium-scraping
いかがですか?ターミナル上に、先程と同じようにタイトルが表示されましたか?

それでは、簡単なサンプルが終わったことですので
早速ケース別のコピペ集を見ていきましょう。

ログインが要らない、普通のサイト

一番簡単です。下をコピペするだけで大丈夫です。

対象がセレクターで選べる場合

ch2_base-scraping.py
URL      = "https://qiita.com" # <= ここにスクレイピングしたい対象URLを書いてください
Selector = "h1.nl-Hero_title"  # <= ここにスクレイピングしたい対象のCSSセレクタを書いてください

# 必須
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from bs4 import BeautifulSoup

# Selenium用オプション
op = Options()
op.add_argument("--disable-gpu");
op.add_argument("--disable-extensions");
op.add_argument("--proxy-server='direct://'");
op.add_argument("--proxy-bypass-list=*");
op.add_argument("--start-maximized");
op.add_argument("--headless");
driver = webdriver.Chrome(chrome_options=op)

# Seleniumでサイトアクセス
# スクレイピングしたい対象が描写されるまでWait
# time.sleep()はご法度!指定した時間待っても描写されない事はままあるので。
driver.get(URL)
WebDriverWait(driver, 30).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, Selector))
)

soup = BeautifulSoup(driver.page_source, features="html.parser")
el = soup.select(Selector)[0].string
print(el)

うまく動作したでしょうか?
試しに、他のサイトでも試してみてください。

例えばここから・・・
URL = "http://blog.livedoor.jp/lalha/archives/50105281.html"
Selector = "h1.article-title a"
python-selenium-scraping

うまく行きましたね!
上記は、セレクターで選べるものは全て可能です。
では次に、もっと複雑なケースの抽出方法を見ていきましょう。

対象が特殊な条件の場合

ここから先は、先程の『対象がセレクターで選べる場合』の「ch2_base-scraping.py」を元に、一部だけを変えます。

まずは「ch2_base-scraping.py」の冒頭にあるSelectorは、「body」だけを代入してください。
ここからbeautifulsoupの指定方法だけ変えれば、ありとあらゆるもの全て取得できます。

特定のテキストを含む要素だけ取得したい

el = soup.find(text="プログラムが上手くなりたい")

特定のテキストを含む『div』だけ取得したい

el = soup.find("div", text="プログラムが上手くなりたい")

正規表現で特定のテキストを含む『div』だけ取得したい

el = soup.find("div", text=re.compile("^(?=.*円)(?!.*:)"))

特定のaltを含むimgだけを取得したい

el = soup.find("img", alt="商品詳細")

(3/20追記 @megmism さん情報)

# select関数を用いる場合
el = soup.select('img[alt="商品詳細"]')[0]

あるリンクのhrefを取得したい

el = soup.find(~~~ a).get("href")

ページ内の全てのh2を取得したい

# 返り値はリスト
el = soup.find_all("h2")

ページ内の全てのh2, h3を取得したい

# 返り値はリスト
el = soup.find_all(['h2', 'h3'])

ページ内の全てのh系タグを取得したい

el = soup.find_all(re.compile("^h[0-9]"))

ある要素の親要素を取得したい

el = soup.find(~~~).parent

ある要素のn番目のdiv子要素を取得したい

el = soup.find(~~~).select("div:nth-of-type(2)")[0]

og:imageを取得したい

el = soup.find('meta', attrs={'property': 'og:image', 'content': True})["content"]

(3/20追記 @megmism さん情報)

# select関数を用いる場合
el = soup.select("meta[property='og:image']")[0]

ログインが要る、会員制サイト

ログインが必要な場合、IDとパスワードの入力が必要になります。
まず、Qiitaで試してみましょう。

ch3_login.py
URL      = "https://qiita.com/login" # <= ここにスクレイピングしたい対象URLを書いてください
ID       = ""                        # <= ここにログインIDを書いてください
ID_sel   = "#identity"               # <= ここにログインID欄のCSSセレクタを書いてください
PASS     = ""                        # <= ここにログインパスワードを書いてください
PASS_sel = "#password"               # <= ここにログインパスワード欄のCSSセレクタを書いてください
Selector = ".st-Header_loginUser img"# <= ここにスクレイピングしたい対象URLを書いてください

# 必須
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from bs4 import BeautifulSoup

# Selenium用オプション
op = Options()
op.add_argument("--disable-gpu");
op.add_argument("--disable-extensions");
op.add_argument("--proxy-server='direct://'");
op.add_argument("--proxy-bypass-list=*");
op.add_argument("--start-maximized");
op.add_argument("--headless");
driver = webdriver.Chrome(chrome_options=op)

# ログインページアクセス
driver.get(URL)
WebDriverWait(driver, 30).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, PASS_sel))
)
driver.find_elements_by_css_selector(ID_sel)[0].send_keys(ID)
driver.find_elements_by_css_selector(PASS_sel)[0].send_keys(PASS)
driver.find_elements_by_css_selector(PASS_sel)[0].send_keys(Keys.ENTER)

# ターゲット出現を待機
WebDriverWait(driver, 30).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, Selector))
)
soup = BeautifulSoup(driver.page_source, features="html.parser")
el = soup.select(Selector)[0]
print(el.get("alt"))

いかがでしょうか?実際にアクセスして、ユーザー名を抜き出す事が出来たでしょうか。

無限スクロール系

Lazy-loadを使っているサイトも当てはまります。
今回はYoutubeで試してみましょう。

ch4_infinite-scroll.py
URL      = "https://infinite-scroll.com/demo/full-page/page3.html"
Selector = "#3c-365daysofmusic-com"

# 必須
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from bs4 import BeautifulSoup

# Selenium用オプション
op = Options()
op.add_argument("--disable-gpu");
op.add_argument("--disable-extensions");
op.add_argument("--proxy-server='direct://'");
op.add_argument("--proxy-bypass-list=*");
op.add_argument("--start-maximized");
op.add_argument("--headless");
driver = webdriver.Chrome(chrome_options=op)

# ページアクセス
driver.get(URL)
WebDriverWait(driver, 30).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, Selector))
)
soup = BeautifulSoup(driver.page_source, features="html.parser")
el = soup.select(Selector)[0]
print(el)

まず、これを動かしてみてください。

list index out of rangeと出たら、「該当したものがないので、リストは空ですよ」という意味です。つまり見つけられなかったという事ですね。

これを突破するためには、スクリプトを一行追加してやりましょう。

ch4_infinite-scroll.py
URL      = "https://infinite-scroll.com/demo/full-page/page3.html"
Selector = "#3c-365daysofmusic-com"

# 必須
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from bs4 import BeautifulSoup

# Selenium用オプション
op = Options()
op.add_argument("--disable-gpu");
op.add_argument("--disable-extensions");
op.add_argument("--proxy-server='direct://'");
op.add_argument("--proxy-bypass-list=*");
op.add_argument("--start-maximized");
op.add_argument("--headless");
driver = webdriver.Chrome(chrome_options=op)

# ページアクセス
driver.get(URL)
WebDriverWait(driver, 30).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, Selector))
)

# こいつを追加!!!!!!!
driver.execute_script("window.scrollTo(0, document.getElementById('js-content-box').scrollHeight)")

soup = BeautifulSoup(driver.page_source, features="html.parser")
el = soup.select(Selector)[0]
print(el)

今度は正常に読み込みが成功しますね。

seleniumでは、javascriptを実行するexecute_script()関数が用意されています。
ここではターゲットのheightを取得して、一番下までスクロールするという動きを実現しています。
一番下までスクロールするので、Lazy-Load後の画像が描写されるわけです。

以上をもって、ユースケース別サンプルの紹介を終わります。

BASIC認証を突破する方法

(2020/06/09追記 @namariver さん情報 )
上記の通り、Seleniumはiframeやpopup alertに対しても操作を可能にする方法が存在します。しかし、BASIC認証に対しては直接操作はできません。その場合は下記の形式を用いてURLアクセスする事でBASIC認証を通過する事が可能です。

driver.get("http://username:password@url")

スクレイピングに失敗してしまった場合の通知方法

(3/22追記 @ma7ma7pipipi さんご質問 )
仮に対象サイトがHTML構成を変えた場合、
もしくは、途中でwifiが何らかの不具合で消えてしまった場合、
こういった時にスクレイピングが失敗してしまいます。

この時、最もオススメする対処法は下記の通りです。
どこでどういったエラーが発生したかをログに残す
それ以降の処理は正常に続ける

まずは①で、エラーを残すログのソースコードを見ましょう。

detect-error.py
# 必須
from selenium.common.exceptions import TimeoutException

try:
    # 対象が画面に表示されるまで、最大30秒待つ
    element = WebDriverWait(driver, 30).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, "#target img"))
    )
    # 以降の処理を記述していく
    # ~~~
except TimeoutException:
    # 30秒経っても対象が表示されなかった場合、エラーログをprintして、続きのプログラムに移行(通知送信などすると良さそう)
    print("Assert: 対象物がページにありませんでした index[" + n + "] URL:" + url)
    continue

まとめ

今回のサンプルを使い、スクレイピングを回す事はできたでしょうか?

SeleniumはE2Eテストなどで大きな威力を発揮できます。どんどんテストを自動化していき開発に活用していきましょう。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
1749
Help us understand the problem. What are the problem?