ユーニックス総合研究所

  • home
  • archives
  • bs4-text

BeautifulSoup4のtextで要素の文字列を得る

  • 作成日: 2020-11-24
  • 更新日: 2023-12-24
  • カテゴリ: BeautifulSoup4

BeautifulSoup4のtextの使い方

Pythonには「BeautifulSoup4(ビューティフル・スープ・フォー)」というHTML/XMLをパースする外部ライブラリがあります。

BeautifulSoup4のbs4.BeautifulSoupまたはbs4.element.Tagオブジェクトにはtextという属性があります。
この属性を参照すると、タグの持つテキスト・コンテンツを取得することが可能です。

この記事ではTagオブジェクトのtext属性の使い方について解説します。
具体的には↓を見ていきます。

  • text属性の正体
  • get_text()の構造
  • text属性で要素のテキストを得る
  • get_text()でセパレータを指定する
  • get_text()でストリップする
  • get_text()でタイプを指定する
  • textとstringの違い

text属性の正体

bs4.BeautifulSoupクラスはbs4.element.Tagを継承したクラスです。

from bs4 import BeautifulSoup  
from bs4.element import Tag  

soup = BeautifulSoup('', 'html.parser')  
print(type(soup))  
print(isinstance(soup, Tag))  
# <class 'bs4.BeautifulSoup'>  
# True  

この記事で扱うtext属性はこのbs4.element.Tagの属性です。
textは↓のようにして使うことが出来ます。

print(soup.text)  

↑のようにタグのtext属性を参照すると、タグ内のテキストを取得することが出来ます。

BeautifulSoup4ではtext属性はどのように定義されているのでしょうか?
BeautifulSoup4のソースコードを見てみます。
text属性はTagクラスで↓のように定義されています。

    text = property(get_text)  

property()関数はプロパティを生成する関数です。
よってtext属性はプロパティであることがわかります。
property()の第1引数はゲッターで使う関数です。
つまりtext属性はテキストを取得するときにTagクラスのget_text()メソッドを使っていることになります。

get_text()の構造

text属性の正体はTag.get_text()でした。
get_text()は↓のようなメソッドになっています。

    def get_text(self, separator="", strip=False,  
                 types=(NavigableString, CData)):  
        """Get all child strings, concatenated using the given separator.  

        :param separator: Strings will be concatenated using this separator.  

        :param strip: If True, strings will be stripped before being  
            concatenated.  

        :types: A tuple of NavigableString subclasses. Any strings of  
            a subclass not found in this list will be ignored. By  
            default, this means only NavigableString and CData objects  
            will be considered. So no comments, processing instructions,  
            stylesheets, etc.  

        :return: A string.  
        """  
        return separator.join([s for s in self._all_strings(  
                    strip, types=types)])  

和訳しました↓。

    def get_text(self, separator="", strip=False,  
                 types=(NavigableString, CData)):  
        """すべての子の文字列を、与えられたセパレーターで結合して取得する。  

        :param separator: 文字列はこのセパレーターの文字列で結合されます。  

        :param strip: もしTrueなら、文字列は結合前にストリップされます。  

        :types: NavigableStringサブクラスのタプル。  
                このリストにないサブクラスの文字列は無視されます。  
                デフォルトでは、これはNavigableStringとCDataオブジェクトのみが考慮されることを意味します。  
                つまり、コメントや処理命令、スタイルシートなどは無視されます。  

        :return: 文字列  
        """  
        return separator.join([s for s in self._all_strings(  
                    strip, types=types)])  

_all_strings()メソッドは内部でdescendantsをループで回して走査しています。
descendantsはプロパティで、contents属性内の要素を辿りながら要素をyieldします。
つまりget_text()contents属性内の要素を順に辿りながらテキストを取得する関数と言うことになります。

これは単一の要素のテキストではなく、子要素も含めて再帰的にテキストを取得するという意味です。

contents属性はただのリストです。
Tagクラスはこのcontentsリストにパースした要素を格納しています。
後述のstringプロパティもtextと同様にこのcontentsを参照しています。

contentsリストに格納される要素は、代表的なのはNavigableStringです。
これは要素内の文字列を表現するクラスです。
このNavigableStringから派生しているクラスも複数定義されています。

typesに指定できるクラスの一覧

↓はget_text()types引数のタプルに格納できるクラスの一覧です。

最も基本的な文字列を表現するクラスです。

PreformattedString

CommentクラスやCDataクラスを表現するための抽象クラスです。

CData

CDATAブロックを表すクラスです。
プレフィックスは<![CDATA[です。
サフィックスは]]>です。

ProcessingInstruction

