Edited at

10分で理解する Beautiful Soup


Beautiful Soup とは

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 をインストールします。

$ pip install beautifulsoup4


Beautiful Soup を使ってクローラーを書く

HTML の取得は requests を使い、HTML のパース処理を Beautiful Soup でやるのが基本的な使い方です。

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() を使うことができます。

# 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 セレクターの場合は階層を一括で指定する事ができるので、どの要素を取得しようとしているかがパッで分かりやすいですし、途中の要素が存在しなかった場合のハンドリングがシンプルになるのでこちらの方がわかりやすいと感じます。

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 を見てみましょう。

<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() を使うと指定したテキストを含む要素が取得でき、同じ階層の隣接ノードにアクセスするには ~ を使います。

これらを使って以下のように書くと目的のタグ名が全て取得できます。

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() を使います。

これが地味にめちゃくちゃ便利で、テキストに含まれる改行やスペースをトリムしたり、複数の要素を任意の文字列で結合する事ができます。


改行、空白文字をトリムする

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

html = '<div>\n   aaa\n  </div>'

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

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

soup.find('div').get_text(strip=True)

# > 'aaa'


複数のテキストの間に区切り文字を入れる

以下のような HTML 構造があった場合に div 要素に対して get_text() を実行すると、その子要素に含まれるテキストは全て結合された形で取得されます。

html = """<div>

<span>text1</span>
<span>text2</span>
<span>text3</span>
</div>"""

soup = BeautifulSoup(html, 'html.parser')
soup.find('div').get_text(strip=True)
# > 'text1text2text3'

この時 get_text() の第一引数に文字列を指定すると、子要素の各テキストの間に指定した文字を入れた文字列が取得できます。

soup.find('div').get_text(',', strip=True)

# > 'text1,text2,text3'


複数のテキストを別々に取得する

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

# 戻り値はジェネレーターで取得されます

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 オブジェクトを生成することでこれまでと同様に使うことができます。

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 クラスを別にする設計にすることもできます。


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 を使った方が簡単にできるかもしれません。

また Scrapy と Beautiful Soup を組み合わせて使う方法についても紹介しました。

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

Scrapy についても10分で理解するシリーズを書いているのでぜひこちらも併せて読んでみください!

10分で理解する Scrapy

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