BeautifulSoupの美しさを知りませんでした。実際に使ってみるまでは

43, 2019-07-13

目次

BeautifulSoup4を使って簡単にパースしよう

こんにちは、narupoです。

スクレイピングなどでおなじみのHTML&XML解析器の「BeautifulSoup4(ビューティフル・スープ・4)」、その使い方の一部をご紹介します。
(この記事では「bs4」を扱います)

この記事を読めばBeautifulSoup4の美しさと使い方がわかるかもしれません。
BeautifulSoup4で快適なスクレイピング道を追求しましょう。

ペロ、この味はBeautifulSoup!

BeautifulSoup4のインストール

pipなどを使って環境にbeautifulsoup4をインストールします。

$ pip install beautifulsoup4

BeautifulSoup4のインポート

BeautifulSoup4をインポートして使うには↓のようにします。
bs4というネームスペースを使っていることに注意が必要です。

from bs4 import BeautifulSoup

コンストラクタ

BeautifulSoup4はコンストラクタにHTMLなどのマークアップを渡すとパースしてくれます。
しかし、bs4では↓のような警告が出ます。

from bs4 import BeautifulSoup

markup = ''
soup = BeautifulSoup(markup)
# main.py:13: UserWarning: No parser was explicitly specified, so I'm using the best available HTML parser for this system ("html.parser"). This usually isn't a problem, but if you run this code on another system, or in a different virtual environment, it may use a different parser and behave differently.
# 
# The code that caused this warning is on line 13 of the file main.py. To get rid of this warning, pass the additional argument 'features="html.parser"' to the BeautifulSoup constructor.

print(type(soup))
# <class 'bs4.BeautifulSoup'>

BeautifulSoup4は以前は自動で内部で使用するパーサーを選択してくれていたらしいのですが、バージョンが新しくなってからコントラクト時にユーザーがパーサーを指定することが推奨されるようになりました。
背景としては、異なった環境でパーサーが自動判別されると、細かいところのパーサーの差異が問題になってくるからです。
あっちでは動いたのにこっちの環境では動かない……そういうケースを避けるために明示的にパーサーを指定します。

BeautifulSoup4では↓のパーサーをコンストラクト時に選択できます。

  • html.parser
  • lxml HTMLパーサー
  • lxml XMLパーサー
  • html5lib

それぞれのパーサーの特徴を見ていきたいと思います。

html.parser

html.parserはPythonに最初から備わっているHTMLパーサーです。

from bs4 import BeautifulSoup

# Python標準に含まれているhtml.parserを使う
# html.parserは最初から利用できる
# ドキュメントいわく
#
#   * バッテリー付き (?)
#   * まともなスピード
#   * 寛容(Python 2.7.3, 3.2以降は)
#
# なパーサーである
# しかし
# 
#   * とても寛容的でない(Python 2.7.3, 3.2.2より前は)
# 
# という一面もある
# 古いPythonで使うにはあまり推奨できないパーサー
markup = ''
soup = BeautifulSoup(markup, 'html.parser')

lxml HTMLパーサー

lxmlはサードパーティー製のパーサーです。
HTMLパーサーとXMLパーサーを含んでいます。

pipでインストールするには↓のようにします。

$ pip install lxml

↓はHTMLパーサーを使う例です。
lxmlのHTMLパーサーを使うにはコンストラクタに文字列lxmlを渡します。

from bs4 import BeautifulSoup

# サードパーティー製のlxmlのHTMLパーサーを使う
# lxmlはpipなどで環境にインストールする必要がある
# ドキュメントいわく
#
#   * めちゃくちゃ速い
#   * 寛容
#
# なパーサーである
# しかし
# 
#   * Cライブラリに依存
# 
# などの側面がある
# 公式では解析速度のためにこのlxmlを使うことを推奨している
# (If you can, I recommend you install and use lxml for speed.)
markup = ''
soup = BeautifulSoup(markup, 'lxml')

# 環境にlxmlがインストールされていないと↓のようなエラーが出る
# bs4.FeatureNotFound: Couldn't find a tree builder with the features you requested: lxml. Do you need to install a parser library?

lxml XMLパーサー

lxmlのXMLパーサーを使うにはコンストラクタに文字列lxml-xmlか文字列xmlを渡します。

from bs4 import BeautifulSoup

