Beautifulsoup触ってみた

beautifulsoupで映画レビューサイトにて酷評されまくっている「ドラゴンクエスト・ユアストーリー」のページをスクレイピングしてみました。
絶対に見に行かないでみたいなレビューが多いので逆に気になる。

## 環境
version: Python3.7.4
IDE: Pycharm無料版

## 準備
pip install beautifulsoup4
pip install requests
pip install lxml

### Beautiful Soupとは
Pythonのスクレイピングに特化したライブラリです。
一択なので選択の余地なしです。

### Requestsとは
Pythonの外部通信用のライブラリでヘッダー情報をbotっぽくなく書き換えられるので、これを使います。

### lxmlとは
PythonのHTMLパーサー、選択肢としては `html.parser`, `html5lib`, `lxml` の３つがありますが、この中で一番速い`lxml`を使うことにします。

## とりあえず使ってみる
スクレイピングする場合はまずはhtmlの構造を確認します。

以下は映画レビューサイトのドラゴンクエスト・ユアストーリーのページのhtmlです。

```
<h1 class="text-xxlarge no-space-bottom">
  <span>ドラゴンクエスト　ユア・ストーリー</span>
  <small class="text-small">(2019)</small>
</h1>
- 略 -
<span class="rating-score">
  <span>2.70</span>
  <small> 点</small>
  <small class="text-xsmall"> / 評価:2,750件</small>
</span>
```

pythonコード(scraper.py)

```python
import requests
import lxml
from bs4 import BeautifulSoup

# User-AgentはデベロッパーツールでNetworkタブからRequest Headersを確認してそれをそのまま使用
headers = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"
}

url = '映画レビューサイトのURL'
html = requests.get(url, headers = headers)
soup = BeautifulSoup(html.text, 'lxml')

# ファイルに書き出す
fp = open('dq.txt','w+')
# h1タグをそのまま抜き出して書き出す(上のh1タグの中身と同じものが書き出される)
fp.write(soup.find('h1').prettify() + '\n')
fp.close()
```

ファイルの実行
`$ python venv/scraper.py`

## BeautifulSoup 各メソッド

### prettify()

```python
#htmlをそのまま書き出す
fp.write(soup.find(class_='rating-score').prettify())

# 出力結果
# <span class="rating-score">
#   <span>2.71</span>
#   <small> 点</small>
#   <small class="text-xsmall"> / 評価:2,734件</small>
# </span>
```

### text, get_text()

```python
#文字列のみを書き出す
fp.write(soup.find(class_='rating-score').text)
fp.write(soup.find(class_='rating-score').get_text())

# 出力結果
# 2.70
# 点
# / 評価:2,755件

# get_text()は引数なしだとtextと同じ
# get_text()はセパレーターを設定するときに使用

fp.write(soup.find('h1').get_text(separator=":", strip=True))

# 出力結果
# 2.70:点:/ 評価:2,755件
```

### find

```python
# 条件に合致した最初の要素を１つだけ返す
# タグを.で直接指定してもfind()と同じ意味になるので以下は同じものを返す
soup.find('h1')
soup.h1


# rating-scoreで囲まれた<span>2.70</span>だけ取得
fp.write(soup.find(class_='rating-score').span.get_text())

# 出力結果
# 2.70

# rating-scoreの中にはsmallタグが２つあるが、それぞれ取得
rating = soup.find(class_='rating-score')
fp.write(rating('small')[0].text)
#=> 点

fp.write(rating('small')[1].text)
#=> / 評価:2,755件

# リスト形式となっているのでfor文でも回せる
rating = soup.find(class_='rating-score')
for small in rating('small'):
  fp.write(f"{small.text}\n")

# 出力結果
# 点
# / 評価;2,755件
```


### find_all


```python
# 条件に合致したものはすべて配列(厳密には)に入れて返す

small_tags = soup.find(class_='rating-score').find_all('small')
for small in small_tags:
  fp.write(f"{small.text}\n")

# 出力結果
# 点
# / 評価;2,755件


# 以下の構造のhtmlでfind_allを試す
fp.write(f"{soup.find(class_='gridnav').prettify()}\n")

# 出力結果
'''
<ul class="gridnav grid4 text-small">
 <li class="is-active">
  <a href="/movie/367173/">
   作品トップ
  </a>
 </li>
 <li class="">
  <a href="/movie/367173/story/">
   解説・あらすじ
  </a>
 </li>
 <li class="">
  <a href="/movie/367173/credit/">
   キャスト・スタッフ
  </a>
 </li>
 <li class="">
  <a href="/movie/367173/review/">
   ユーザーレビュー
  </a>
 </li>
 <li class="">
  <a href="/movie/367173/photo/">
   フォトギャラリー
  </a>
 </li>
 <li class="">
  <a href="/movie/367173/trailer/">
   本編/予告/関連動画
  </a>
 </li>
 <li class="">
  <a href="/movie/367173/schedule/">
   上映スケジュール
  </a>
 </li>
 <li class="is-disabled">
  <span>
   <i class="icon-rental">
   </i>
   レンタル情報
  </span>
 </li>
</ul>
'''

# 上のhtmlのaタグのリンクのみ取得
# 属性はget('属性名')で取得
urls = soup.find(class_='gridnav').find_all('a')
for url in urls:
  fp.write(f"{url.get('href')}\n")

# 出力結果
'''
/movie/367173/
/movie/367173/story/
/movie/367173/credit/
/movie/367173/review/
/movie/367173/photo/
/movie/367173/trailer/
/movie/367173/schedule/
'''
```

基本はfind_all, findでディグダグするだけなので非常に簡単です