65
71

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

INTECAdvent Calendar 2020

Day 2

Pythonで手軽に始めるWebスクレイピング

Last updated at Posted at 2020-12-01

#はじめに
ウェブスクレイピングの方法は数あれど、やっぱりPythonが一番楽だし便利だよね!
本投稿では、Pythonですぐに始められる、ウェブスクレイピングの方法について紹介します。
スクレイピングに使うライブラリのまとまった記事が欲しかった、かつての私のための投稿です。

#目次

#Requests + Beautiful Soup
RequestsはPythonで使える便利なHTTPライブラリです。今回はHTTPのGETメソッドならびにPOSTメソッドのために使用します(Webページ情報の取得)。
Beautiful SoupはHTMLやXMLの解析、ならびに要素の抽出に用いられるPythonのモジュールです。

###インストール
Requestsとbs4(Beautiful Soupの入っているパッケージ)のインストールは、pipで実行できます。

$ pip install requests
$ pip install bs4

###GET(基本)
Requests + Beautiful Soupによるスクレイピングは、以下の手順で実行されます。

1BSget.png
下記のようにHTMLやXMLのみで書かれたページのほとんどが、下記get.pyのような簡単なコードで解析ならびに要素の抽出ができます。

See the Pen sampleGet by Mizuki Ohno (@Moh_no) on CodePen.

get.py
import requests
from bs4 import BeautifulSoup

# HTMLの取得(GET)
req = requests.get("sampleGet.html")
req.encoding = req.apparent_encoding # 日本語の文字化け防止

# HTMLの解析
bsObj = BeautifulSoup(req.text,"html.parser")

# 要素の抽出
items = bsObj.find_all("li")
item = items.find("a").get("href")
print(item)

上記のスクレイピング結果は、以下になります。

ビーフカレー.html

Beautiful Soupによる要素の抽出の簡単な説明
要素の検索は、要素名と属性でフィルタリングできるfind系がオススメです。
また、検索した要素からの属性の抽出には、getを利用します。テキストの抽出はtextがオススメです。

・find_all
指定したタグを全て返す(すべての要素をリストで返す)
items = bsObj.find_all("li")のitemsは、3つのliタグの要素(ビーフ、チキン、キーマ)をリスト型でもつ。
items = bsObj.find_all(id="keema")のように属性で指定も可能であり、その場合のitemsは、id="keema"のliタグの要素だけを取得する。

・find
指定したタグを1つ返す(最初にヒットした要素を返す)
item = items.find("a")のitemは、aタグの要素を最初の一つだけ取得する。
find_all同様に、属性でも指定可能である。

・get
指定した属性の値を返す。
item = items.find("a").get("href")では、findで検索したaタグの属性"href"をitemに代入している。

・text
指定したタグのテキストを取得する。
item = items.find("a", id=""keema).textとした場合、id="keema"のaタグに含まれるテキストである「キーマ」がitemに代入される。

要素の抽出方法について、より詳しく知りたい方は、有識者の方々がこれらの記事に詳しくまとめてくださっています。
 - PythonとBeautiful Soupでスクレイピング
 - [Beautiful Soup のfind_all( ) と select( ) の使い方の違い]
