蛇ノ目の記

技術のことも。そうでないことも。

BeautifulSoup4でコメントタグの中身を取得する方法に腹落ちするまでの話

同人音楽即売会M3の出展サークルリストから、サークル名やキーワードを抜こうとしている。何に使うかは未定だけど。

サークルリスト 2018年春 | M3 - 音系・メディアミックス同人即売会 を見てわかるように スペース, サークル名, 概要 となっている。サークルに関する情報はこれだけではなくて、ソースを見てみるとコメントとしてサークル名(カナ)やジャンルコード、キーワードが記述されている。キーワードも取りたくなってしまったのでひたすらググることになったという話。なお、BeautifulSoupに関しての説明はだいぶ省く。

<!--  アナベル    A01 女性ヴォーカル   ジョセイヴォーカル オリジナル オリジナル ポップス    ポップス    -->

コメントアウトされた情報はキーワードの数や並びが統一されていないのだけどそれは置いておく。これが実データのつらみか。

コメントタグの中身を取る方法

さて、beautifulsoup comment と検索するとトップにstackoverflowがヒットする。

stackoverflow.com

曰く、find_allメソッドのキーワード引数stringに以下のような無名関数を渡してやれば無名関数を渡してやればコメントが取れる。

lambda text: isinstance(text, Comment)

これで無事コメントタグの中身が取得できてめでたしめでたし、となるけどイマイチ腹落ちしなかったので find_allメソッドを調べた。

BeautifulSoupと無名関数については以下を参考にしたりした。

www.tomordonez.com

find_allメソッド

find_allメソッドは引数で指定した条件に一致するタグの要素を返す。

ここではpタグを取得してみよう。

from bs4 import BeautifulSoup as bs

markup = """
<body>
<h1>First Heading</h1>
<p>This is paragraph.</p>
<p>spam ham eggs</p>
<!-- This is comment -->
<!-- foo bar baz -->
</body>
"""

soup = bs(markup, 'html.parser')
paras = soup.find_all('p')
for p in paras:
    print(p)

実行結果は以下。

<p>This is paragraph.</p>
<p>spam ham eggs</p>

変数pTagオブジェクトで、get_text()メソッドでタグに囲まれた文字列を取得できる。

This is paragraph.
spam ham eggs

となる。

コメントタグの中身を取る方法: 再訪

先の例に当てはめて、コメントタグの中身を取得してみる。

from bs4 import BeautifulSoup as bs
from bs4 import Comment

markup = """
<body>
<h1>First Heading</h1>
<p>This is paragraph.</p>
<p>spam ham eggs</p>
<!-- This is comment -->
<!-- foo bar baz -->
</body>
"""

soup = bs(markup, 'html.parser')
comments = soup.find_all(string=lambda text: isinstance(text, Comment))
for c in comments:
    print(c)
 This is comment 
 foo bar baz 

CommentはBeatifulSoup4でコメントを表すクラス。

https://github.com/waylan/beautifulsoup/blob/master/bs4/element.py#L746L749

つまりCommentオブジェクトであるということが条件になっている。

次にstring引数について調べてみる。

https://www.crummy.com/software/BeautifulSoup/bs4/doc/#the-string-argument

With string you can search for strings instead of tags. As with name and the keyword arguments, you can pass in a string, a regular expression, a list, a function, or the value True.

正規表現やリスト、関数を渡すことができるとある。無名関数を渡しているのはそのためだった。

変数textがどのような値なのか気になって仕方ないので、普通の関数にして確認してみる。

from bs4 import BeautifulSoup as bs
from bs4 import Comment

def is_comment(text):
    print(text, type(text)
    return isinstance(text, Comment)

markup = """
<body>
<h1>First Heading</h1>
<p>This is paragraph.</p>
<p>spam ham eggs</p>
<!-- This is comment -->
<!-- foo bar baz -->
</body>
"""

soup = bs(markup, 'html.parser')
comments = soup.find_all(string=is_comment)
 <class 'bs4.element.NavigableString'>
 <class 'bs4.element.NavigableString'>
First Heading <class 'bs4.element.NavigableString'>
 <class 'bs4.element.NavigableString'>
This is paragraph. <class 'bs4.element.NavigableString'>
 <class 'bs4.element.NavigableString'>
spam ham eggs <class 'bs4.element.NavigableString'>
 <class 'bs4.element.NavigableString'>
This is comment <class 'bs4.element.Comment'>
 <class 'bs4.element.NavigableString'>
foo bar baz <class 'bs4.element.Comment'>
 <class 'bs4.element.NavigableString'>
 <class 'bs4.element.NavigableString'>

見づらいうえに改行も認識されてる(´・ω・`)

h1タグやpタグはNavigableString オブジェクト、コメントタグはCommentオブジェクトということがわかった。

ついでにパースしたHTMLの各行に対して処理が行われていることも実感できた。

まとめると、

  • find_allメソッドのstring引数には関数を渡すことができる

  • パースしたHTMLの各行が、タグの場合はNavigableString、コメントであればCommentオブジェクトになる

  • string関数に各行がCommentオブジェクトであることを判定する関数を渡すことで、コメントだけを取得できる

ひとまず腹落ちしたので心置きなくHTMLからコメントが取得できるようになった。

数や順番が統一されていないサークル情報をどう使うかを考えないとならないけど、それは別の話になる。