# サードパーティー製のlxmlのXMLパーサーを使う
# lxmlはpipなどで環境にインストールする必要がある
# ドキュメントいわく
#
#   * めちゃくちゃ速い
#   * 現在サポートされている唯一のXMLパーサー
#
# なパーサーである
# しかし
# 
#   * Cライブラリに依存
# 
# などの側面がある
markup = ''
soup = BeautifulSoup(markup, 'lxml-xml')

# または
soup = BeautifulSoup(markup, 'xml')

html5lib

pipでhtml5libをインストールするには↓のようにします。

$ pip install html5lib

html5libを使うにはコンストラクタに文字列html5libを渡します。

from bs4 import BeautifulSoup

# サードパーティー製のhtml5libを使う
# html5libはpipなどで環境にインストールする必要がある
# ドキュメントいわく
#
#   * 非常に寛大
#   * Webブラウザと同じ方法でページを解析
#   * 有効なHTML5を作成
#
# なパーサーである
# しかし
# 
#   * めちゃくちゃ遅い
#   * Pythonの外部ライブラリに依存
# 
# などの側面がある
markup = ''
soup = BeautifulSoup(markup, 'html5lib')

# 環境にhtml5libがインストールされていないと↓のようなエラーが出る
# bs4.FeatureNotFound: Couldn't find a tree builder with the features you requested: html5lib. Do you need to install a parser library?

find系でタグを探す

構築したスープから目的のタグを探すにはfind系のメソッドを使います。
↓は<h1>タグを探す例です。

from bs4 import BeautifulSoup

markup = '<h1>Heading</h1>'
soup = BeautifulSoup(markup, 'html.parser')

# タグを見つける
tag = soup.find('h1')

print(type(tag))
# <class 'bs4.element.Tag'>

print(tag)
# <h1>Heading</h1>

findはタグが見つからなかった場合にNoneを返します。

from bs4 import BeautifulSoup

markup = '<h1>Heading</h1>'
soup = BeautifulSoup(markup, 'html.parser')

# タグが見つからなかったらNoneを返す
tag = soup.find('p')

print(type(tag))
# <class 'NoneType'>

print(tag)
# None

タグの属性を指定して探す場合はattrsを使います。

from bs4 import BeautifulSoup

markup = '''
    <p>I am Cat.</p>
    <p class="strong">This Dog is my friend.</p>
'''
soup = BeautifulSoup(markup, 'html.parser')

# 属性で狙い撃ちする
tag = soup.find('p', attrs={
    'class': 'strong'
})
print(tag)
# <p class="strong">This Dog is my friend.</p>

# attrsを使うのがめんどくさいあなたへ
tag = soup.find('p', class_='strong')
print(tag)
# <p class="strong">This Dog is my friend.</p>

タグ内の文字列を指定して探すことも出来ます。

from bs4 import BeautifulSoup

markup = '''
    <p>I am Cat.</p>
    <p>This <span>Dog</span> is <span>my friend</span>.</p>
'''
soup = BeautifulSoup(markup, 'html.parser')

# テキストで狙い撃ちする
tag = soup.find('p', text='I am Cat.')
print(tag)
# <p>I am Cat.</p>

複数のタグを探すにはfind_allを使います。
使い方はfindと代わりませんが、戻り値が異なります。
find_allはbs4.element.ResultSetを返します。
これは普通のlistのように扱えます。

from bs4 import BeautifulSoup

markup = '''
    <ul>
        <li>One</li>
        <li>Two</li>
        <li>Three</li>
    </ul>
'''
soup = BeautifulSoup(markup, 'html.parser')

# 結果はResultSetで返ってくる
lis = soup.find_all('li')

print(type(lis))
# <class 'bs4.element.ResultSet'>

print(lis)
# [<li>One</li>, <li>Two</li>, <li>Three</li>]

for li in lis:
    print(type(li))
    # <class 'bs4.element.Tag'>

print(lis[0].text)
# One

# 見つからなかった場合は空のResultSetが返ってくる
lis = soup.find_all('p')

print(type(lis))
# <class 'bs4.element.ResultSet'>

print(lis)
# []

print(len(lis))
# 0

select系でタグを探す

.box > pのようにCSSセレクタを使ってタグを取得することも可能です。
CSSセレクタを使うにはselectを使います。

from bs4 import BeautifulSoup

markup = '''
    <div class="box">
        <p>One</p>
        <p>Two</p>
    </div>
'''
soup = BeautifulSoup(markup, 'html.parser')

# CSSセレクタで要素を得る
# 結果はlistで返ってくる
tags = soup.select('.box > p')

print(type(tags))
# <class 'list'>

print(tags)
# [<p>One</p>, <p>Two</p>]

