はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

BeautifulSoupの隣接ノード取得系のメソッドのまとめ および 他(コメント取得やテキスト取得、SoupStrainer)

BeautifulSoupについては、find系やselect系でのオブジェクト取得により取り回していくのが基本だとは思いますが、parentやnext_siblingなど隣接取得系(造語です)についても、たまに使うにせよ、使おうとするとどうだっけというところで悩ましいので、これらのうち主なものについて、可視化してまとめてみました。

f:id:azotar:20200225223241p:plain

上記の(ほぼ)全部入りのPythonプログラムの例

from bs4 import BeautifulSoup
import re

h ="""
<html>
<head>
<x1>aaa</x1>
<!-- コメント -->
<x2>aaa</x2>

<x3>aaa</x3>
</head>
<body>
    <y1>aaa
        <y1a>
        bbb
        </y1a>
        <y1b>
        bbb
        </y1b>
        xbbbbbx
    </y1>
    <y2>aaa</y2>
    <y3>aaa</y3>
    <y4>aaa1st</y4>
    <y4>aaa2nd</y4>
    <y4>aaa3rd</y4>
</body>
</html>
"""


b = BeautifulSoup(h, 'html.parser')

# children, parent, previous_sibling, next_sibling, ...
b.body.children

b.y1a.parent

b.y1a.parents

b.y3.previous_sibling

b.y3.next_sibling

b.y3.next_siblings

b.y3.previous_siblings

b.y3.next_sibling

# find_next_sibling
b.y3.find_next_sibling('y4')

# find_all_next
b.y3.find_all_next(re.compile('^y'))

# insert_after ex1
b.y3.insert_after(BeautifulSoup('<insert>inss</insert>', 'html.parser'))

# insert_after ex2
if tail := b.body.children:
    list(tail)[-1].insert_after(BeautifulSoup('<insert>inss</insert>', 'html.parser'))

# コメントタグを出力
import bs4
for c in b(text=lambda x: isinstance(x, bs4.Comment)):
    print(c)
    # 「コメント」が出力される

# すべてのテキストを出力
for t in b.find_all(text=True):
    print(t)


# SoupStrainer の例
from bs4 import SoupStrainer
print(BeautifulSoup(h, 'html.parser', parse_only=SoupStrainer('y4')))

print(BeautifulSoup(h, 'html.parser', parse_only=SoupStrainer(string=lambda string: len(string) > 3 if isinstance(string,bs4.Tag) else False )))


補足1

  1. まとめてみてわかったのですが、next_siblingとnext_elementの違いを理解しておけば、無用な混乱をさけやすいと思われます。(自分はこれを区別しておらず雰囲気重視で過ごしていたら思わぬ苦労したというだけですが...)
  2. この場合、あるあるであるbody閉じタグの前に挿入...というのが意外に難しくて、body直下の要素のうち、最後尾のものの後ろに挿入という形になるのですが、他に良い方法があれば知りたい(例のinsert_after ex2 )。

補足2

主題とは関係ないのですが、私の中で関連度が高い次のような例も末尾あたりに入れ込んでいます。

2-1. コメントタグを取得

コメントタグだけ取得したいということが稀にあります。

どこかで見た例を参考に秘伝のタレとしてメモっています。→ 上記コード中の「コメントタグを出力」の部分。

まあ、その昔のようにhtmlの中にコメントタグを入れて、後から動的に置換するといったオレオレテンプレートエンジンは今時はやらないのでしょうね。

どちらかといえば、コメントタグを削除したいというところでしょうから、上記の例でいうと、c.extract()などとやるのかも。

なお、ここに書いてありますが、現時点ではstring=とtext=は同じ挙動ぽいですが、本来(今後)は、string=にした方が良いかもしれません。

https://www.crummy.com/software/BeautifulSoup/bs4/doc/#the-string-argument

あと、「BeautifulSoup」クラスのインスタンスは、ヘルプをみると「call」のセクションに記載がありますが、「find_all」相当のようです。

2-2. 全てのテキストを取得

find_allにtext=Trueを指定してやるのが定石のようです。(前項のコメントタグもtextにBooleanを戻すlambdaを指定してやっているのと同じですね。)

新旧比較(diff)のためにテキストを取得してやるだけであれば、BeautifulSoup.get_text()でしょうか。

一方、find_allでイテレートさせつつ、先のparentなどを使うといい感じです。

例えば、Webブラウザ上で気になるテキストの文字でざっくりノード検索の上、そのノードがどのタグなのかを確認するといったことをCLIで対話的に確認するといったことがやりやすいので、そのような用途もありでしょう。

In [324]: for i in b.find_all(text='aaa'): 
     ...:  
     ...:        print(i.parent.prettify()) 
     ...:                                                                                                                                    
<x1>
 aaa
</x1>

<x2>
 aaa
</x2>

<x3>
 aaa
</x3>

<y2>
 aaa
</y2>


※ bはBeautifulSoupオブジェクトです。

parentは取得エラーになるケースは考えにくいので、少なくともアドホックな用途の範囲であれば、ifなどを省略してタイプ量を少なめにするということもアリかもしれません。

2-3. SoupStrainer

find系の条件式を格納したオブジェクトといったところでしょうか。BeautifulSoupのコンストラクタにparse_onlyパラメータで渡してやれば、該当のものだけにノードを絞り込んでくれるようです。

処理対象としたいノードが確実に決まっており、最初にスコープを絞りたいような用途には便利そうです。

私はワンライナーと相性が良さそうだと感じました。

心残り?

まとめると言いながら、挙動がわかりづらいと予測されるdescendantsの例を入れるのを忘れました。多分、parentsの反対と言って良いのだろうけど...

www.crummy.com

公式サイトへの関連章へのリンク

Beautiful Soup Documentation — Beautiful Soup 4.4.0 documentation

関連別記事

本件の理解には次の記事が役にたつかもしれません。

itdepends.hateblo.jp