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

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

#目次
- [Requests + Beautiful Soup](#requests--beautiful-soup)
 - [インストール](#インストール)
 - [GET（基本）](#get基本)
 - [POST（発展）](#post発展)
- [Selenium + Beautiful Soup](#selenium--beautiful-soup)
 - [インストール](#インストール-webdriverなど)
 - [Seleniumでスクレイピング](#seleniumでスクレイピング)
 - [SeleniumとBeautiful Soupでスクレイピング](#seleniumとbeautiful-soupでスクレイピング)

#Requests + Beautiful Soup
[Requests](https://requests.readthedocs.io/en/master/)はPythonで使える便利なHTTPライブラリです。今回はHTTPのGETメソッドならびにPOSTメソッドのために使用します（Webページ情報の取得）。
[Beautiful Soup](https://www.crummy.com/software/BeautifulSoup/bs4/doc/)はHTMLやXMLの解析、ならびに要素の抽出に用いられるPythonのモジュールです。

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

```
$ pip install requests
$ pip install bs4
```
###GET（基本）
Requests + Beautiful Soupによるスクレイピングは、以下の手順で実行されます。

![1BSget.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652177/c3ee3a15-e78a-6087-08ab-95aaf3f568a6.png)
下記のようにHTMLやXMLのみで書かれたページのほとんどが、下記```get.py```のような簡単なコードで解析ならびに要素の抽出ができます。

<p class="codepen" data-height="265" data-theme-id="dark" data-default-tab="html,result" data-user="Moh_no" data-slug-hash="YzWvRaV" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="sampleGet">
  <span>See the Pen <a href="https://codepen.io/Moh_no/pen/YzWvRaV">
  sampleGet</a> by Mizuki Ohno (<a href="https://codepen.io/Moh_no">@Moh_no</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

```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は、３つの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でスクレイピング](https://qiita.com/itkr/items/513318a9b5b92bd56185)
　- [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に保存　特定メーカーの情報をスクレイピング](https://qiita.com/se_fy/items/b2fd8edd8dbba637668a#%E7%89%B9%E5%AE%9A%E3%83%A1%E3%83%BC%E3%82%AB%E3%83%BC%E3%81%AE%E6%83%85%E5%A0%B1%E3%82%92%E3%82%B9%E3%82%AF%E3%83%AC%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0)
>表示されるURLは`https://www.recall.caa.go.jp/result/index.php`で、正しいhtmlが取得できない。パラメータはPOSTされているみたい。

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

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

![2BSpost.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652177/bdd73585-670b-49d3-213a-90e7ff59ab36.png)
例として、下記にPOSTするHP（HTML）とPOSTを受け取るHP（```sampleIndex.php```）ならびに、スクレイピングの例（```post.py```）を掲載します。

<p class="codepen" data-height="265" data-theme-id="dark" data-default-tab="html,result" data-user="Moh_no" data-slug-hash="xxEGLEm" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="samplePost">
  <span>See the Pen <a href="https://codepen.io/Moh_no/pen/xxEGLEm">
  samplePost</a> by Mizuki Ohno (<a href="https://codepen.io/Moh_no">@Moh_no</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.sfo2.cdn.digitaloceanspaces.com/assets/embed/ei.js"></script>

```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](https://sites.google.com/a/chromium.org/chromedriver/downloads)と、それを操作するライブラリである[Selenium](https://selenium-python.readthedocs.io/)を使用します。
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のインストールがうまくいかない場合、下記の記事に従って、別の方法でインストールを試みてください。
  - [Ubuntuにchromedriverをインストールする](https://qiita.com/pyon_kiti_jp/items/0ba98f140142d4ac6b25)

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

![3Selenium.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652177/738666ae-aab7-7118-f91d-515e91d912d4.png)
例えば下記のJavaScriptを使用したページは、ブラウザ上でボタンを押すと```<div id="figure"></div>```に```<div class=“対応するCSS” name="対応するテキスト"></div>```が追加される仕組みとなっています。
つまり、ボタンを押したら現れるid="image"の中身をスクレイピングしたい場合、Webドライバでまず「ボタンを押す」必要があります。

<p class="codepen" data-height="265" data-theme-id="dark" data-default-tab="js,result" data-user="Moh_no" data-slug-hash="jOrKXKW" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="sampleSelenium">
  <span>See the Pen <a href="https://codepen.io/Moh_no/pen/jOrKXKW">
  sampleSelenium</a> by Mizuki Ohno (<a href="https://codepen.io/Moh_no">@Moh_no</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

このような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 の自動操作を一通り](https://qiita.com/memakura/items/20a02161fa7e18d8a693)
いつも参考にさせていただきありがとうございます。

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

![4SeleniumBS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652177/23ca554c-3d04-9214-fa86-4b65178b551f.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**

本記事では、それぞれのライブラリについてよく使うものをピックアップしてご紹介しました。本記事が、スクレイピングを「手軽に始める」きっかけになれば幸いです。
<br>
**追記**
せっかく作ったスクレイピングコード。じゃあ、どうやってこれを運用すればいいの？
そんなあなたのために、下記の記事を投稿しました。
クラウドを利用して、手軽に定期実行型のクローラーを作ってみよう！
[クラウドで手軽に始めるクローリング](https://qiita.com/Moh_no/items/e8902ec40df7ddf9448a)


#参考
[Python, Requestsの使い方 | note.nkmk.me](https://note.nkmk.me/python-requests-usage/)
[Requestsで日本語を扱うときの文字化けを直す](https://qiita.com/nittyan/items/d3f49a7699296a58605b)
[PythonとBeautiful Soupでスクレイピング](https://qiita.com/itkr/items/513318a9b5b92bd56185)
[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で親子、兄弟、前後要素の検索方法](https://senablog.com/python-bs4-search/)
[Pythonでリコール情報をスクレイピングしてmongoDBに保存]
(https://qiita.com/se_fy/items/b2fd8edd8dbba637668a)
[PHPでフォームからデータを受け取る方法（GETとPOST）]
(https://techacademy.jp/magazine/4955)
[Ubuntuにchromedriverをインストールする](https://qiita.com/pyon_kiti_jp/items/0ba98f140142d4ac6b25)
[UbuntuにChromeをインストールする](https://qiita.com/pyon_kiti_jp/items/e6032eb6061a4774aece)
[UbuntuにGoogle Chromeを簡単にインストールする方法](https://www.karelie.net/how-to-install-google-chrome-on-ubuntu/#toc_)
[Pythonでseleniumを用いたスクレイピング]
(https://qiita.com/yuta_ichi/items/0be6671fbaea842d9a38)
[Selenium webdriverよく使う操作メソッドまとめ]
(https://qiita.com/mochio/items/dc9935ee607895420186)
[Python + Selenium で Chrome の自動操作を一通り](https://qiita.com/memakura/items/20a02161fa7e18d8a693)
[WebElement 内部を find_element_by_xpath() で再検索した時のつまずき](https://qiita.com/ttn_tt/items/948b4c9f03c1460504b9)