for tag in tags:
    print(type(tag))
    # <class 'bs4.element.Tag'>

print(tags[0].text)
# One

selectはタグが見つからなかった場合に空のlistを返します。

# 見つからない場合は空のリストが返ってくる
tags = soup.select('nothing')

print(type(tags))
# <class 'list'>

print(tags)
# []

print(len(tags))
# 0

単独のタグをCSSセレクタで取得したい場合はselect_oneを使います。

from bs4 import BeautifulSoup

markup = '''
    <div class="box">
        <p>One</p>
        <p>Two</p>
    </div>
'''
soup = BeautifulSoup(markup, 'html.parser')

# CSSセレクタで要素を1つだけ得る
tag = soup.select_one('.box > p')
print(tag)
# <p>One</p>

# 見つからない場合はNoneが返る
tag = soup.select_one('nothing')
print(tag)
# None

CSSセレクタが使えるのは便利ですね

タグのテキストを取得する

string属性を使えばタグ内のテキストを取得できます。
類似のtext属性も合わせて紹介します。

from bs4 import BeautifulSoup

markup = '''
    <h1>Heading</h1>
    <div>First <span>Second</span> Third</div>
    <p></p>
'''
soup = BeautifulSoup(markup, 'html.parser')

# タグに含まれる文字列を得る
# タグが単一のタグで、文字列を持っていれば戻り値はその文字列である
h1 = soup.find('h1')
print(h1.string)
# Heading

# タグが複数の子を持っている場合はNoneを返す
# この仕様には注意したい
div = soup.find('div')
print(div.string)
# None

# 単一のタグで、文字列が含まれていなければNoneを返す
p = soup.find('p')
print(p.string)
# None

stringとtextの違いは子要素をパースするかどうかです。
textは子要素も守備範囲です。

from bs4 import BeautifulSoup

markup = '''
    <h1>First</h1>
    <h2>One <span>Two <span>Three</span> Four</span> Five</h2>
    <h3></h3>
'''
soup = BeautifulSoup(markup, 'html.parser')

# text属性は寛大な属性である
# 単一のタグで文字列があればその文字列を返す
h1 = soup.find('h1')
print(h1.text)
# One

# タグが複数の子を持っている場合、その子からも再帰的に文字列を得る
h2 = soup.find('h2')
print(h2.text)
# One Two Three Four Five

# 空のタグの場合はNoneではなく空文字列を返す
# この仕様には注意したい
h3 = soup.find('h3')
print(h3.text)
# 

print(type(h3.text))
# <class 'str'>

print(len(h3.text))
# 0

getでタグの属性値を取得する

タグの属性値を取得したい場合はgetを使います。

from bs4 import BeautifulSoup

markup = '<h1 id="cat" class="dog bird">Heading</h1>'
soup = BeautifulSoup(markup, 'html.parser')
tag = soup.find('h1')

# id属性を得る
attr_id = tag.get('id')

print(type(attr_id))
# <class 'str'>

print(attr_id)
# cat

# class属性を得る
attr_class = tag.get('class')

print(type(attr_class))
# <class 'list'>

print(attr_class)
# ['dog', 'bird']

# 属性が見つからなければNoneを返す
attr = tag.get('unknown')
print(attr)
# None

# 取得できなかった場合のデフォルト値を設定できる
attr = tag.get('unknown', 'default')
print(attr)
# default

Tagでタグを作る

Tagでタグを作成することができます。

from bs4 import Tag

# h1タグを作成する
tag = Tag(name='h1')
print(tag)
# <h1></h1>

# 作成したタグにテキストを入れる
tag.string = 'Heading'
print(tag)
# <h1>Heading</h1>

# 属性を付加する
tag = Tag(name='h2', attrs={
    'id': 'sub-heading',
    'class': 'red strong',
})
print(tag)
# <h2 class="red strong" id="sub-heading"></h2>

appendでタグにタグを追加する

Tagでタグを作成したあとに、そのタグを既存の構造に追加することができます。
appendを使えば簡単にタグを構造の末尾に追加できます。

from bs4 import BeautifulSoup
from bs4 import Tag

markup = '''
    <ul>
        <li>One</li>
    </ul>
'''
soup = BeautifulSoup(markup, 'html.parser')

print(soup)
# <ul>
# <li>One</li>
# </ul>

# liタグをulの末尾に追加する
tag = Tag(name='li')
tag.string = 'Two'
soup.find('ul').append(tag)

