Python自動化 #3 — Webスクレイピング ①: httpx と BeautifulSoup で静的ページを収集
毎日ブラウザで開いて確認するページが 1 つくらいはあるはずです。商品の価格、お知らせ、在庫状況のように、内容は毎回違っても確認する動作は同じです。ページを開き、同じ場所を見て、昨日と変わったかを比べます。このように動作が決まっている繰り返しは、コードが代わりにこなせます。今回は httpx で HTML を取得し、BeautifulSoup で目的の部分だけを選び出す静的ページスクレイピングを扱います。
- #1 スクリプトの第一歩
- #2 Excel自動化
- #3 Webスクレイピング ①: 静的ページ収集 ← 今回
- #4 Webスクレイピング ②: 動的ページ
- #5 メールと通知
- #6 スケジューリング
- #7 CLIツールに仕上げる
HTML がそのままデータです #
ブラウザが見せる画面の裏には、必ず HTML があります。価格表も、お知らせ一覧も、在庫表示も、結局はタグとテキストです。スクレイピングは、この HTML から目的のタグを見つけてテキストを取り出す作業です。
だから最初のステップはコードではなく観察です。収集したいページを開き、目的の値の上で右クリックして「検証」を押すと、開発者ツールの Elements タブが該当タグを見せてくれます。ここで確認することは 2 つです。
- 目的の値がどのタグの中にあるか (
<p>,<span>,<a>など) - そのタグを他のタグと区別してくれる目印が何か (class, id, 親の構造)
今回の実習対象は books.toscrape.com です。スクレイピングの練習用に公開された架空の書店サイトなので、安心して練習できます。本 1 冊が article.product_pod タグ 1 つで、その中にタイトルと価格が入っている構造です。
準備: パッケージ 2 つ #
プロジェクトに httpx と beautifulsoup4 を追加します。
uv add httpx beautifulsoup4httpx は HTTP リクエストを送るライブラリです。長く使われてきた requests と API がほぼ同じでありながら、デフォルトのタイムアウトが組み込まれていて、HTTP/2 と非同期までサポートするので、これから始めるなら httpx をおすすめします。beautifulsoup4 は、取得した HTML をパースしてタグ単位で探索できるようにするライブラリです。
httpx でページをリクエストする #
import httpx
headers = {
"User-Agent": "Mozilla/5.0 (compatible; book-scraper/1.0)"
}
resp = httpx.get(
"https://books.toscrape.com/",
headers=headers,
timeout=10.0,
)
resp.raise_for_status()
print(resp.status_code) # 200
print(resp.text[:300]) # HTML の先頭部分3 点を押さえます。
- User-Agent: リクエストを送るとき「誰がリクエストしたか」を知らせるヘッダーです。デフォルトのままだと
python-httpx/0.xとして送られ、これをブロックするサイトがあります。自分を識別できる文字列を入れておくのがよいです。 - timeout: 応答がないとき無限に待たないよう、制限時間を設けます。
httpxはデフォルトで 5 秒が設定されていますが、明示しておくと意図がはっきりします。 - raise_for_status: 404 や 500 のような失敗の応答なら例外を投げます。失敗した HTML を気づかずにパースする事故を防いでくれます。
BeautifulSoup で選び出す #
取得した HTML 文字列を BeautifulSoup に渡すと、タグ単位で探索できるオブジェクトになります。探索方法はいろいろありますが、CSS セレクタを使う select 1 つでほぼすべて解決します。
from bs4 import BeautifulSoup
soup = BeautifulSoup(resp.text, "html.parser")
books = soup.select("article.product_pod") # 本 1 冊 = article 1 つ
print(len(books)) # 20
first = books[0]
title = first.select_one("h3 a")["title"] # 属性から取り出す
price = first.select_one("p.price_color").text # テキストとして取り出す
print(title, price)select は条件に合うタグをすべてリストで、select_one は最初の 1 つだけを返します。よく使う CSS セレクタはこの程度で十分です。
article.product_pod: class がproduct_podのarticleタグ#content: id がcontentのタグh3 a:h3の内側のどこかにあるaタグul > li:ulの直下の子lia[href]:href属性を持つaタグ
開発者ツールで見た構造をセレクタに写し、select の結果の数が画面で数えた数と合うかを確認します。この過程こそがスクレイピングの本体です。
完成例: タイトルと価格を CSV に #
リクエスト、パース、保存を関数に分けて 1 ファイルにまとめます。
import csv
import httpx
from bs4 import BeautifulSoup
HEADERS = {"User-Agent": "Mozilla/5.0 (compatible; book-scraper/1.0)"}
def fetch(url: str) -> str:
resp = httpx.get(url, headers=HEADERS, timeout=10.0)
resp.raise_for_status()
return resp.text
def parse(html: str) -> list[dict]:
soup = BeautifulSoup(html, "html.parser")
rows = []
for book in soup.select("article.product_pod"):
rows.append({
"title": book.select_one("h3 a")["title"],
"price": book.select_one("p.price_color").text.lstrip("£"),
})
return rows
def save(rows: list[dict], path: str) -> None:
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["title", "price"])
writer.writeheader()
writer.writerows(rows)
if __name__ == "__main__":
rows = parse(fetch("https://books.toscrape.com/"))
save(rows, "books.csv")
print(f"{len(rows)}冊を保存しました")uv run scrape.py
# 20冊を保存しましたbooks.csv を開くと、タイトルと価格が表に整理されています。#2 で扱った Excel 自動化とつなげれば、収集からレポート整理まで一気に流れるパイプラインになります。
ページネーションの巡回 #
一覧が複数ページにまたがっているなら、URL のパターンを見つけて巡回します。books.toscrape.com は catalogue/page-2.html という形式でページが続きます。
import time
BASE = "https://books.toscrape.com/catalogue/page-{}.html"
all_rows = []
for page in range(1, 6): # まずは 1〜5 ページのみ
html = fetch(BASE.format(page))
all_rows.extend(parse(html))
print(f"{page}ページ収集、累計 {len(all_rows)}冊")
time.sleep(1) # リクエスト間に 1 秒待機
save(all_rows, "books_all.csv")ポイントは time.sleep(1) です。人間は 1 秒に 1 ページもめくれませんが、コードは止めなければ毎秒数十件をリクエストします。相手のサーバーから見れば攻撃と区別がつきません。リクエストの間に間隔を置くことは、選択ではなく基本です。
守るべき一線 #
技術的に可能なことと、やってよいことは別です。スクレイピングのコードを回す前に、次を確認します。
- robots.txt: サイトのアドレスの後ろに
/robots.txtを付けると、サイトが自動収集について示した方針が見られます。Disallowに指定されたパスは収集せず、Crawl-delayがあればその間隔を守ります。 - 利用規約: 自動収集を明示的に禁止するサービスがあります。ログインが必要なページなら、特に規約を先に確認する必要があります。
- リクエスト頻度: 間隔を空けずに大量のリクエストを送ると、相手のサーバーに実際の負担をかけます。
sleepを入れ、必要なページだけ取得します。 - 個人の自動化とサービス化の違い: 自分の確認作業を 1 日 1 回代行するスクリプトと、収集したデータを再配布したりサービスとして提供したりすることは、まったく別の問題です。後者は著作権と法的な検討が必要な領域なので、このシリーズの範囲を超えます。
取得した HTML が空っぽなら #
この方式が通用しないページもあります。応答は 200 なのに、肝心のデータが HTML にない場合です。
resp = httpx.get("https://example-spa.com/products")
print(resp.text)
# <div id="root"></div> だけで商品データはないこうしたページは、サーバーが空の骨組みだけの HTML を返し、ブラウザが JavaScript を実行したあとでデータが埋め込まれる構造です。httpx は JavaScript を実行しないので、空の骨組みだけを受け取ります。開発者ツールには見えるのに resp.text にはない、という場合がこれです。このときはブラウザそのものをコードで操縦する必要があり、その方法は次回扱います。
まとめ #
今回作った流れです。
- 開発者ツールで目的の値のタグと class を観察
httpx.getに User-Agent と timeout を明示し、raise_for_statusで失敗を遮断BeautifulSoupのselectと CSS セレクタで目的のタグだけを抽出- タイトルと価格を取り出して CSV に保存する完成スクリプト
- URL パターンの巡回 +
time.sleepで複数ページを収集 - robots.txt、利用規約、リクエスト頻度という守るべき一線
次回(#4 Webスクレイピング ②: 動的ページ)では、JavaScript がデータを描画するページを扱います。Playwright で実際のブラウザを立ち上げ、レンダリングが終わった画面からデータを取り出す方法を整理します。