beautifulsoup4によるタグの取得方法を色々試す

スポンサーリンク

例えば、以下のようなhtmlがあって、aタグの「サンプルリンク」文字列までたどり着きたい場合、

<div>
    <article>
        <p id="main">
            <a href="https://example.com">サンプルリンク</a>
        </p>
    </article>
<div>

beautifulsoupでは色々な道順が用意されていて面白いです。
パーサーはlxmlを使用しています。

soup = BeautifulSoup(response.body, "lxml") #以降は省略

#findで要素を取得
link_name = soup.find('p', id='main').a.string
print('link_name', link_name)
#link_name サンプルリンク

#CSSセレクタで指定して要素を取得(戻り値はリスト)
link_name = soup.select('#main > a')[0].string
print('link_name', link_name)
#link_name サンプルリンク

#ドット繋ぎで要素を取得
link_name = soup.div.article.p.a.string
print('link_name', link_name)
#link_name サンプルリンク

stringとtextの違い

<p id="main">
    <a href="https://example.com">サンプルリンク</a>
</p>
_a = soup.find('p', id='main').a

print('_a.string', _a.string)
#_a.string サンプルリンク
print('type(_a.string)', type(_a.string))
#type(_a.string) <class 'bs4.element.NavigableString'>

print('_a.text', _a.text)
#_a.text サンプルリンク
print('type(_a.text)', type(_a.text))
#type(_a.text) <class 'str'>

stringはNavigableStringオブジェクトというやつで、textは文字列です。
NavigableStringは、htmlをツリー構造化したbeautifulsoup独自のクラスだそうです。
例えば以下の様に、span要素の中にimg要素が入っているといった、文字列以外の要素が含まれている場合などは、spanのstringを表示させようとしてもNoneになります。
こういう場合はtextを使います。
textはタグを除去して全テキストを抽出します。

<div>
    <span class="new">spanコンテンツ<img src="https://example.com/img/img.jpg" /></span>
<div>
span_str = soup.div.span.string
print('span_str', span_str)
#span_str None

span_text = soup.div.span.text
print('span_text', span_text)
#span_text spanコンテンツ

もしくはこんな方法もあります。
extractでdivからimg要素を除去します。
stringは、タグの子要素が1つだけ(NavigableString)なら利用できるので、この場合はdivからimgが除去されて「spanコンテンツ」というNavigableStringオブジェクトだけが残るので、条件が整うことになります。

div = soup.div
img = div.img.extract()
span_str = div.span.string
print('span_str', span_str)
#span_str spanコンテンツ

文字列の空白除去

よくあるのが、タグの中に空白や改行が混ざっているケースです。
そーいう場合は、pythonのstrip関数で先頭と末尾の空白を削除します。

<div class="new">     
    divコンテンツ
     </div>
div = soup.div
print('div.string', div.string)
#div.string
#                divコンテンツ
#                
strip_str = str(div.string).strip()
print('strip_str', strip_str)
#strip_str divコンテンツ

[python]strip()の引数を省略すると空白だけでなく改行も除去される - dackdive's blog

兄弟要素

同じ階層の要素を兄弟要素と言います。
以下だと、dl同士は兄弟要素で、dtとddも兄弟要素です。

<div class="news_list">
    <dl>
        <dt><span class="date">2018年1月1日</span></dt>
        <dd><a href="https://example.com/1">コンテンツ1</a></dd>
    </dl>
    <dl>
        <dt><span class="date">2018年1月2日</span></dt>
        <dd><a href="https://example.com/2">コンテンツ2</a></dd>
    </dl>
    <dl>
        <dt><span class="date">2018年1月3日</span></dt>
        <dd><a href="https://example.com/3">コンテンツ3</a></dd>
    </dl>
</div>
#classによる検索
#「class」はpythonの予約語なため「class_」を使う
news_list = soup.find('div', class_='news_list')
for dl in news_list.find_all('dl'):
    dd = dl.dd
    content = dd.a.text
    #ddの一つ前のdt兄弟要素を取得する
    date = dd.find_previous_sibling('dt').text
    print(date, content)
    #2018年1月1日 コンテンツ1
    #2018年1月2日 コンテンツ2
    #2018年1月3日 コンテンツ3

previous_siblingという属性も使えますが、</dt>と<dd>の間の改行(\n)を要素として取得するので、もっとシビアな調整が必要になりそうです。

二番目の要素の取得

<article>
    <div>コンテンツ1</div>
    <div>コンテンツ2</div>
    <div>コンテンツ3</div>
</article>

二番目の「コンテンツ2」って要素が取得したい場合はこれでいいのかな?
なんかちょっとブサイクなような気もするんだけど……。

second_div = soup.article.div.find_next_sibling('div')
print('second_div.text', second_div.text)
#second_div.text コンテンツ2

あ、こんな感じでもいけるか。

divs = soup.article.find_all('div')
print('divs[1].text', divs[1].text)
#second_div.text コンテンツ2

属性の存在チェック・取得

<article>
    <div id="id_content">IDコンテンツ</div>
    <div class="class_content">Classコンテンツ</div>
</article>
divs = soup.article.find_all('div')
print(divs[0].has_attr('id')) #True
print(divs[0].has_attr('class')) #False
print(divs[0].get('id')) #id_content
print(divs[0].get('class')) #None
print(divs[1].has_attr('id')) #False
print(divs[1].has_attr('class')) #True
print(divs[1].get('id')) #None
print(divs[1].get('class')) #['class_content']

has_attrで属性の存在確認ができます。
getで属性を取得した時に、idと違いclassの場合はリストが返ってきます。

最強のドキュメント
kondou.com - Beautiful Soup 4.2.0 Doc. 日本語訳 (2013-11-19最終更新)