BeautifulSoup
BeautifulSoupはHTMLやXMLといった構造体からデータを取り出すためのライブラリです。
Webサイトから情報をスクレイピングしてくるのによく使われます。
https://www.crummy.com/software/BeautifulSoup/bs4/doc/
私はできる限りスクレイピングは使いたくないし使うべきではないと考えているのですが、残念ながらやらねばならないときもあるものです。
また、スクレイピングは節度を持って、ルールを守ってやりましょう。
この記事の内容
以下の2つの話題のうち、前者について挙動を確認します。
後半はこちら
- Tag.textとTag.stringの違い。特に文字列が
<br>
タグを含む場合はTag.stringはNoneになる。 - Tag.find(string='hoge')とTag.find(text='hoge')の違い。両者は同じになる。
環境
$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.14.2
BuildVersion: 18C54
$ python --version
Python 3.7.2
$ pip show bs4
Name: bs4
Version: 0.0.1
Tag.textとTag.stringの話
ユースケース1
requestsやseleniumなどで取得したhtml文字列を解析して中のデータを取り出したい。特に次のようなhtmlの一部から、テキスト部分 hogefuga
という文字列を取得したいとします。
html = '''
<p>hogefuga</p>
'''
これは次のようなpythonスクリプトで取得できます。(実施前にpipでbs4をインストールしてください)
from bs4 import BeautifulSoup
html = '''
<p>hogefuga</p>
'''
soup = BeautifulSoup(html, 'html.parser')
text = soup.find('p').string
print(text)
実際に実行してみます。
$ python sample_code1.py
hogefuga
できました。
これが基本的なBeautifulSoupの使い方です。
ユースケース2
それでは、抽出する文字列の中に<br>
タグが入っている場合はどうでしょうか。
今回の対象htmlはこれです。
html = '''
<p>hoge<br>fuga</p>
'''
先程と同じように書くと、失敗してしまいます。
from bs4 import BeautifulSoup
html = '''
<p>hoge<br>fuga</p>
'''
soup = BeautifulSoup(html, 'html.parser')
text = soup.find('p').string
print(text)
$ python sample_code2.py
None
抽出文字列に<br>
が含まれる場合は Tag.stringではなくTag.textを利用する
こちらで言及されているように、 Tag.textプロパティを利用すると取得できるようです。
なにはともあれやってみます。
from bs4 import BeautifulSoup
html = '''
<p>hoge<br>fuga</p>
'''
soup = BeautifulSoup(html, 'html.parser')
text = soup.find('p').text
print(text)
$ python sample_code3.py
hogefuga
改行タグを除いたテキストが取得できました。
Tag.stringとTag.textは何が違うのか?
ソースコードを読んでみると、挙動の違いがわかります。
Tag.string
Tag.stringはこのように定義されています。
@property
def string(self):
"""Convenience property to get the single string within this tag.
:Return: If this tag has a single string child, return value
is that string. If this tag has no children, or more than one
child, return value is None. If this tag has one child tag,
return value is the 'string' attribute of the child tag,
recursively.
"""
if len(self.contents) != 1:
return None
child = self.contents[0]
if isinstance(child, NavigableString):
return child
return child.string
コメントにもあるように、次のルールで値を返します。
- もしタグの中身が単一の文字列(例:
<p>hogefuga</p>
)の場合、その文字列を返す - もしタグの中身が無い(例:
<p></p>
)か、中身が複数の要素から成る(例:<p>hoge<span>fuga</span></p>
)場合、Noneを返す - もしタグの中身が単一のタグ(例:
<p><span>hogefuga</span></p>
)の場合、子要素のタグの文字列をそのタグの文字列として返す。return child.string
部分によって再帰的に実行される。例えば子要素が1つのタグのみ持っている場合は孫要素の文字列が返される。
日本語よりコードの方がわかりやすいと思うので、コードで例を挙げます。
from bs4 import BeautifulSoup
html = '''
<p>hogefuga</p>
<p></p>
<p>hoge<span>fuga</span></p>
<p><span>hogefuga</span></p>
<p><span><b>hogefuga</b></span></p>
'''
soup = BeautifulSoup(html, 'html.parser')
print('中身が単一文字列: ', soup.find_all('p')[0].string)
print('中身が無い: ', soup.find_all('p')[1].string)
print('中身が複数要素: ', soup.find_all('p')[2].string)
print('中身が単一タグ: ', soup.find_all('p')[3].string)
print('孫要素: ', soup.find_all('p')[4].string)
$ python sample_code4.py
中身が単一文字列: hogefuga
中身が無い: None
中身が複数要素: None
中身が単一タグ: hogefuga
孫要素: hogefuga
それでは、改行タグを含む文字列 <p>hoge<br>fuga</p>
はなぜNoneが返されたのでしょうか?
Tagの子要素を表示するTag.childrenプロパティで問題のhtmlの中身を調べてみます。childrenはgeneratorを返すので、listにして表示します。
from bs4 import BeautifulSoup
html = '''
<p>hoge<br>fuga</p>
'''
soup = BeautifulSoup(html, 'html.parser')
print(list(soup.find_all('p')[0].children))
$ python sample_code5.py
['hoge', <br/>, 'fuga']
上記より、 <p>hoge<br>fuga</p>
は3つの要素が含まれるタグなので、Tag.stringがNoneが返すという挙動をしたことがわかりました。
Tag.text
前節では、Tag.stringプロパティが<br>
タグを含む文字列をうまく扱えないことを確認しました。
それに対してTag.textでは文字列中にタグがあっても文字列を抽出することができます。この挙動を調べます。
ソースコードではこう書かれています
def get_text(self, separator=u"", strip=False,
types=(NavigableString, CData)):
"""
Get all child strings, concatenated using the given separator.
"""
return separator.join([s for s in self._all_strings(
strip, types=types)])
getText = get_text
text = property(get_text)
def _all_strings(self, strip=False, types=(NavigableString, CData)):
"""Yield all strings of certain classes, possibly stripping them.
By default, yields only NavigableString and CData objects. So
no comments, processing instructions, etc.
"""
for descendant in self.descendants:
if (
(types is None and not isinstance(descendant, NavigableString))
or
(types is not None and type(descendant) not in types)):
continue
if strip:
descendant = descendant.strip()
if len(descendant) == 0:
continue
yield descendant
@property
def descendants(self):
if not len(self.contents):
return
stopNode = self._last_descendant().next_element
current = self.contents[0]
while current is not stopNode:
yield current
current = current.next_element
Tag.textを呼ぶとget_text関数が実行され、Tag以下の全ての要素の文字列が順番に文字列連結されているのがわかります。
例えば、<p>hoge<br>fuga</p>
に対してTag.textを実行するとどうなるかというと、 3つの要素 ['hoge', <br>, 'fuga']
のうち<br>
タグには文字列が無いので、hogefuga
が返されるという挙動になります。
結論
Tag.stringとTag.textは全く別の処理を行っていることがわかりました。
作者はTag.textは再帰的に処理するため可能な場合はstringの方を使って欲しいようですが、文字列に他のhtmlタグが入る可能性がある場合はtextを利用する方が無難でしょう。
今回は <br>
タグを槍玉に上げましたが、<b></b>
タグなど他にも文字列中に入ってくるinlineなタグがあるので気をつけましょう。