BeautifulSoupについては、find系やselect系でのオブジェクト取得により取り回していくのが基本だとは思いますが、parentやnext_siblingなど隣接取得系(造語です)についても、たまに使うにせよ、使おうとするとどうだっけというところで悩ましいので、これらのうち主なものについて、可視化してまとめてみました。
上記の(ほぼ)全部入りの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
- まとめてみてわかったのですが、next_siblingとnext_elementの違いを理解しておけば、無用な混乱をさけやすいと思われます。(自分はこれを区別しておらず雰囲気重視で過ごしていたら思わぬ苦労したというだけですが...)
- この場合、あるあるである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の反対と言って良いのだろうけど...
公式サイトへの関連章へのリンク
Beautiful Soup Documentation — Beautiful Soup 4.4.0 documentation
関連別記事
本件の理解には次の記事が役にたつかもしれません。