Pythonを使ったWebスクレイピングの比較的メジャーなライブラリBeautifulSoupのメソッドを一挙紹介します。
このページを読めばBeautifulSoupのほとんどの動作、メソッドを確認することができます。
以下の目次は展開でき、逆引きリファレンスの形式になっていますので、調べたい操作がある方は、気になる箇所へすぐにジャンプできます。
BeautifulSoupとは
一言で言うと、HTMLをパースするPythonのライブラリです。
スクレイピングという処理は、HTMLの取得と解析の二段構成です。
僕はHTMLの取得にはrequestsというモジュールを使うことが多いです。
一応標準のライブラリでもあるにはあるんですが、Pythonのhttpアクセスのディファクトスタンダードはrequestsかなと個人的には思っています。
BeautifulSoupは解析の部分を担当するライブラリですね。
主にスクレイピングという作業では目的のHTMLタグや、テキストの抜き出しに使うことが多いです。
とりあえずこれを使えばHTML色々いじれるんだー、便利なんだーって思ってもらえれば。
BeautifulSoupインストール
- bs4(BeautifulSoup)
- lxml
Anacondaでパッケージを管理していれば基本的にどちらも初期状態から入っています。
必要なHTMLを抜き出すためにbs4というモジュールを使います。Python用のHTMLパーサもいくつか存在しますが、bs4とlxmlの組み合わせは割とメジャーなのでこれらを使っていきます。
lxmlというのはパーサと言って、解析方法の一つです。Python標準の”html.parser”というパーサよりも高速で動作可能なのが特徴です。
ない場合はどちらもpipでインストール可能です。適宜インストールしてください。
sudo pip install bs4 sudo pip install lxml
BeautifulSoupの使い方
from bs4 import BeautifulSoup soup = BeautifulSoup(HTML_TEXT,'html.parser')
こんな感じでインポートして使います。
第一引数のHTML_TEXTの部分は取ってきたHTMLをそのまま渡して、第二引数はパーサを指定します。
パーサっていうのは、”読み取り方式” と思ってもらえればOKです。省略可能な引数で、省略した場合は、標準の’html.parser’が使用されます。
つまり例に挙げている’html.parser’の部分は全く書く必要がありません。
僕は、より実践的にこんな形で書くことが多いです。
from bs4 import BeautifulSoup as bs4 soup = bs4(res.content,'lxml')
インポートの時点でas bs4 として、今後はBeautifulSoupなんてながったらしく書かねーからな、という宣言を行います。
何回も書く可能性がある時に長いメソッド名書いているとそれだけでスペルミスとか、余計な失敗するので・・・。
bs4に渡している res.content というのはrequestsで取ってきたHTMLのbytes形式のデータです。
res.text を渡してももちろん動作するのですが、 res.content を渡した方が「文字化け」する可能性を減らせますのでこちらで書く癖をつけましょう。
上で少し説明しましたが、’lxml’というのはCで書かれたパース方式。標準の”html.parser”よりも早いです。ただ、HTMLの状況によってはパース出来ないことがたまにあります。
基本は’lxml’を使いつつ、なんか知らんが上手いこと行かない時は’html.parser’を使っています。
BeautifulSoupオブジェクトの操作
上の例のコードで挙げたsoup変数にはBeautifulSoupのオブジェクトが格納されています。
このsoupオブジェクトを操作して、htmlの中身を取得したり、書き換えたり出来ます。
タグ名で検索
最初のaタグを取得
soup.a soup.find('a')
どちらも同じ動作をします。html上で”最初に出てくるaタグ”を返します。
最初のaタグに囲まれたテキストのみを取得
soup.a.text soup.find('a').text # または soup.a.string soup.find('a').string
.textを付けるだけですね。<a></a>に囲まれた文字列だけを返します。
.stringでも似たような動作になりますが、状況によってはNoneが返ってくるパターンがあります。以下の記事で両者の明確な挙動の差を解説しています。
全てのaタグを取得
soup('a') soup.find_all('a')
どちらも同じ動作です。find_allは単純に省略可能です。
全てのaタグのテキストのみを取得
#取れません soup('a').text soup.find_all('a').text
soup使い始めて最初の頃、やりがちなんですが、.findの時とは違い、返ってくるのはリスト型(bs4.element.ResultSet)ですので、そのままテキストだけを抜き出すことはできません。
JQueryとか使い慣れてると何となく「いけんじゃねーかな」と思っちゃうんですよね。無理です!
#こうすれば取れます [tag.text for tag in soup('a')]
これですべてのaタグのテキストのみをリスト型で取得できます。
リスト内包表記というPythonの特徴的な書き方ですが、簡単に説明すると一行でfor文を書けて、結果をリスト型で返します。
上記例ではtag変数がリスト内全ての要素にアクセスして tag.text でtag内のテキストのみを抜き出しています。
指定タグ内の属性値を取得
例えば、多いパターンはaタグ内のhref属性を取得したいパターンなんかですね。
URLを集めたいスクレイピングなんかでよく使います。
soup.a.get('href') #最初のaタグのhref属性を取得 soup.find('a').get('href') #最初のaタグのhref属性を取得 [tag.get('href') for tag in soup('a')] #全てのaタグのhref属性を取得
上記はgetメソッドを使うパターンですが、下記はブラケットでアクセスするパターンです。
soup.a['href'] #最初のaタグのhref属性を取得 soup.find('a')['href'] #最初のaタグのhref属性を取得 [tag['href'] for tag in soup('a')] #全てのaタグのhref属性を取得
基本的にこれらはget()でアクセスした時と同じ結果を返します。
何が違うかというと、ブラケットアクセスの場合、その属性がタグ内に存在しなかった場合KeyErrorの例外が発生します。
対してget()メソッドの場合は “none” が返却され例外は発生しません。
属性名で検索
最初にクラス属性に一致するタグを取得
soup.find(class_='class_name')
class= ではなく class_= です。
Pythonではclassは予約語(別の意味のある)単語なのでそれと区別するためアンダースコアが必要です。
クラス属性に一致するタグを全て取得
soup(class_='class_name') soup.find_all(class_='class_name')
どちらも同じ動作。find_allは単純に省略可能です。
後から見た時の可読性を考慮すれば書いた方がいいかもしれません。
find_allなんて名前、何してるか一発で分かりますしね。
僕は書きません。
id属性に一致するタグを取得
soup.find(id='id_name')
id はそのまま id なんですね。ちなみにHTML構文の基本と矛盾しますが、
soup(id='id_name') soup.find_all(id='id_name')
こんな書き方も通ります。どうせ id を find_all しても一個しか出てこないんですけどね。この場合返ってくるのはリスト型です。
その他の属性に一致するタグを取得する
classとidはよく使うのでもちろんあって当然ですが、その他の属性一致も、.find() や .find_al() のメソッドで検索取得することが可能です。
# href 属性に一致する最初のaタグ soup.find(href="#") # href 属性に一致するすべてのaタグ soup(href="#") soup.find_all(href="#")
その他、各種属性検索もfind系のメソッドで取得可能です。
タグ内のテキストで検索して取得
テキストでの検索も可能です。注意点としては検索条件に一致するテキストを持っているタグが返ってくるのではなく、テキスト本体が一致して返ってきます。
以下で詳細を解説します。
テキストを検索して完全一致する文字列を取得
基本的にBeautifulSoupに実装されているテキスト検索は[text color=red]完全一致[/text]がベースです。そのままでは正規表現やワイルドカード的なものは使えません。
# 最初にsearch_textに完全一致する文字列を取得 soup.find(text='search_text') #search_textに完全一致する文字列を全て取得 soup(text='search_text') soup.find_all(text='search_text')
上位タグが返却されるわけではなく、文字列本体が返却されます。
つまり、上記の例の場合、返ってくる値は’search_text‘自体となりますので、このままではすでにメソッドに引数として渡してる値の取得になり、使い道はありません。
しかし、返却される形式が普通の文字列型(str)ではなく”[text color=red]NavigableString[/text]”ですので、BeautifulSoupの各種メソッドチェーンが適用可能です。
# 検索文字列を子要素に持つタグ名を取得 soup.find(text='search_text').parent.name # 検索文字列を子要素に持つタグを全て削除 [text.parent.decompose() for text in soup(text='search_text')]
などが実践的な使い方になります。
ちなみにこんな書き方でも同様の動作をします。
# text= が string= になっても同様の文字列検索 soup.find(string='search_text')
両者の引数としての違いは調べたけどわかりませんでした。
挙動的には違いは全く感じないですね。
アトリビュートとしては明確な違いがありますので、以下の記事を参考にしてみてください。
テキストを正規表現で検索して文字列を取得
上記の文字列検索では”完全一致”が前提でしたが、部分一致で取得する方法もあります。正規表現モジュール「re」を使います。
import re # searchで始まる文字列のNavigableStringを検索 search = re.compile('^search') soup.find(text=search) # searchで終わる文字列のNavigableStringを検索 search = re.compile('search$') soup.find(text=search)
正規表現ですので、あらゆる条件一致で文字列を探せます。
また似たような動作は後述するCSSセレクタ形式で取得できる.select()メソッドでも実現可能です。
複合的なタグ名または属性名で検索
複数の条件を組み合わせてタグを取得したい時があります。
soup("a", class_="class_name", href="href_text") soup.find_all("a", class_="class_name", href="href_text") soup('a',attrs={"class": "class_name", "href": "href_text"}) soup.find_all('a',attrs={"class": "class_name", "href": "href_text"})
全て同じ動作です。
attrsで指定するときはDict型(辞書型)で渡します。この場合classのアンダースコアはいりません。
CSSセレクタ型の検索
ここまで読んできて、JQueryを普段使い慣れている人からしたら、「BeautifulSoupめんどくさくね?」って思っているでしょう?
そんなあなたに、
soup.select('a > #id_name > .class_name') #aタグの子要素id_nameの子要素class_name soup.select('a[href^="href_text"]') #href属性がhref_textのaタグを取得
こんな感じでCSSセレクタも使えます。返却型はリスト型になります。
その他のタグの取得検索方法
上記メソッドの他にもいくつかタグやタグ内テキスト、属性などを絞り込んで取得する方法が存在しますので紹介します。
タグ名を返却
soup.a.name
ちなみにこの書き方だと当然 “a” がstring型で返却されてきますので、ほとんど意味がありませんね。メソッド書いてる時点で”a”だと確定しますからね。
基本的には取得自体より、比較とか代入が目的のアトリビュートです。
直下子要素をリスト形式で返却
soup.a.contents
そのタグ内の子要素をそれぞれ、要素として格納したリストを返却します。
直下子要素をリストイテレータ形式で返却
soup.a.children
根本の動作はcontentsと一緒なんですが、返却の形式がリストイテレータという、そのままでは参照できない形で返ってきます。
使いどころはイマイチ僕も分かりません。
子孫要素をジェネレータ形式で再帰的に返却
soup.a.descendants
DOMツリーの最深部に行きつくまで各要素を再帰的に取得し続けます。
子要素に含まれる文字列全てをジェネレータ形式で返却
soup.a.strings
文字列取得には他にも”.string”や”.text”など存在します。
両者の明確な違いについての記事も書いていますので、参考にしてみてください。
親要素全体を取得
soup.a.parent
先祖要素を再帰的に取得
soup.a.parents
“parent”に対して、複数形で再帰取得という感じですね。
BeautifulSoupはこのメソッドが単数形、複数形のそれぞれ存在しているパターンが多いので、気に留めておいてください。
最初のa要素の次の兄弟要素を取得
soup.a.next_sibling
最初のa要素の前の兄弟要素を取得
soup.a.previous_sibling
兄弟要素をジェネレータで一気に取得
上記で紹介したnext_sibling、previous_sibling はそれぞれ、”次”と”前”の兄弟要素を「ひとつだけ」取得する方法ですが、以下のようにすると、”以降の兄弟要素” と “以前の兄弟要素” を全て取得できるようになります。
# sibling を複数形の siblings にする soup.a.next_siblings soup.a.previous_siblings
ちなみに返却の形式はジェネレータ形式です。
parent と parents みたいな感じですね。
最初のa要素の次の兄弟要素を取得(要素内も順次取得)
soup.a.next_element
要素の書き換え
BeautiflSoupという名前だけあって、きれいにHTMLを整形したり、置き換えたりすることもできます。もしかしたら本来はこっちの使い方がメインなのかもしれませんが・・・。
純粋なスクレイピングを行うだけならあまり使う機会のないメソッドが多いかもしれませんが、機械的な引用転載なんかで活躍したりしますね。
要素の書き換えの基本事項
基本的には以下で紹介するメソッドは新規のsoupオブジェクトを返すワケではなく、現状のsoupオブジェクトを改変するものと思ってください。
Rubyでいう「破壊的メソッド」なイメージです。
書き換えだけではなく、以降に紹介する削除系のメソッドも同様に元本のオブジェクトを改変します。
また、オブジェクトが格納された変数などを操作して得られたオブジェクトは参照渡しとなります。
どういうことか、以下に例を挙げます。
soup = bs4(res.content,'lxml') #ここでbs4オブジェクトを作成 a_tags = soup('a') #すべてのaタグを取得し、a_tagsという変数に格納(リスト型) [tag.decompose() for tag in a_tags] #すべてのaタグを削除
後に紹介しますが、decompose()はタグを削除するメソッドです。
この場合、変数”a_tags“に格納されたaタグが全て削除されるのは理解できると思いますが、実は、aタグの削除は元の”soup“という変数にも及びます。
意外と見落としがちで、挙動が変なことに後から気づくケースが多いので、念頭に入れてください。
最初のdivタグをpタグに変更
soup.div.name = 'p'
divタグをpタグに変更します。この置き換えでは文字列で直接指定でき、new_tag()メソッドでの指定は必要ありません。
最初のpタグのタグ内テキストをreplace_textに変更
soup.p.string = "replace_text"
タグ内テキストを変更可能です。
ちなみに以下の方法では書き換えが失敗します。
soup.p.text= "replace_text" #失敗します
stringはbs4の独自クラス(bs4.element.NavigableString)textアトリビュートはpython標準クラスのstrクラスであることに起因します。
最初のp要素内にappend_textを追加
soup.p.append("append_text")
最初のa要素のhref属性を変更
soup.a['href'] = 'https://lets-hack.tech'
属性には[]ブラケットでアクセスできます。
その場合、オブジェクトtypeはbs4.element.Tagである必要があります。
最初のpタグを新規divタグで囲う
soup.p.wrap(soup.new_tag("div"))
指定した要素を覆うように新規タグを配置します。
soup.new_tag()メソッドでタグを作る必要があるのがミソです。
要素の削除
soupオブジェクトから要素を削除することが可能です。
要素の削除の基本事項
基本的には以下で紹介するメソッドは新規のsoupオブジェクトを返すワケではなく、現状のsoupオブジェクトを改変するものと思ってください。
Rubyでいう「破壊的メソッド」なイメージです。
削除だけではなく、上で紹介している書き換え系のメソッドも同様に元本のオブジェクトを改変します。
また、オブジェクトが格納された変数などを操作して得られたオブジェクトは参照渡しとなります。
どういうことか、以下に例を挙げます。
soup = bs4(res.content,'lxml') #ここでbs4オブジェクトを作成 a_tags = soup('a') #すべてのaタグを取得し、a_tagsという変数に格納(リスト型) [tag.decompose() for tag in a_tags] #すべてのaタグを削除
後に紹介しますが、decompose()はタグを削除するメソッドです。
この例の場合、変数”a_tags“に格納されたaタグが全て削除されるのはもちろん理解できると思いますが、実は、aタグの削除はaタグ取り出し元の”soup“という変数にも及びます。
意外と見落としがちで、挙動が変なことに後から気づくケースが多いので、念頭に入れておいてください。
最初のdivタグからクラス属性を削除
del soup.div["class"]
属性を削除したい場合はdelステートメントを使います。
最初のスクリプトタグを削除
decompose()はタグを丸ごと取り除きます。
soup.script.decompose()
string 系のメソッドで取得した文字列 (bs4.element.NavigableString)に対してはdecompose は効きません。例外が発生します。
NavigableStringクラスを削除したい場合は以下で紹介するextract()を利用する必要があります。
最初のpタグを取り出し、かつ、元の変数からその要素を削除
p = soup.p.extract()
decompose()と何が違うのかというと、extract()は結果を返すという点が異なります。
pという変数には最初のpタグが代入された状態になり、元のsoupオブジェクトからはそのタグが削除されます。
jQueryとか使ってる人なら「pop」がイメージしやすいですかね。
最初のpタグの中身を削除する
soup.p.clear()
decompose()との違いは中身だけを消去する点です。
つまり、この場合<p>~</p>の枠は残ります。
最初のaタグから中身を残してタグだけ削除
soup.a.unwrap()
指定した要素のタグだけ取り除きます。
この場合、aタグの中の文字列などの子要素は存続します。
親要素が消えるのではなく、指定されたそのタグ自身が消えることに注意。
jQuery使っていると少し違和感あるかもしれません。
消えたタグは bs4.element.Tagとして 返却されます。イメージとしてはextract()の中身消えない版です。
BeautifulSoupでXMLをパースする
BeautifulSoupはHTMLのパースというイメージがあるかと思いますが、実はXMLの解析にも使えます。
pythonのXML解析用のライブラリには他にもElementTreeなどがありますが、BeautifulSoupを使い慣れているならこちらの方が直感的に操作しやすいかもしれません。
メソッドなどの取り扱いはHTMLをパースする時とほとんど同じです。
ただ、XMLのパース時は少しだけ、オブジェクトの作り方が異なります。
import requests from bs4 import BeautifulSoup as bs4 res = requests.post(XML) #詳しくは省略、XMLデータが返ってくるものと思ってください soup = bs4(res.content,'lxml-xml') #←ここがちょっと違う
基本的にはパーサに’lxml-xml‘を指定するのがいいと思います。
‘lxml’のままでも動くには動きますが、キャメルバックのタグが全て小文字になったり、少し挙動が変化します。
特に理由がないのであれば専用のパーサを使っておくのが無難です。
BeautifulSoupを用いたXMLの解析は以下の記事で、例として、AmazonのMWSから取得できるXMLデータを用いて詳細に解説しています。
BeautifulSoupでXMLドキュメントを作成する
HTMLを削除・改変・作成できることなどから推測できるかもしれませんが、実はXML形式のドキュメントの作成もBeautifulSoupによって行うことができます。
リクエストにXML形式のドキュメントをpostする必要があった時にこの方法で作成してみました。
詳しくはこちらの記事で解説しています。
BeautifulSoup 総括
スクレイピングでは主に要素の取得が目的になることが多いかと思いますが、実際のBeautifulSoupはjQueryライクな、総合的にHTMLの加工をすることを得意としています。
CSSセレクタ型の操作にも対応していて、Pythonによるスクレイピングでは手放せない便利なライブラリです。
コメント