print(soup)
# <ul>
# <li>One</li>
# <li>Two</li></ul>

extendでタグにタグのリストを追加する

extendを使えば既存の構造にタグのリストを追加することが出来ます。

from bs4 import BeautifulSoup
from bs4 import Tag

markup = '''
    <ul class="animals">
        <li>Cat</li>
        <li>Dog</li>
    </ul>
    <ul class="colors">
        <li>Red</li>
        <li>Blue</li>
    </ul>
'''
soup = BeautifulSoup(markup, 'html.parser')
print(soup)
# <ul class="animals">
# <li>Cat</li>
# <li>Dog</li>
# </ul>
# <ul class="colors">
# <li>Red</li>
# <li>Blue</li>
# </ul> 

# animalsのliをcolorsに入れる
tags = soup.select('.animals > li')
colors = soup.find('ul', class_='colors')
colors.extend(tags)

# animalsが空になってることに注意
print(soup)
# <ul class="animals">
# 
# 
# </ul>
# <ul class="colors">
# <li>Red</li>
# <li>Blue</li>
# <li>Cat</li><li>Dog</li></ul>

extractでタグをタグから切り出す

extractを使えば、既存の構造からタグを抽出することが出来ます。

from bs4 import BeautifulSoup
from bs4 import Tag

markup = '''
    <ul>
        <li>One</li>
        <li>Two</li>
    </ul>
'''
soup = BeautifulSoup(markup, 'html.parser')
print(soup)
# <ul>
# <li>One</li>
# <li>Two</li>
# </ul>

# タグをツリーから切り抜く
tag = soup.find('li', text='One')
tag.extract()

print(soup)
# <ul>
#
# <li>Two</li>
# </ul>

find_parentでタグの親を探す

タグのfind_panretで親を探すことができます。

from bs4 import BeautifulSoup
from bs4 import Tag

markup = '''
    <ul>
        <li>One</li>
    </ul>
'''
soup = BeautifulSoup(markup, 'html.parser')

# 子要素から親を探す
tag = soup.find('li', text='One')
parent = tag.find_parent()
print(parent)
# <ul>
# <li>One</li>
# </ul>

# ulの親は?
# root要素が返ってくる
tag = soup.find('ul')
parent = tag.find_parent()
print(parent)
# <ul>
# <li>One</li>
# </ul>

隣接するタグを取得する

あるタグに隣接している別のタグを取得するにはどうすればいいのでしょうか。
その場合はfind_previous、またはfind_nextを使います。

from bs4 import BeautifulSoup
from bs4 import Tag

markup = '''
    <ul>
        <li>One</li>
        <li>Two</li>
    </ul>
'''
soup = BeautifulSoup(markup, 'html.parser')

# Twoの1つ前の要素を探す
two = soup.find('li', text='Two')
one = two.find_previous()
print(one)
# <li>One</li>

# Oneの次の要素を探す
one = soup.find('li', text='One')
two = one.find_next()
print(two)
# <li>Two</li>

隣接する兄弟タグを取得する

あるタグに隣接している兄弟のタグを取得するにはfind_previous_siblingfind_next_siblingを使います。

from bs4 import BeautifulSoup
from bs4 import Tag

markup = '''
    <ul>
        <li>One</li>
        <li>Two</li>
    </ul>
'''
soup = BeautifulSoup(markup, 'html.parser')

# Twoの1つ前の兄弟要素を探す
two = soup.find('li', text='Two')
one = two.find_previous_sibling()
print(one)
# <li>One</li>

# Oneの次の兄弟要素を探す
one = soup.find('li', text='One')
two = one.find_next_sibling()
print(two)
# <li>Two</li>

siblingの違いとは?

find_previousとfind_previous_siblingの違いは何でしょうか?
find_previousが、隣接するタグをさかのぼって取得するのに対し、find_previous_siblingが取得するタグは兄弟、つまり同じレイヤーのタグに限られます。
まぎらわらしいですが、実際にコードを書いて確認してみるとその違いがよくわかります。

from bs4 import BeautifulSoup
from bs4 import Tag

markup = '''
    <ul>
        <li>One</li>
        <li>Two</li>
    </ul>
'''
soup = BeautifulSoup(markup, 'html.parser')

# Oneのひとつ前はul
one = soup.find('li', text='One')
ul = one.find_previous()
print(ul)
# <ul>
# <li>One</li>
# <li>Two</li>
# </ul>

# find_previous_siblingでは検索対象が兄弟の要素に限定される
# Oneのひとつ前の兄弟はいない
one = soup.find('li', text='One')
ul = one.find_previous_sibling()
print(ul)
# None