SGMLを表すクラスです。
SGMLとはXMLの前身になったフォーマットです。
プレフィックスは<?です。
サフィックスは>です。

XMLProcessingInstruction

XMLを表すクラスです。
プレフィックスは<?です。
サフィックスは?>です。

Comment

HTML/XMLのコメントを表すクラスです。
プレフィックスは<!--です。
サフィックスは-->です。

Declaration

XMLの宣言を表すクラスです。
プレフィックスは<?です。
サフィックスは?>です。

Doctype

DOCTYPEを表すクラスです。
プレフィックスは<!DOCTYPEです。
サフィックスは>\nです。

Stylesheet

スタイルシートを表すクラスです。

Script

スクリプトを表すクラスです。
JavaScriptなど。

TemplateString

大きな文書の中に埋め込まれたHTMLテンプレートの中で見られる文字列を表すNavigableString。
文書の本文と区別するために使用します。

text属性で要素のテキストを得る

text属性で取得した要素のテキストを取得するには↓のようにします。

from bs4 import BeautifulSoup  

html = '''  
<div>  
  blue  
  <p>yellow</p>  
  <span>green</span>  
</div>  
'''  
soup = BeautifulSoup(html, 'html.parser')  
el = soup.find('div')  
print(el.text)  

↑の出力結果↓。


  blue  
  yellow  
green  

↑のように子要素のテキストも再帰的に取得してる点に注目してください。
これがtextの特徴です。

get_text()でセパレータを指定する

get_text()の引数separatorに文字列を指定すると、要素の文字列をそのセパレーターで囲って取得します。

from bs4 import BeautifulSoup  

html = '''  
<div>  
  blue  
  <p>yellow</p>  
  <span>green</span>  
</div>  
'''  
soup = BeautifulSoup(html, 'html.parser')  
el = soup.find('div')  
print(el.get_text(separator='★'))  

↑の出力結果↓。


  blue  
  ★yellow★  
★green★  

↑のようにセパレーターは子要素のみに作用します。
親要素、↑の例ではdivには作用しません。

get_text()でストリップする

get_text()で要素のテキストを取得するときにテキストの両端をストリップするにはstrip引数にTrueを指定します。

from bs4 import BeautifulSoup  

html = '''  
<div>  
  blue  
  <p>    yellow    </p>  
  <span>    green    </span>  
</div>  
'''  
soup = BeautifulSoup(html, 'html.parser')  
el = soup.find('div')  
print(soup.get_text(strip=True))  

↑の出力結果↓。

blueyellowgreen  

get_text()でタイプを指定する

get_text()はデフォルトでは↓の文字列のみ取得します。

  • NavigableString
  • CData

このデフォルトの状態ではたとえば<script>タグの中身は取得できません。
<script>タグの内容を取得したい場合はbs4.element.Scriptをタイプに指定します。

from bs4 import BeautifulSoup  
from bs4.element import Script  

html = '''  
<script>console.log(1)</script>  
'''  
soup = BeautifulSoup(html, 'html.parser')  
print(soup.get_text(types=(Script, )))  

textとstringの違い

textと似た属性にstringというのもあります。
textとの違いは、textが複数の要素を再帰的に取得するのに対し、stringは最初の子要素のみに作用する点です。
これはプロパティで、中身は↓のようになっています。

    @property  
    def string(self):  
        """Convenience property to get the single string within this  
        PageElement.  

        TODO It might make sense to have NavigableString.string return  
        itself.  

        :return: If this element has a single string child, return  
         value is that string. If this element has one child tag,  
         return value is the 'string' attribute of the child tag,  
         recursively. If this element is itself a string, has no  
         children, or has more than one child, return value is None.  
        """  
        if len(self.contents) != 1:  
            return None  
        child = self.contents[0]  
        if isinstance(child, NavigableString):  
            return child  
        return child.string  

↑の実装を見ると、contentsリストの最初の要素を取得して返していることがわかります。
stringの仕様はちょっと独特です。
まずcontents1じゃない場合にはNoneを返しています。
つまり子要素が複数ある要素の場合は常に得られるのはNoneです。
それからcontentsの最初の要素がNavigableStringでない場合は再帰的にテキストを取得します。

つまり↓のようなHTMLはstringNoneを返しますが、

<p>1</p>  
<p>2</p>  
<p>3</p>  

↓のようなHTMLではstringはテキストを取得できます。

<div><p><span>1</span></p></div>  

おわりに

BeautifulSoup4のtext属性は非常によく使うプロパティです。
使えるようになっておくと色々なシーンで役立ちます。

🦝 < textの正体はget_text()! 謎はすべて解けた!