(https://gammasoft.jp/blog/difference-find-and-select-in-beautiful-soup-of-python/)
 - [BeautifulSoup4のチートシート(セレクターなど)]
(https://python.civic-apps.com/beautifulsoup4-selector/)
いつも参考にさせていただきありがとうございます。

###POST(発展)
PHPを使ったHPの中には、スクレイピングに少し工夫が必要な場合もあります。
例えば消費者庁のリコール情報サイトで、リコール情報をスクレイピングするには、https://www.recall.caa.go.jp/result/index.phpにパラメータをPOSTする必要があります。

参考:Pythonでリコール情報をスクレイピングしてmongoDBに保存 特定メーカーの情報をスクレイピング
表示されるURLはhttps://www.recall.caa.go.jp/result/index.phpで、正しいhtmlが取得できない。パラメータはPOSTされているみたい。

このような「クライアント側から送られてきた内容をもとに、サーバ側で同じURLで表示する内容を変えるタイプのホームページ」は、ブラウザではPOSTするパラメータをフォームに入力するなどして渡します。
パラメータをPOSTで渡す必要があるため、requests.getでは取得できません。
requests.postでパラメータを受け渡すことで、目的のページを取得することができます。

具体的には、Requestsライブラリで下記のようにPOSTリクエストすることで、リクエストに対応したレスポンスの取得ができます。

2BSpost.png
例として、下記にPOSTするHP(HTML)とPOSTを受け取るHP(sampleIndex.php)ならびに、スクレイピングの例(post.py)を掲載します。

See the Pen samplePost by Mizuki Ohno (@Moh_no) on CodePen.

sampleIndex.php
<!DOCTYPE html>
<html lang="ja">
  <!-- ### 中略 ### -->
  <body>
    <h2>あなたの好きなカレー情報</h2>
    <?php
    if(isset($_POST["carry"])){
      $carry = $_POST["carry"];
      if (isset($_POST["name"])){
        $name = $_POST["name"];
      } else {
        $name = "あなた";
      }
      echo "<p name='you'>", $name, "の好きなカレー:", $carry, "カレー</p>";
    }
    ?>
  </body>
</html>
post.py
import requests
from bs4 import BeautifulSoup

# HTMLの取得(POST)
payload = {"name": "カレー大好き太郎", "carry": "ポーク"}
req = requests.post("sampleIndex.php", payload)
req.encoding = req.apparent_encoding # 日本語の文字化け防止

# HTMLの解析
bsObj = BeautifulSoup(req.text,"html.parser")

# 要素の取得
item = bsObj.find("p", name="you").text
print(item)

これで、任意の入力に対応したWebページを取得、解析することができます。
このスクリプトの実行結果は以下になります。

カレー大好き太郎の好きなカレー:ポークカレー

sampleIndex.php は、任意のテキストをサーバ側に送信しないと、スクレイピング対象(pタグ)が出力されません。
これはブラウザでは、POSTするHPのフォームなどに入力して送信します。

post.pyでは、辞書型のpayload = {"name": "カレー大好き太郎", "carry": "ポークカレー"}を、requestsライブラリによりrequests.post("sampleIndex.php", payload)でsampleIndex.phpに送信(POST)しています。
POSTを受け取ったsampleIndex.phpは、ブラウザのフォームでそれぞれの値を受け取った時と同様の結果を返します。
これによって、POSTされた情報をもとに構築されるページについても取得ができました。

#Selenium + Beautiful Soup
最近のホームページには、JavaScriptによる動的なコンテンツが埋め込まれるようになってきました。
JavaScriptはクライアント側でHTMLを書き換えるため、requestsによる解決ができません
(サーバ側でページを書き換えるPHPと違い、requestsでパラメータを渡しても目的の要素を獲得できない)。
目的の要素を表示させるためには、webdriverでブラウザを操作する必要があります(ボタンを押す、タブを変更するなど)。

今回は、Google ChromeのWebDriverであるChromeDriverと、それを操作するライブラリであるSeleniumを使用します。
Seleniumは特に動的ページに強く、requestsとはまた違ったスクレイピングのアプローチが期待できます。

###インストール (WebDriverなど)
今回はGoogle Chromeを使用するため、そもそもChromeが入っていない場合はインストールが必要です。
GUI環境があるのでしたら、Google公式HPからインストーラーをダウンロードして、インストールするのが直感的にわかりやすいですね。
UbuntuなどDebian系環境でしたら、下記コマンドでインストールできます。

$ sudo sh -c 'echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
$ sudo wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
$ sudo apt-get update
$ sudo apt-get install google-chrome-stable

ChromeDriverもSeleniumも、同様にpipでインストールできます。
しかしながら、ChromeDriverをpip install chromedriver-binaryでインストールしてしまうと、開発版をインストールしてしまいうまく動かない危険性があります。自分のChromeのバージョンに合わせたドライバをインストールしてください。
Chromeのバージョンは、Google Chromeの右上メニューより「設定→Google Chromeについて」を選択して確認してください。
Debian系の環境でしたらapt list --installed google*で出力されたリストのgoogle-chrome-stable項目を見れば、Chromeのバージョンがわかります。

$ pip install chromedriver-binary==<Chromeバージョン番号>
$ pip install selenium

Google Chromeのバージョンが86.0.4240.111の場合pip install chromedriver-binary==86.0.4240.111となります。

chromedriverのインストールがうまくいかない場合、下記の記事に従って、別の方法でインストールを試みてください。

###Seleniumでスクレイピング
Seleniumによるスクレイピングは以下のように行います。

3Selenium.png
例えば下記のJavaScriptを使用したページは、ブラウザ上でボタンを押すと<div id="figure"></div><div class=“対応するCSS” name="対応するテキスト"></div>が追加される仕組みとなっています。
つまり、ボタンを押したら現れるid="image"の中身をスクレイピングしたい場合、Webドライバでまず「ボタンを押す」必要があります。

See the Pen sampleSelenium by Mizuki Ohno (@Moh_no) on CodePen.

このようなJavaScriptを用いる動的なページも、下記のようなコードで解析ならびに要素の抽出ができます(例:selenium.py)。

selenium.py
from selenium import webdriver
import chromedriver_binary
import time

# ドライバ設定とURL取得
driver = webdriver.Chrome()
driver.get("sampleSelenium.html")
time.sleep(0.5) # 読み込み時間を考慮

# ブラウザの操作
bottum = driver.find_element_by_id("bottun3") # 要素の取得
bottum.click()# ボタンの押下
time.sleep(0.5) # 読み込み時間を考慮

# 要素の取得
fig = driver.find_element_by_xpath("//div[@id='figure']/div[1]") # 要素の取得
name = fig.get_attribute("name") # 属性の取得
print(name)

# WebDriverの終了
driver.close()
driver.quit()

結果は以下のようになります。

カレーパンの絵

Seleniumのメソッドの簡単な説明
Seleniumには便利なメソッドが多数存在しますが、ここでは特に上記のスクリプトで使用されているメソッドについて説明します。

・get
driver.get("URL")でURLを指定したウェブページをブラウザで開く。

・find_elements
指定した要素を全て取得する。
Seleniumによる要素の検索は主に、ID属性, class属性, name属性, XPathで行う。
figs = driver.find_elements_by_id("figure")のfigsは、id="figure"の要素を全て取得する。

・find_element
指定した要素を1つ取得する(最初にヒットした要素を返す)。
fig = driver.find_element_by_id("figure")のfigは、id="figure"の要素を最初の一つだけ取得する。
上記のfig = driver.find_element_by_xpath("//div[@id='figure']/div[1]")はXPathを使用した例で、指定したパスの要素を取得できる。
(XPathの//は、HTML内の全要素から検索するの意味をもつ)

・get_attribute
指定した属性を取得する。基本的に、事前にfind_elementで要素を取得して、対象を絞って実行する。

・click
find_elementで取得した要素をクリックする。

・close / quit
ウェブドライバの終了時には、driver.close()driver.quit()を両方記載して、ブラウザを閉じる必要がある。

Seleniumのメソッドについて、より詳しく知りたい方は、有識者の方々がこれらの記事に詳しくまとめてくださっています。
 - [Selenium webdriverよく使う操作メソッドまとめ]
(https://qiita.com/mochio/items/dc9935ee607895420186)
 - Python + Selenium で Chrome の自動操作を一通り
いつも参考にさせていただきありがとうございます。

#SeleniumとBeautiful Soupでスクレイピング
Seleniumはウェブドライバであるため、Beauchiful Soupと比べてひとつひとつの動作が遅いです。さらにWebページの読み込み時間があるため、予想外のエラーが生じることがあります。
そこで、Beautiful Soupと組み合わせて使うことで、安定化と使いやすさの向上を目指します。

4SeleniumBS.png
Selenium + Beautiful Soupでselenium.pyと同様のスクレイピングを行うと、下記のようになります。

seleniumBS.py
from selenium import webdriver
from selenium.webdriver.support.ui import Select
import chromedriver_binary
from bs4 import BeautifulSoup
import time

# ドライバ設定とURL取得
driver = webdriver.Chrome()
driver.get("sampleSelenium.html")
time.sleep(0.5) # 読み込み時間を考慮

# ブラウザの操作
bottum = driver.find_element_by_id("button3")
bottum.click() # リンクの押下
time.sleep(0.5) # 読み込み時間を考慮

# BSによるHTMLの解析
html = driver.page_source.encode("utf-8") # HTMLのエンコード
bsObj = BeautifulSoup(html, "html.parser")

# タグを指定した要素の抽出
items = bsObj.find("div", id="figure")
item = items.find("div").get("name")
print(item)

# WebDriverの終了
driver.close()
driver.quit()

結果は以下のようになります。

カレーパンの絵

Seleniumの比重を下げることで、長時間に及ぶスクレイピングの動作が安定し、エラーが少なくなります。
何より、要素の抽出が簡単で書きやすいです(私見)。
したがって、動的な要素のスクレイピングにはこちらの方法をオススメします。

#注意事項
スクレイピングは自動でWebページにアクセスすることから、DoS攻撃にならないようにしましょう。
特にseleniumの場合、短時間に操作を行いすぎると、サーバ側に負荷をかけます。time.sleepなどで定期的に実行を休ませてください。
「HTTP 500番台エラー」が帰ってくる場合、サーバエラーを引き起こしている可能性があります。500番台のエラーが発生した時には、処理を止める、コードを負荷をかけない様式に見直す、スクレイピングは間隔を開けて行うなど、気をつけましょう。
また、スクレイピングを利用規約で許可していないページも存在するため(例:Twitter)注意しましょう。

#おわりに
大半のWebページは、以下の2パターンでスクレイピングすることができます。
静的なページ Requests + Beautiful Soup
動的なページ Selenium + Beautiful Soup

本記事では、それぞれのライブラリについてよく使うものをピックアップしてご紹介しました。本記事が、スクレイピングを「手軽に始める」きっかけになれば幸いです。


追記
せっかく作ったスクレイピングコード。じゃあ、どうやってこれを運用すればいいの?
そんなあなたのために、下記の記事を投稿しました。
クラウドを利用して、手軽に定期実行型のクローラーを作ってみよう!
クラウドで手軽に始めるクローリング

#参考
Python, Requestsの使い方 | note.nkmk.me
Requestsで日本語を扱うときの文字化けを直す
PythonとBeautiful Soupでスクレイピング
[Beautiful Soup のfind_all( ) と select( ) の使い方の違い]
(https://gammasoft.jp/blog/difference-find-and-select-in-beautiful-soup-of-python/)
[BeautifulSoup4のチートシート(セレクターなど)]
(https://python.civic-apps.com/beautifulsoup4-selector/)
BeautifulSoup4で親子、兄弟、前後要素の検索方法
[Pythonでリコール情報をスクレイピングしてmongoDBに保存]
(https://qiita.com/se_fy/items/b2fd8edd8dbba637668a)
[PHPでフォームからデータを受け取る方法(GETとPOST)]
(https://techacademy.jp/magazine/4955)
Ubuntuにchromedriverをインストールする
UbuntuにChromeをインストールする
UbuntuにGoogle Chromeを簡単にインストールする方法
[Pythonでseleniumを用いたスクレイピング]
(https://qiita.com/yuta_ichi/items/0be6671fbaea842d9a38)
[Selenium webdriverよく使う操作メソッドまとめ]
(https://qiita.com/mochio/items/dc9935ee607895420186)
Python + Selenium で Chrome の自動操作を一通り
WebElement 内部を find_element_by_xpath() で再検索した時のつまずき

65
71
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
65
71

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?