Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
2
Help us understand the problem. What is going on with this article?
@amuyikam

BeautifulSoupで<br>タグを含む文字列の扱い(後半)

More than 1 year has passed since last update.

BeautifulSoup

BeautifulSoupはHTMLやXMLといった構造体からデータを取り出すためのライブラリです。
Webサイトから情報をスクレイピングしてくるのによく使われます。
https://www.crummy.com/software/BeautifulSoup/bs4/doc/

私はできる限りスクレイピングは使いたくないし使うべきではないと考えているのですが、残念ながらやらねばならないときもあるものです。
また、スクレイピングは節度を持って、ルールを守ってやりましょう。

この記事の内容

以下の2つの話題のうち、後者について挙動を確認します。
前者はこちら

  1. Tag.textとTag.stringの違い。特に文字列が<br>タグを含む場合はTag.stringはNoneになる。
  2. Tag.find(string='hoge')とTag.find(text='hoge')の違い。両者は同じになる。

環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.2
BuildVersion:   18C54
$ python --version
Python 3.7.2
$ pip show bs4
Name: bs4
Version: 0.0.1

Tag.find関数

前半でhtmlの一部から文字列を抽出する次のようなスクリプトを書きました。
この中で利用しているTag.find関数について調べます。

sample_code1.py
from bs4 import BeautifulSoup

html = '''
<p>hogefuga</p>
'''

soup = BeautifulSoup(html, 'html.parser')
text = soup.find('p').string
print(text)

ケース

次のhtmlの一部から文字列がhoge<br>fugaの行を抽出したい。
必ず2番目にあるとは限らない。

<a>hoge</a>
<a>hoge<br>fuga</a>
<a>fuga</a>

うまくいかない方法

次のようにしてもうまくいきません。

うまくいかない
from bs4 import BeautifulSoup
import re

html = '''
<a>hoge</a>
<a>hoge<br>fuga</a>
<a>fuga</a>
'''

soup = BeautifulSoup(html, 'html.parser')
print(soup.find('a', string='hoge<br>fuga')) # None
print(soup.find('a', string='hogefuga')) # None
print(soup.find('a', string=re.compile('hoge.*fuga'))) # None

BeautifulSoupで<br>タグを含む文字列の扱い(前半)に書いたように、BeautifulSoupを使う上でstringとtextは区別されます。そこでstringではなくtextを使うと良いのかと思いましたが、これもうまくいきません。

全然ダメ
from bs4 import BeautifulSoup
import re

html = '''
<a>hoge</a>
<a>hoge<br>fuga</a>
<a>fuga</a>
'''

soup = BeautifulSoup(html, 'html.parser')
print(soup.find('a', text='hoge<br>fuga')) # None
print(soup.find('a', text='hogefuga')) # None
print(soup.find('a', text=re.compile('hoge.*fuga'))) # None

Tag.findのstringキーワード引数とtextキーワード引数

Tag.find関数のドキュメントはこちら
これを読んでもわからないので、例によってソースを読みます。

まず、text引数とstring引数は、Tag._find_all関数の中で次のようにまとめられるためどちらを指定しても同じです。

        if text is None and 'string' in kwargs:
            text = kwargs['string']
            del kwargs['string']

もともとtextだったのですが、仕様が.stringと.textの関係と混ざって紛らわしいということでfind関数の仕様としてはstring引数になりました。
後方互換のためtext引数も利用できるようになっていますが、Tag.find関数やTag.find_all関数に関しては同じですので素直にstringを使いましょう。
という話がこのissueにかかれています。

Tag.find関数のstring引数の挙動

また、Tag.find関数やTag.find_all関数はTagに含まれる要素に対してnameやstringなどの条件を与えてfilter条件に合うかどうかを確認しています。
先程の例でうまく行かなかった原因はSoupStrainer.search_tag関数の次の3行が肝になります。

        if found and self.text and not self._matches(found.string, self.text):
            found = None
        return found

foundにはfilter条件に当てはまるTag(例: <a>hoge<br>fuga</a>)
self.textにはfilter条件(例: string='hoge'のhoge, ここのselfはSoupStrainer型)
が代入されています。
また最後にreturnする値がTag.findやTag.find_allから返される値になります。

このとき、 self._matches(found.string, self.text) でfoundの文字列とfilter条件を比較しています。
こちらで確認した通り、Tag.textはTagの中の文字列を再帰ですべて抽出しようとしますが、Tag.stringは中身の要素が2つ以上の場合にNoneを返します。
その結果、 Noneに対してfilter条件を適用することになってしまい、結果的にNoneがreturnされてしまいます。

上記挙動はどうも直感的ではなく、self._matches(found.text, self.text)にした方が良いのではないかとissueが上がっていますが、作者はいくつかの理由から変更する気はないようです。

うまくいく方法

issueの作者のコメントにもあるように、Tag.find関数のname引数やstring引数には独自関数を渡してfilter関数とすることができます。
ドキュメントだとここ

次のようにTag型の引数を1つとるfilter関数 filter_a_hogefugaを作ってやるとうまく抽出できます。
重要なのは、tag.stringではなくtag.textを利用しているところです。

関数を使ってうまくやる方法
from bs4 import BeautifulSoup

def filter_a_hogefuga(tag):
    if tag.name == 'a' and tag.text == 'hogefuga':
        return True
    return False

html = '''
<a>hoge</a>
<a>hoge<br>fuga</a>
<a>fuga</a>
'''

soup = BeautifulSoup(html, 'html.parser')
print(soup.find(filter_a_hogefuga))
実行結果
$ python sample.py
<a>hoge<br/>fuga</a>

lambdaでやるとよりスマートかもしれません。

関数を使ってうまくいく方法
from bs4 import BeautifulSoup

html = '''
<a>hoge</a>
<a>hoge<br>fuga</a>
<a>fuga</a>
'''

soup = BeautifulSoup(html, 'html.parser')
print(soup.find(lambda x: 'hogefuga' in x.text and x.name == 'a'))
実行結果
$ python sample.py
<a>hoge<br/>fuga</a>

よさそうです。

結論

BeautifulSoupで、Tag.textとTag.stringの挙動は異なるが、Tag.find関数やTag.find_all関数のtext引数とstring引数は同じ挙動になる。
また、filterに関数を利用することで<br>を含む文字列に対しても柔軟に目的のTagを抽出することができる。

参考

https://www.crummy.com/software/BeautifulSoup/bs4/doc/
https://bugs.launchpad.net/beautifulsoup/+bug/1518409
https://bugs.launchpad.net/beautifulsoup/+bug/1366856

2
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
amuyikam

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
2
Help us understand the problem. What is going on with this article?