毎朝おなじサイトを開いて、新着情報の欄だけを目で追って、更新があればメモする。そんな確認作業をしていませんか。
数分で終わる作業でも、毎日となると地味に負担です。しかも見落としたときにかぎって大事な更新だったりします。
Webページの中身をPythonから読み取って、必要なところだけ抜き出す。それができると、この手の確認作業はまるごと自動化できます。
その入り口になるのがBeautifulSoupというライブラリです。HTMLという少し取っつきにくい文字の塊を、Pythonから扱いやすい形に整理してくれます。
この記事では、BeautifulSoupの基本と、初心者がつまずきやすいところを順番に見ていきます。
BeautifulSoupは、HTMLを読み解いてくれる道具¶
まずは役割をはっきりさせておきましょう。BeautifulSoupは、HTMLの文字列を受け取って、タグの入れ子構造として扱えるようにしてくれるライブラリです。
公式の説明でも、HTMLやXMLのパーサーの上に乗って、探索や検索をPythonらしい書き方で行えるようにするもの、と位置づけられています。
ここで大事なのは、BeautifulSoupはページを取ってくる役目を持っていないという点です。するのは、あくまで手元にあるHTMLを読み解くことだけ。
なので実際の作業は、2つの道具の分担になります。役割を混同すると調べものが遠回りになるので、先に整理しておきます。
| 道具 | 役割 | よく使う書き方 |
|---|---|---|
| requests | Webサーバーからページを取ってくる | requests.get(url) |
| BeautifulSoup | 取ってきたHTMLから欲しい部分を探す | soup.select("h2") |
| csvやopenpyxl | 取り出した結果を保存する | writer.writerow(row) |
ページを取ってくる側の話は別の記事にまとめてあります。あわせて読むと全体像がつかみやすいはずです。【関連記事】Pythonのrequestsとは?WebからデータをとってくるHTTP通信の基本を初心者向けに解説
準備はpipのインストールから¶
BeautifulSoupは標準ライブラリではないので、まずインストールが必要です。パッケージ名が少し独特なので、そこだけ注意してください。
pip install beautifulsoup4
インポートするときの名前は bs4 です。インストール名とインポート名が違う、初心者泣かせのライブラリの代表格でもあります。
プロジェクトごとに仮想環境を分けておくと、ライブラリのバージョン違いで悩まされることが減ります。【関連記事】Pythonの仮想環境(venv)って何のためにある?プロジェクトごとに混ぜない管理法
パーサーは最初はhtml.parserでいい¶
BeautifulSoupを作るときには、HTMLをどう解釈するかを決めるパーサーを指定します。選択肢は3つありますが、最初は迷わなくて大丈夫です。
| 指定 | 追加インストール | 特徴 |
|---|---|---|
"html.parser" |
不要(Python標準) | まずはこれで十分。速度も実用範囲 |
"lxml" |
pip install lxml が必要 |
速い。大量のページを処理するときに効く |
"html5lib" |
pip install html5lib が必要 |
ブラウザに近い解釈をする。壊れたHTMLに強い |
第2引数を省略すると、環境にあるパーサーが勝手に選ばれます。そのとき警告が出るのですが、これは環境が変わると結果も変わりうるという注意です。
私は10年ほどエンジニアとして開発に関わってきましたが、パーサーを省略したまま書いたコードが、自分の手元では動くのにサーバーでは空振りする、という事故を一度やっています。第2引数は必ず書く、とだけ覚えておいてください。
まずはHTMLから1つ取り出してみる¶
いきなり実在のサイトを相手にすると、うまくいかない原因が絞りづらくなります。最初は手元の文字列で試すのが近道です。
次のコードは、お知らせ一覧のような小さなHTMLから、リンクの文字を取り出す例です。
from bs4 import BeautifulSoup
html = """
<ul class="news">
<li class="item"><a href="/news/1">セミナー開催のお知らせ</a></li>
<li class="item new"><a href="/news/2">夏期休業について</a></li>
</ul>
"""
soup = BeautifulSoup(html, "html.parser")
print(soup.find("a").get_text()) # セミナー開催のお知らせ
print(len(soup.find_all("a"))) # 2
print(soup.find("a")["href"]) # /news/1
get_text() はタグの中の文字だけを取り出すメソッドです。角かっこで属性を指定すると、リンク先などの値が取れます。
findとfind_allの違い¶
名前が似ている2つですが、返ってくるものがまったく違います。ここを取り違えるとエラーの原因になります。
find() は最初の1つ、find_all() は見つかったものすべてを返します。1つだけ欲しいのか、全部欲しいのかで使い分けてください。
クラス名で絞り込むときは、引数名が class_ になります。class はPythonの予約語なので、末尾にアンダースコアが付く決まりです。
items = soup.find_all("li", class_="item")
print(len(items)) # 2
CSSセレクタで狙い撃ちする¶
タグ名だけで探すと、目的以外のものまで拾ってしまうことがあります。そんなときに便利なのが select() です。
CSSセレクタという、Webページの見た目を指定するときと同じ書き方が使えます。入れ子の条件をそのまま書けるので、複雑な構造でも一行で済みます。
titles = [a.get_text() for a in soup.select("ul.news li a")]
print(titles) # ['セミナー開催のお知らせ', '夏期休業について']
print(soup.select_one("li.new a").get_text()) # 夏期休業について
select() が全件、select_one() が1件です。findとfind_allの関係とおなじと考えれば覚えやすいはずです。
なおCSSセレクタの解釈は、BeautifulSoupに同梱されるSoup Sieveというライブラリが担当しています。バージョン4.7.0以降はこれが公式の実装になっているので、かなり細かいセレクタまで通ります。
requestsと組み合わせて実際のページを読む¶
手元のHTMLで感覚がつかめたら、いよいよ本物のページに向かいます。取ってくる部分をrequestsに任せるだけで、形はほとんど変わりません。
import requests
from bs4 import BeautifulSoup
res = requests.get("https://example.com/news/", timeout=10)
res.raise_for_status()
soup = BeautifulSoup(res.text, "html.parser")
for a in soup.select("ul.news li a"):
print(a.get_text(strip=True), a["href"])
raise_for_status() は、404などの失敗を見逃さないための一行です。これが無いと、エラーページのHTMLを真面目に解析してしまいます。
get_text(strip=True) の strip は、前後の余分な空白や改行を落としてくれる指定です。HTMLは見た目を整えるための改行が多いので、付けておくと結果がきれいになります。
つまずきやすいのは、だいたいこの2つ¶
ここまで来ると動くコードが書けます。ただ、実際のページを相手にすると必ずと言っていいほど同じところで止まります。
NoneType object has no attribute というエラー¶
いちばん多いのがこれです。原因はシンプルで、探した要素が見つからなかったから。
find() や select_one() は、見つからないとエラーではなく None を返します。その None に対して get_text() を呼ぶので、そこで落ちるわけです。
target = soup.select_one("h1.title")
if target is None:
print("見つかりませんでした")
else:
print(target.get_text(strip=True))
セレクタの打ち間違いのほか、JavaScriptで後から表示される部分だった、という場合もあります。ブラウザで見えているのにPythonからは取れないときは、これを疑ってください。
リンクが途中までしか取れない¶
もうひとつが相対URLです。ページの中のリンクは /news/1 のように途中から書かれていることが多く、そのままでは開けません。
標準ライブラリの urljoin を使うと、元のURLとつなげて完全な形にできます。
from urllib.parse import urljoin
print(urljoin("https://example.com/news/", "/news/1"))
# https://example.com/news/1
なお、HTMLから情報を抜き出す作業を正規表現だけでやろうとすると、たいてい破綻します。タグの入れ子は正規表現が苦手とする構造だからです。【関連記事】正規表現とは?複雑な文字列検索や置換を一瞬で終わらせる
技術より先に、取り方のマナーを知っておく¶
ここまで読むと何でも取れそうな気がしてきますが、いちばん大事なのはここからです。相手のサーバーは、他人の持ち物だという前提を忘れないでください。
短い間隔で何百回もアクセスすれば、相手にとっては攻撃と区別がつきません。最低限、次の3つは確認する習慣をつけてください。
サイトの利用規約を読むこと。robots.txtで断られている場所には行かないこと。そして、繰り返し取得するなら1回ごとに待ち時間を入れることです。
robots.txtは、クローラーに対してどこを見ていいかを伝えるファイルです。2022年にRFC 9309として標準化されており、Pythonにはこれを読むための標準ライブラリまで用意されています。
import time
from urllib.robotparser import RobotFileParser
rp = RobotFileParser()
rp.set_url("https://example.com/robots.txt")
rp.read()
if rp.can_fetch("*", "https://example.com/news/"):
print("取得してよい")
time.sleep(3) # 次のアクセスまで間を空ける
can_fetch() は、そのURLを取りに行っていいかを真偽値で返します。crawl_delay() で待つべき秒数が書かれていれば、それも読み取れます。
新人のころ、待ち時間を入れないまま検証用のスクリプトを回してしまい、先方から連絡が来たことがあります。取得の間隔を空けるのは技術ではなく礼儀だと、そのとき教わりました。
取り出したデータをどう残すか¶
画面に表示するだけでは、翌日には消えてしまいます。取り出したものは、後から見返せる形で保存しておきましょう。
表のようなデータなら、標準ライブラリのcsvが手軽です。文字化けを避ける書き方まで含めて、こちらで詳しく扱っています。【関連記事】Pythonのcsvモジュールとは?表データの読み書きと文字化けの防ぎ方を初心者向けに解説
保存までできたら、あとは自動で動かすだけです。毎朝決まった時刻に走らせておけば、起きたときには結果が手元にあります。【関連記事】Pythonで定期実行するには?cronとタスクスケジューラの基本
まとめ¶
BeautifulSoupは、HTMLという読みにくい塊を、Pythonから扱える形に整えてくれる道具です。ページを取ってくるrequestsと組み合わせて使います。
覚えることは多くありません。find と find_all、select と select_one、そして get_text() の5つで、たいていの用は足ります。
つまずいたときは、まず取れているHTMLを疑ってください。見えているものと取れているものは別だと分かれば、原因の切り分けはぐっと早くなります。
そして、相手のサーバーへの配慮を最優先に。この一点さえ守れれば、スクレイピングは日々の作業を確実にラクにしてくれます。
まずは手元の文字列を相手に、select() を一度動かしてみるところから始めてみてください。
ここまでお読みいただきありがとうございました。