概要(BeautifulSoupは「HTMLから欲しい所だけ抜く」ための定番)
BeautifulSoupは、取得したHTMLを“読みやすい木構造”にして、タグ名・クラス名・CSSセレクタで必要部分だけを取り出すためのライブラリです。初心者がまず押さえるのは「HTML取得(requests)→ 解析(BeautifulSoup)→ 抽出(find/select)→ 整形・保存」の流れです。重要ポイントは、文字化け対策、セレクタの選び分け、例外とタイムアウト、サイトの規約と負荷への配慮です。
はじめ方(インストールと最短の抽出)
インストールと基本の流れ
# インストール
# pip install requests beautifulsoup4 lxml
import requests
from bs4 import BeautifulSoup
url = "https://example.com"
resp = requests.get(url, timeout=5)
resp.raise_for_status()
resp.encoding = resp.apparent_encoding # 文字化け対策(推定で上書き)
soup = BeautifulSoup(resp.text, "lxml") # 解析(パーサにlxmlを指定)
title = soup.title.get_text(strip=True)
print(title)
Python「requestsでHTMLを取得」「BeautifulSoupで解析」「get_textや属性アクセスで取り出す」が最短ルートです。パーサは標準のhtml.parserでも動きますが、lxmlの方が速くて頑丈なことが多いです。
見出し・リンク・本文の基本抽出
from bs4 import BeautifulSoup
import requests
resp = requests.get("https://httpbin.org/html", timeout=5)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "lxml")
h1 = soup.find("h1").get_text(strip=True)
links = [a["href"] for a in soup.find_all("a", href=True)]
paragraphs = [p.get_text(strip=True) for p in soup.find_all("p")]
print(h1, links[:5], paragraphs[:3])
Pythonfindは最初の一致、find_allは全部。hrefのような属性は辞書アクセスで取り出せます。get_text(strip=True)で前後の空白を除去して整えます。
抽出の要点(タグ・クラス・CSSセレクタの選び分け)
タグ名・クラス名で精度を上げる
soup = BeautifulSoup(html, "lxml")
# 例:記事リストのタイトル(div.article > a.title)
items = soup.find_all("div", class_="article")
titles = [it.find("a", class_="title").get_text(strip=True) for it in items]
print(titles[:10])
Pythonclass属性でグッと絞ると、不要な要素を拾いにくくなります。idは一意なことが多いので、特定要素の取得に向いています。
CSSセレクタ(select / select_one)が強力
# 例:ul#news li > a.title のテキスト
for a in soup.select("ul#news li > a.title"):
print(a.get_text(strip=True), a["href"])
PythonCSSセレクタは「#id」「.class」「親子・子孫(> / 空白)」が使え、フロントエンド経験者には直感的です。複雑な構造でも短く書けます。
属性で条件指定(部分一致や存在チェック)
# data-* 属性を持つボタンを抽出
buttons = soup.select('button[data-action]')
# 特定パターンのhrefだけ
docs = soup.select('a[href*="/docs/"]') # 部分一致はCSSではなく、後からifで絞ることも多い
Python属性存在([attr])や値指定([attr=”…”])で精度を上げます。部分一致は最終的にPython側でフィルタするのが確実です。
文字コード・整形・保存(使える形に仕上げる)
文字化け対策と安全なテキスト抽出
resp = requests.get(url, timeout=5)
resp.raise_for_status()
resp.encoding = resp.apparent_encoding # 推定で上書き
soup = BeautifulSoup(resp.text, "lxml")
text = soup.get_text("\n", strip=True) # ページ全体のテキスト(改行区切り)
Pythonapparent_encodingは万能ではありません。日本語サイトでmeta charsetやHTTPヘッダーが正しく付いていない場合、推定上書きが役立ちます。
CSVやJSONでの保存
import csv, json
from pathlib import Path
records = [{"title": t, "href": h} for t, h in zip(titles, links)]
Path("out").mkdir(exist_ok=True)
with open("out/articles.csv", "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=["title", "href"])
w.writeheader(); w.writerows(records)
with open("out/articles.json", "w", encoding="utf-8") as f:
json.dump(records, f, ensure_ascii=False, indent=2)
Python整形後はCSV/JSONで保存すると、後続の分析や再利用が楽になります。ensure_ascii=Falseで日本語をそのまま保存できます。
実務の型(例外処理・速度・マナー・動的ページ)
例外・タイムアウト・リトライ(壊れない取得)
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=Retry(total=3, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504])))
try:
r = session.get("https://example.com", timeout=5)
r.raise_for_status()
except requests.Timeout:
print("タイムアウト")
except requests.ConnectionError:
print("接続エラー")
except requests.HTTPError as e:
print("HTTPエラー:", e)
Pythonタイムアウトは必須です。raise_for_statusで失敗を例外にし、5xx限定で短いバックオフのリトライが有効です。
robots.txt・規約・アクセス間隔(マナーと法的配慮)
スクレイピングが許可されているかをサイトの利用規約やrobots.txtで確認します。取得間隔を空け、必要最小限のアクセスに留め、APIが提供されているならAPIを利用します。機密ページや認証が必要な領域の無断取得は避けます。
動的レンダリング(JavaScript生成)への対処
BeautifulSoupは“取得時点の静的HTML”しか見えません。JavaScriptで後からDOMが生成されるサイトは、APIを探す、ネットワークタブでJSONを特定する、どうしても必要なら自動ブラウザ(Selenium/Playwright)を使うなどで対応します。
速度と安定性(セレクタ・パーサ・分割保存)
セレクタはできる限り絞り、不要な全ページ取得を避けます。パーサはlxmlが速い傾向。大量ページでは小分け保存と途中再開(チェックポイント)で安定させます。
例題で身につける(定番から一歩先まで)
例題1:ニュース一覧からタイトルとリンクを抽出
import requests
from bs4 import BeautifulSoup
url = "https://httpbin.org/html"
r = requests.get(url, timeout=5)
r.raise_for_status()
r.encoding = r.apparent_encoding
soup = BeautifulSoup(r.text, "lxml")
titles = [a.get_text(strip=True) for a in soup.select("a.title")]
links = [a["href"] for a in soup.select("a.title[href]")]
for t, h in zip(titles, links):
print(t, h)
PythonCSSセレクタで狙い撃つと、短いコードで読みやすく書けます。
例題2:テーブルから行データを抽出してCSV化
import csv, requests
from bs4 import BeautifulSoup
r = requests.get("https://httpbin.org/html", timeout=5)
r.raise_for_status()
soup = BeautifulSoup(r.text, "lxml")
rows = []
for tr in soup.select("table.data tr")[1:]: # ヘッダ行を除外
cells = [td.get_text(strip=True) for td in tr.select("td")]
rows.append(cells)
with open("table.csv", "w", newline="", encoding="utf-8") as f:
csv.writer(f).writerows(rows)
Pythonテーブルはtr/tdで素直に辿ります。ヘッダ行の扱いだけ意識しましょう。
例題3:ページネーションを辿ってまとめ取得
import time, requests
from bs4 import BeautifulSoup
base = "https://example.com/list?page={}"
all_titles = []
for page in range(1, 4):
r = requests.get(base.format(page), timeout=5)
r.raise_for_status()
soup = BeautifulSoup(r.text, "lxml")
titles = [a.get_text(strip=True) for a in soup.select("h2.item-title")]
all_titles.extend(titles)
time.sleep(0.3) # 負荷軽減の小休止
print(len(all_titles), "items")
Python複数ページは「上限を決めて」「間隔を置いて」取得します。次ページのリンク(a.next)を辿る方式でもOKです。
例題4:詳細ページへジャンプして属性を収集
import requests
from bs4 import BeautifulSoup
index = requests.get("https://example.com/products", timeout=5); index.raise_for_status()
soup = BeautifulSoup(index.text, "lxml")
items = []
for a in soup.select("a.product[href]"):
detail_url = a["href"]
r = requests.get(detail_url, timeout=5); r.raise_for_status()
d = BeautifulSoup(r.text, "lxml")
name = d.select_one("h1.product-name").get_text(strip=True)
price = d.select_one("span.price").get_text(strip=True)
items.append({"name": name, "price": price})
print(items[:3])
Python「一覧→詳細」の2段構えは定番です。失敗時の例外処理と途中保存を入れると実務で安定します。
まとめ
スクレイピングの骨子は「requestsでHTML取得→BeautifulSoupで解析→セレクタで抽出→整形・保存」です。重要なのは、文字化け対策(encoding)、findとselectの使い分け、例外処理とタイムアウト、ページネーションの小休止、サイト規約と負荷への配慮です。静的HTMLが基本で、動的生成はAPIや自動ブラウザの検討が必要。これらを型として覚えれば、初心者でも短いコードで“必要な情報だけを正確に抜く”スクレイピングが安定して書けます。