siblingは仁義のあるメソッドなんですね

insertでタグにタグを挿入する

insertを使えばタグの指定した位置にタグを挿入することができます。
insertの第1引数には0オリジンのインデックス、第2引数にはTagを渡します。

from bs4 import BeautifulSoup
from bs4 import Tag

markup = '''
    <ul>
        <li>Cat</li>
    </ul>
'''
soup = BeautifulSoup(markup, 'html.parser')

# insertで0番目に要素を挿入する
dog = Tag(name='li')
dog.string = 'Dog'
soup.find('ul').insert(0, dog)
print(soup)
# <ul><li>Dog</li>
# <li>Cat</li>
# </ul>

# insertで1番目に要素を挿入する
bird = Tag(name='li')
bird.string = 'Bird'
soup.find('ul').insert(1, bird)
print(soup)
# <ul><li>Dog</li><li>Bird</li>
# <li>Cat</li>
# </ul>

insert_beforeでタグの直前にタグを挿入する

insert_beforeを使えばタグの直前にタグを挿入することができます。
JavaScriptなどでも馴染みのあるメソッドですね。

from bs4 import BeautifulSoup
from bs4 import Tag

markup = '''
    <ul>
        <li>Cat</li>
    </ul>
'''
soup = BeautifulSoup(markup, 'html.parser')

# catの前にdogを挿入する
dog = Tag(name='li')
dog.string = 'Dog'
cat = soup.find('li', text='Cat')
cat.insert_before(dog)
print(soup)
# <ul>
# <li>Dog</li><li>Cat</li>
# </ul>

insert_afterでタグの直後にタグを挿入する

insert_afterを使えばタグの直後にタグを挿入することが出来ます。

from bs4 import BeautifulSoup
from bs4 import Tag

markup = '''
    <ul>
        <li>Cat</li>
    </ul>
'''
soup = BeautifulSoup(markup, 'html.parser')

# catの後にdogを挿入する
dog = Tag(name='li')
dog.string = 'Dog'
cat = soup.find('li', text='Cat')
cat.insert_after(dog)
print(soup)
# <ul>
# <li>Cat</li><li>Dog</li>
# </ul>

unwrapで自由を得る

不自由な世界に束縛されたテキスト・コンテンツを解放するにはunwrapを使います。

from bs4 import BeautifulSoup
from bs4 import Tag

markup = '''
    <ul>
        <li>One</li>
    </ul>
'''
soup = BeautifulSoup(markup, 'html.parser')

# Oneからタグを外す
li = soup.find('li', text='One')
li.unwrap()
print(soup)
# <ul>
# One
# </ul>

wrapで秩序を得る

タグを秩序のある世界へ束縛するにはwrapを使います。

from bs4 import BeautifulSoup
from bs4 import Tag

markup = '''
    <p>I am Cat.</p>
    <div></div>
'''
soup = BeautifulSoup(markup, 'html.parser')

# divでpを囲う
p = soup.find('p')
div = soup.find('div')
p.wrap(div)
print(soup)
# <div><p>I am Cat.</p></div>

clearで世界をリセットする

この世界に飽きてしまいましたか?
clearで世界をリセットしましょう。
新しい秩序を組み立てるのです。

from bs4 import BeautifulSoup
from bs4 import Tag

markup = '''
    <ul>
        <li>One</li>
        <li>Two <span>Three</span></li>
    </ul>
'''
soup = BeautifulSoup(markup, 'html.parser')

# 子要素をクリアする
ul = soup.find('ul')
ul.clear()
print(ul)
# <ul></ul>

おわりに

いかがでしたでしょうか。
BeautifulSoup4を使えば簡単にスクレイピングが可能になります。
皆さんの武器庫に追加されてみてはいかがでしょうか。

以上、narupoでした。

関連記事

参考

Beautiful Soup 4.x では parser を明示指定しよう - AWS / PHP / Python ちょいメモ

Webアプリケーションの制作ならNARUPORT

Webアプリケーションの制作ならNARUPORTにお任せください。
Webアプリの他にもGUIアプリやChromeExtension, スクリプトの制作など可能です。
以下のお問い合わせフォームからご依頼ください!

投稿者名です。64字以内で入力してください。

必要な場合はEメールアドレスを入力してください(全体に公開されます)。

投稿する内容です。

スポンサーリンク

スポンサーリンク

スポンサーリンク

スポンサーリンク