10分で理解する Beautiful Soup

# [Beautiful Soup](https://www.crummy.com/software/BeautifulSoup/bs4/doc/) とは
HTML や XML から狙ったデータを抽出するためのライブラリです。
公式ドキュメントの冒頭の説明を見るとこれは HTML や XML のパーサーそのものではなく、パーサーをラップして扱いやすくするライブラリのようです。

> Beautiful Soup is a Python library for pulling data out of HTML and XML files. It works with your favorite parser to provide idiomatic ways of navigating, searching, and modifying the parse tree. It commonly saves programmers hours or days of work.

この記事では Beautiful Soup の基本的な使い方と、実践で役に立つ Tips をいくつかご紹介します。

# TL;DR
- CSS セレクターを使えばだいたいの場合で困らない
- `get_text()` めちゃ便利
- Scrapy と組み合わせても使える

# 基本的な使い方
## インストール
pip で `beautifulsoup4` をインストールします。

```bash
$ pip install beautifulsoup4
```

## Beautiful Soup を使ってクローラーを書く
HTML の取得は [requests](https://2.python-requests.org/en/master/) を使い、HTML のパース処理を Beautiful Soup でやるのが基本的な使い方です。


``` python
import requests
from bs4 import BeautifulSoup

# スクレイピング対象の URL にリクエストを送り HTML を取得する
res = requests.get('http://quotes.toscrape.com/')

# レスポンスの HTML から BeautifulSoup オブジェクトを作る
soup = BeautifulSoup(res.text, 'html.parser')

# title タグの文字列を取得する
title_text = soup.find('title').get_text()
print(title_text)
# > Quotes to Scrape

# ページに含まれるリンクを全て取得する
links = [url.get('href') for url in soup.find_all('a')]
print(links)
# > ['/', '/login', '/author/Albert-Einstein', '/tag/change/page/1/', '/tag/deep-thoughts/page/1/', '/tag/thinking/page/1/', '/tag/world/page/1/', '/author/J-K-Rowling', '/tag/abilities/page/1/', '/tag/choices/page/1/', '/author/Albert-Einstein', '/tag/inspirational/page/1/', '/tag/life/page/1/', '/tag/live/page/1/', '/tag/miracle/page/1/', '/tag/miracles/page/1/', '/author/Jane-Austen', '/tag/aliteracy/page/1/', '/tag/books/page/1/', '/tag/classic/page/1/', '/tag/humor/page/1/', '/author/Marilyn-Monroe', '/tag/be-yourself/page/1/', '/tag/inspirational/page/1/', '/author/Albert-Einstein', '/tag/adulthood/page/1/', '/tag/success/page/1/', '/tag/value/page/1/', '/author/Andre-Gide', '/tag/life/page/1/', '/tag/love/page/1/', '/author/Thomas-A-Edison', '/tag/edison/page/1/', '/tag/failure/page/1/', '/tag/inspirational/page/1/', '/tag/paraphrased/page/1/', '/author/Eleanor-Roosevelt', '/tag/misattributed-eleanor-roosevelt/page/1/', '/author/Steve-Martin', '/tag/humor/page/1/', '/tag/obvious/page/1/', '/tag/simile/page/1/', '/page/2/', '/tag/love/', '/tag/inspirational/', '/tag/life/', '/tag/humor/', '/tag/books/', '/tag/reading/', '/tag/friendship/', '/tag/friends/', '/tag/truth/', '/tag/simile/', 'https://www.goodreads.com/quotes', 'https://scrapinghub.com']

# class が quote の div 要素を全て取得する
quote_elms = soup.find_all('div', {'class': 'quote'})
print(len(quote_elms))
# > 10
```

※本記事のサンプルコードでは Scrapy のチュートリアルで利用されている http://quotes.toscrape.com/ に対してアクセスしてます。
クロールする場合はクロール先サイトの robots.txt や利用規約をよく確認して行いましょう。


# 実践で使える Tips 
Beautiful Soup を使ってスクレイピングをする場合に、個人的にどういう使い方をしているかをいくつかサンプルでご紹介します。

## CSS セレクターを使って抽出
Beautiful Soup のチュートリアルや色々なサンプルでは `find_xxx` 系のメソッドがよく使われていますが、個人的には CSS セレクターを使う場面がほとんどです。

CSS セレクターを使って要素を取得したい場合、複数の要素を取得する場合は `select()` 1つだけ取得する場合は `select_one()` を使うことができます。

``` python
# CSS セレクターを使って author を全て取得する
author_names = [n.get_text() for n in soup.select('div.quote small.author')]
print(author_names)
# > ['Albert Einstein', 'J.K. Rowling', 'Albert Einstein', 'Jane Austen', 'Marilyn Monroe', 'Albert Einstein', 'André Gide', 'Thomas A. Edison', 'Eleanor Roosevelt', 'Steve Martin']
```

ページによってその要素が存在するかしないかが分からないけども、存在するページの場合は取得したいという場面がよくあります。
その場合に find 系のメソッドの条件では要素を1つづつ指定する必要がありますが、 CSS セレクターの場合は階層を一括で指定する事ができるので、どの要素を取得しようとしているかがパッで分かりやすいですし、途中の要素が存在しなかった場合のハンドリングがシンプルになるのでこちらの方がわかりやすいと感じます。

``` python
def get_piyo_text():
    piyo = soup.select_one('div.hoge div.fuga div.piyo')
    if not piyo:
        return None
    return piyo.get_text()
```

実際のスクレイピングのコードではこのようなパターンはとてもよく使うので、CSS セレクターを使うとこういった場合に特に便利に感じます。

公式ドキュメントの `CSS selectors` のセクションも併せて見てみてください。
https://www.crummy.com/software/BeautifulSoup/bs4/doc/#css-selectors

## テキストを起点に抽出する
世の中にある様々な Web サイトは ID やクラスを適切に指定してくれている綺麗な構造のサイトばかりではないので、サイトによっては「テーブルの1列目にXXと書いてある次の列のテキストを取得する」というような抽出が必要になる場合もよくあります。

http://quotes.toscrape.com/ のページの右側にある `Top Ten tags` のタイトルの下にあるタグ名を取得してみるのをこの方法でやってみます。
`Top Ten tags` のところの HTML を見てみましょう。

``` html
<div class="col-md-4 tags-box">
        
            <h2>Top Ten tags</h2>
            
            <span class="tag-item">
            <a class="tag" style="font-size: 28px" href="/tag/love/">love</a>
            </span>
            
            <span class="tag-item">
...
```

テキストに `Top Ten tags` と書かれている h2 要素と横並びになっている span 要素を取得すれば目的のタグ名が取得できる事がわかります。
これを取得する CSS セレクターは `h2:contains("Top Ten tags") ~ span` となります。

Beautiful Soup は関係なく一般的な CSS セレクターの書き方ですが、 `:contains()` を使うと指定したテキストを含む要素が取得でき、同じ階層の隣接ノードにアクセスするには `~` を使います。
これらを使って以下のように書くと目的のタグ名が全て取得できます。

``` python
tag_items = soup.select('h2:contains("Top Ten tags") ~ span')
print([t.get_text(strip=True) for t in tag_items])
# > ['love', 'inspirational', 'life', 'humor', 'books', 'reading', 'friendship', 'friends', 'truth', 'simile']
```
※本来は `span.tag-item` だけで取得できるのでサンプルのためにあえて遠回りなことをやっていますが

## テキストを取得する
ここまでのサンプルでも使っていますが、指定した要素に含まれるテキストを取得するには `get_text()` を使います。
これが地味にめちゃくちゃ便利で、テキストに含まれる改行やスペースをトリムしたり、複数の要素を任意の文字列で結合する事ができます。

### 改行、空白文字をトリムする
例えば以下のサンプルのように取得するテキストによっては前後に改行やスペースが含まれている場合があります。

``` python
html = '<div>\n   aaa\n  </div>'
soup = BeautifulSoup(html, 'html.parser')
soup.find('div').get_text()
# > '\n   aaa\n  '
```

この時 `get_text()` の引数に `strip=True` を指定するとテキストに含まれる改行や空白文字を除去してくれます。

``` python
soup.find('div').get_text(strip=True)
# > 'aaa'
```

## 指定した要素の属性を取得する
指定した要素の属性を取得するには要素に対して `element['attr_name']` のように要素名を指定して取得ができます。

```python
html = """<a href="http://hoge.hoge">リンクテキスト</a>"""
soup = BeautifulSoup(html, 'html.parser')

# href を取得する
soup.find('a')['href']
# > 'http://hoge.hoge'

# リンクのテキストを取得する
soup.find('a').get_text()
# > 'リンクテキスト'
```

### 複数のテキストを別々に取得する
指定した要素の `.stripped_strings` のプロパティを使うと、小要素に含まれるテキストを1つづつ返すジェネレーターを取得することができます。

```python
# 戻り値はジェネレーターで取得されます
soup.find('div').stripped_strings
# > <generator object Tag.stripped_strings at 0x10677d750>

# list に変換するか for でイテレーションすることでテキストを1つづつ利用できます
list(soup.find('div').stripped_strings)
# > ['text1', 'text2', 'text3']
```

これらとても細かいですが実際のスクレイピングではよく使う処理だと思うので覚えておくととても便利です。

公式ドキュメントの `get_text()` のセクションも併せて見てみてください。
https://www.crummy.com/software/BeautifulSoup/bs4/doc/#get-text

## Scrapy と Beautiful Soup を組み合わせて使う
Scrapy と Beautiful Soup を組み合わせて使うのも簡単にできます。
コールバックで呼ばれる parse メソッドの中でレスポンスの内容を取得して BeautifulSoup オブジェクトを生成することでこれまでと同様に使うことができます。

```python
import scrapy
from bs4 import BeautifulSoup


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
    ]

    def parse(self, response):
        # レスポンスから BeautifulSoup オブジェクトを生成する
        soup = BeautifulSoup(response.text, 'html.parser')
        
        # 通常の Beautiful Soup と同じように使うことができる
        title_text = soup.find('title').get_text(strip=True)
```

例えばクローリングとスクレイピングの責務を明確に分けて以下のように Parser クラスを別にする設計にすることもできます。

```python

class Parser(object):
    def __init__(self, html: str):
        self._soup = BeautifulSoup(html, 'html.parser')
    
    def parse_title(self):
        return self._soup.find('title').get_text(strip=True)

class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
    ]

    def parse(self, response):
        # レスポンスから Parser オブジェクトを生成する
        parser = Parser(response.text)
        title_text = parser.parse_title()
```

この場合 Parser 単体でテストできるようになりますしクローリングとスクレイピングのそれぞれを修正する場合に影響範囲が明確になりかなりメンテナンスがし易くなるのでオススメです。

# まとめ
さて、今回は Beautiful Soup の使い方についてざっくりと説明しました。

今回紹介した方法の他にも正規表現を使って要素を取得することもできたりとスクレイピングに必要な処理は一通り揃っています。
https://www.crummy.com/software/BeautifulSoup/bs4/doc/#a-regular-expression

だいたいの要素の取得には CSS セレクターを使えば取得できますが、親の階層をいくつか登ってから降りるような指定をやろうとすると少し難しいので、その場合は多少複雑になりますが `.parent` などを使ってツリーを辿っていく事で解決できます。
https://www.crummy.com/software/BeautifulSoup/bs4/doc/#going-up
こういう場合には xpath を使えれば一発で指定できて便利だったりするので [lxml](https://lxml.de/) を使った方が簡単にできるかもしれません。

また Scrapy と Beautiful Soup を組み合わせて使う方法についても紹介しました。
あるサイトの全ページをスクレイピングしたい場合や特定のパターンの URL を全て走査したい場合な、 `requests` だけでクロール処理を書こうとすると地味に大変なので、そういった場合はクロールの処理は Scrapy を使ってスクレイピングの処理に Beautiful Soup を使うという組み合わせが便利だと思います。

Scrapy についても10分で理解するシリーズを書いているのでぜひこちらも併せて読んでみください！
[10分で理解する Scrapy](https://qiita.com/Chanmoro/items/f4df85eb73b18d902739)


それでは Beautiful Soup の使い方を理解して、今日も1日楽しいスクレイピングライフを！