BeautifulSoupでstringとtextの挙動の明確な違い – Python



スクレイピングなどで、最終的に文字列を取得したい場合は、soupオブジェクトに対して".string"や".text" で文字列を抽出することが出来ますが、両者の明確な挙動の違いを例を挙げて紹介します。

<div>
   <h2>文字列取得テスト</h2>
   <p>BeautifulSoupはスクレイピングに欠かせないPythonのライブラリです。<br/>複雑なHTMLソースも、手軽にパースできる事が<b>最大の魅力</b>です。</p>
</div>

例えば上記のようなHTMLソースがあった場合を例に見ていきます。

from bs4 import BeautifulSoup as bs4

soup = bs4(html, 'lxml')

ここまでが前提のソース。

soup.p.text の場合

".text" の方は非常に単純で、そのタグ内に含まれるすべての文字列をつなぎ合わせて返却します。

.stringに対して、予想外の挙動になることが少ないので、「文字列の取得は.textを使っている」という人も少なくないはず。

soup.p.text

In [1]:soup.p.text
Out[1]: 'BeautifulSoupはスクレイピングに欠かせないPythonのライブラリです。複雑なHTMLソースも、手軽にパースできる事が最大の魅力です。'

ここまでは普通ですね。pタグ内のタグ<br/>と<b></b>タグがしっかり消えています。

In [2]:type(soup.p.text)
Out[2]: str

ちなみに返却の型はstr型です。

このように".text"は子孫要素の最下部に至るまで文字列を再帰的に取得してつなぎ合わせて返却します。

もちろん、最上位の親タグ<div>を指定しても、全ての文字列のみが返却されます。

In [3]:soup.div.text
Out[3]: 'n文字列取得テストnBeautifulSoupはスクレイピングに欠かせないPythonのライブラリです。複雑なHTMLソースも、手軽にパースできる事が最大の魅力です。n'

ちょっとうっとおしい"n"も文字列としてしっかり帰ってきています。

soup.p.string の場合

何となくどこで使えばいいのか分からない".string"ですが、こちらの挙動を見ていきます。

soup.p.string

In [4]:soup.p.string
Out[4]:

???
なにも帰って来ません。

In [5]:type(soup.p.string)
Out[5]: NoneType

返却はNoneですね。

soup.b.string

In [6]:soup.b.string
Out[6]: '最大の魅力'

ターゲットをpタグからbタグに変更してみるとしっかり取れます。

In [7]:type(soup.b.string)
Out[7]: bs4.element.NavigableString

ちなみに返却型は通常の文字列型(str)とは違いBeautifulSoup専用の特殊文字列クラス"NavigableString"です。

soup.stringが返却される条件

".string"は直下にタグ要素を持っているとNoneとなり、返却されません。

と、今まで思っていたのですが、どうやら違うようです。
その様に解説しているブログとかも結構あったりします。
でも、違います

以下で少し、実験してみます。
元のhtmlをbs4を使って加工、pタグ内に子要素として他のタグが存在しない状態を作ります。

In [8]:soup.b.unwrap() # pタグ内のbタグだけ削除(文字列は残る)
Out[8]: <b></b>
In [9]:soup
Out[9]: 
<html><body><div>
<h2>文字列取得テスト</h2>
<p>BeautifulSoupはスクレイピングに欠かせないPythonのライブラリです。<br/>複雑なHTMLソースも、手軽にパースできる事が最大の魅力です。</p>
</div></body></html>
# ↑この状態
In [10]:soup.p.string
Out[10]:

pタグ内のbタグを取り除いたこの状態でもNoneで、何も返って来ません。
まだ、pタグの中にbrタグがいるからですね。

In [11]:soup.br.decompose() # さらにbrを削除
In [12]:soup
Out[12]: 
<html><body><div>
<h2>文字列取得テスト</h2>
<p>BeautifulSoupはスクレイピングに欠かせないPythonのライブラリです。複雑なHTMLソースも、手軽にパースできる事が最大の魅力です。</p>
</div></body></html>
# ↑この状態、pタグ内には子要素タグは完全に消え去りました。
In [13]:soup.p.string
Out[13]:

???

何も返却されません。pタグ内に他のタグは存在しないのに変ですね。

soup.stringが返却される本当の条件

結論から言うと、".string"は指定した要素の、子孫要素に渡って"NavigableString"クラスが一つしか存在しない時にstringとして返却されます。

上の実験で何故stringが返ってこないかの答えを解説します。

In [14]:list(soup.p.strings)
Out[14]: 
['BeautifulSoupはスクレイピングに欠かせないPythonのライブラリです。',
 '複雑なHTMLソースも、手軽にパースできる事が',
 '最大の魅力',
 'です。']

答えは".strings"という似たようなメソッド(アトリビュート?)を見てみると分かります。
このメソッドは子孫要素に渡って"NavigableString"クラスを再帰的に取得し、ジェネレータ型で返却します。

結果はご覧の通り、返ってきたNavigableStringをリスト化してみると、4つ存在します。

よく見ていただくと分かるのですが、実験内で消したタグが存在していた位置で文字列が途切れています。

BeautifulSoupのメソッドでタグを取り除いても厳密には文字列が繋がるワケではないという事です。

NavigableStringクラスというのは各種タグで分断されるようですね。

「タグで分断されるなら、子孫要素にタグがある時点で、.stringは返却されないんじゃ?」
と思いました?

In [15]:html = '<p><span>BeautifulSoupはスクレイピングに欠かせないPythonのライブラリです。
複雑なHTMLソースも、手軽にパースできる事が最大の魅力です。</span></p>'

# ↑ pタグ内にspanタグ

In[16]:soup = bs4(html, 'lxml')
In[17]:soup.p.string
Out[17]: 'BeautifulSoupはスクレイピングに欠かせないPythonのライブラリです。n複雑なHTMLソースも、手軽にパースできる事が最大の魅力です。'

このように指定要素内に別のタグが存在していても、テキストを分断しない配置になっていれば.stringは返却されるのです。

BeautifulSoupにおけるstringとtextの違い まとめ

どちらもテキストを取得するために用いるメソッドだけど、何が何でも根こそぎ取得してくる".text"に対して、割と繊細で使いどころが難しい".string"という印象は変わらないですね。

".string"のメリットは何かというと、一般的なstr型文字列ではなく、bs4派生クラスの文字列なので、bs4各種メソッドが使える点が大きいですね。
.textで返却されるのは標準のstr型なので、もちろんBeatifulSoupのメソッドを使うことはできません。

単純な文字列取得だけなら、あまり".string"を使うことはないかもしれませんが、HTML自体を加工して流用する場合なんかだと .extract() とか、親まで返って .decompose() とか、使えないと困りますので使いどころはあるにはあると思います。

今回の記事で、両者の明確な違いを少し頭に入れていただいて、実際使ったときに挙動が変で混乱することがなくなれば幸いです。