1
0

More than 1 year has passed since last update.

【備忘録】Beautiful Soup 4の.stringの挙動を正しく理解していなかった話

Last updated at Posted at 2022-02-26

要約

.stringは挙動をしっかり把握して使わなくてはいけない。

子タグを残して文字列を取得したいときはBeautiful Soup 4を利用せずにre.sub()などを用いることを検討するべきかもしれない。

2022年2月27日追記

コメントで情報をいただきました.contentsを利用すれば子タグを残して文字列が取得できるそうです。

公式ドキュメント(日本語訳版)へのリンク⇒http://kondou.com/BS4/#contents-children

環境

  • python : 3.10.1
  • BeautifulSoup4 : 4.10.0
  • Windows11

経緯

先日、こちらの記事で宣伝したこのプログラムに「<br/>を含む<span>タグの内側の文字列がなぜかちゃんと出力されない」問題が見つかりました。

問題のコードがこちら(2022年2月27日追記:コメントを受けてコードを整形しました):

app.py
    def __search_and_get(self):
        messages = self.soup.find_all('p')
        get_inner = slice(1, -1)
        for msg in messages:
            inlist = []
            for span in msg.find_all('span'):
                st = span.string
                if str is not None:
                    str = str.strip()
                else:
                    str = ''
                if '#' in str:
                    str = str.replace('#', '\\#')
                inlist.append(str)
            inlist[0] = inlist[0][get_inner]  # タブ名の[]の内側の文字列を取得
            self.meslist.append(inlist)

(タグ名).stringで取得できる値はstr型だと思い込んでいた(もしくはstr型ではないにせよstr型と同じように扱って良いものと考えていた)ころに書いたものです。

たとえば、次のようなinput.htmlがあったとします

<!DOCTYPE html>
    <head>
        <!--関係ないので省略-->
    </head>
    <body>
        <p>
            <span>[hoge]</span>
            <span>fuga</span>:
            <span>改行がない文章です</span>
        </p>
        <p>
            <span>[hoge]</span>
            <span>fuga</span>:
            <span>改行がある<br/>文章です</span>
        </p>
    </body>
</html>

問題のコードを書いたとき、私は次のようなリストが生成されることを期待していました。

想定
[['hoge', 'fuga', '改行がない文章です'], ['hoge', 'fuga', '改行が<br/>ある文章です']]

しかし実際は、こうなります。

現実
[['hoge', 'fuga', '改行がない文章です'], ['hoge', 'fuga', '']]

strNoneだったらstr''を代入する処理を入れていたので、実際にspan.stringから返ってきた値はNoneということになりますね。

なぜNoneが返されたのか?

BeautifulSoupでstringとtextの挙動の明確な違い – Pythonによると、

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

とのことです。

リンク先に詳しい解説が掲載されていますので、ちゃんとした説明はそちらに任せます。

ざっくりと、私の理解の範囲で言えば、指定要素内の文字列はタグの存在した位置で分割された状態で保持されており、.stringは文字列が複数に分割されている場合はNoneを返すようになっている……ということのようです。

なので、文字列がタグで分断さえされていなければ——たとえば、<span><pre>別のタグで囲まれた文章です</pre></span>などは——Noneは返ってきません。

<span>改行がある文章です<br/></span>ならNoneは返ってこない?

文字列の途中にタグが入っていなければいいのかと言うと、どうやらそういうわけでもないようです。

<span>改行がある文章です<br/></span>の場合は、Noneが返ってきました。

これは['改行がある文章です', '']として処理されているということなのかと思いましたが、

print(list(span.strings))

すると、

['改行がある文章です']

と表示されてしまいました。

ほかにもいくつか実験してみたのでその結果を今までのものと一緒に、以下の表にまとめました。

番号 対象文字列 結果
1. <span>文章です<br/></span> None
2. <span>文章です<hr></span> None
3. <span>文章です<pre></pre></span> None
4. <span><pre>文章です</pre></span> '文章です'

Beautiful Soup 4のソースコードをちゃんと読んでいないのであくまで仮説ですが、テキストの後ろに開始タグが存在したため、長さ4の文字列と長さ0の文字列に分割されてしまったのでしょう。

結局どうしたのか

話を私が作っているプログラムの方に戻します。

結局、<br/>で示された改行の情報は残しておきたかったのでget_text().textは使わず、re.sub()を利用することにしました。

(2022年2月27日追記:コメントを受けてコードを整形及び一部変更しました)

    def _search_and_get(self):
        messages = self.soup.find_all('p')
        get_inner_slice = slice(1, -1)
        for msg in messages:
            tmplist = list()
            for span in msg.find_all('span'):
                if span is None:
                    s = ''
                else:
                    s = str(span)
                s = re.sub(r'</?span>', '',
                           s.replace('<br/>', '\n')).strip()
                if '#' in s:
                    s = s.replace('#', '\\#')
                tmplist.append(s)
            tmplist[0] = tmplist[0][get_inner_slice]  # タブ名の[]の内側の文字列を取得
            self.meslist.append(tmplist)

以上です。

参考

2022年2月27日追記:

1
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0