要約
.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日追記:コメントを受けてコードを整形しました):
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', '']]
str
がNone
だったら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日追記: