Python基礎

Webスクレイピング:requests×BeautifulSoupの基本|“合法×丁寧×再現性”でデータ取得を設計する

スクレイピングって、まず何から始めればいい?

禁止されていないか不安だし、アクセスのマナーや失敗しない実装も知りたい…!

結論は「合法性の確認 → 丁寧なアクセス → 再現性ある実装」の三位一体。本記事では、現場で使い倒してきたrequests+BeautifulSoupの実務テンプレを、そのままコピペで動くコードつきで解説します。

この記事で身に付く力

  • 合法性チェックの型(規約/robots.txt/API優先)
  • 丁寧なアクセス(遅いリクエスト/UA明示/429対処)
  • 再現性ある実装(例外・リトライ・ログ・テスト)
  • 壊れにくいセレクタ設計と入出力(CSV/Parquet)

まずは前提:スクレイピングの3つの落とし穴

よくある事故は(1)規約違反(2)過剰アクセス(3)脆い実装。これを避ける鍵が合法×丁寧×再現性です。
公式にAPIやダウンロードが用意されているなら必ずAPI優先、HTMLは最後の手段が鉄則。

想定読者

Pythonの基本文法とパッケージインストールができ、小規模〜中規模の業務自動化データ収集を自分で回したい方。

著者からひとこと(ふみとの実体験)

データ/マーケサイエンティストとして10年、数十案件でスクレイピングを運用してきました。深夜にDOM変更で落ちた経験や、429でレート制限を受けた反省から、「まず規約」「人より遅く」「テストで再現」の型に落ち着いています。この記事はその型を凝縮しています。

準備:依存関係とディレクトリ

requests>=2.32
beautifulsoup4>=4.12
lxml>=5
pandas>=2.2
project/
  scrape/
    __init__.py
    main.py
    fetch.py
    parse.py
    io_utils.py
  tests/
    test_parse.py
  data/
    raw/
    out/

合法性チェック:必ず最初にやること

事前確認リスト

  • 利用規約/著作権/二次利用条件を確認
  • robots.txtUser-agent: *Disallowがないか
  • 公式API/データ配布があればAPI優先
  • ログイン・会員限定/CAPTCHA/技術的防御は回避せず撤退
  • 個人情報・機微情報は収集しない

関連記事: [内部リンク:Webスクレイピングの法的リスクと安全運用]

from urllib.parse import urlparse
from urllib.robotparser import RobotFileParser

def is\_allowed(url: str, user\_agent: str = "pythonbunseki-bot/1.0") -> bool:
u = urlparse(url)
robots = f"{u.scheme}://{u.netloc}/robots.txt"
rp = RobotFileParser()
try:
rp.set\_url(robots)
rp.read()
return rp.can\_fetch(user\_agent, url)
except Exception:
\# 取得不可は安全側でFalse(人の判断を挟む)
return False

丁寧なアクセス:Session・リトライ・バックオフ

User-Agentを明示し、人間より遅いアクセス+429/5xxのみ限定リトライが基本です。

import time, random, requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

UA = "pythonbunseki-bot/1.0 (+[https://pythonbunseki.com](https://pythonbunseki.com))"

def make\_session() -> requests.Session:
s = requests.Session()
s.headers.update({
"User-Agent": UA,
"Accept-Language": "ja,en;q=0.8",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
})
retry = Retry(
total=3,
backoff\_factor=0.7,
status\_forcelist=\[429, 500, 502, 503, 504],
allowed\_methods=\["GET", "HEAD"],
respect\_retry\_after\_header=True,
)
s.mount("http\://", HTTPAdapter(max\_retries=retry))
s.mount("https\://", HTTPAdapter(max\_retries=retry))
return s

def get\_html(s: requests.Session, url: str, \*, min\_delay=1.0, max\_delay=2.5, timeout=10) -> str:
if not is\_allowed(url, UA):
raise PermissionError(f"robots.txt disallow: {url}")
r = s.get(url, timeout=timeout)
if r.status\_code == 429:
raise RuntimeError("rate limited: 429")  # ここで停止/再開判断
r.raise\_for\_status()
if not r.encoding:
r.encoding = r.apparent\_encoding
html = r.text
time.sleep(random.uniform(min\_delay, max\_delay))  # 人間より遅く
return html

解析:BeautifulSoupで“壊れにくい”セレクタ

装飾クラスに依存せず、意味のある属性安定クラスを優先。テーブルは辞書化が有効です。

from bs4 import BeautifulSoup
from dataclasses import dataclass

@dataclass
class Item:
title: str
price: int | None
url: str

def parse\_list(html: str) -> list\[str]:
soup = BeautifulSoup(html, "lxml")
links = \[a\["href"] for a in soup.select("a.item-link\[href]")]
return links

def parse\_detail(html: str, url: str) -> Item:
soup = BeautifulSoup(html, "lxml")
title = soup.select\_one("h1.item-title").get\_text(strip=True)
price\_el = soup.select\_one(".price .amount")
price = None
if price\_el:
txt = price\_el.get\_text(strip=True).replace(",", "")
price = int(txt) if txt.isdigit() else None
return Item(title=title, price=price, url=url)
from bs4 import BeautifulSoup

def table\_to\_dicts(html: str, selector: str) -> list\[dict]:
soup = BeautifulSoup(html, "lxml")
table = soup.select\_one(selector)
headers = \[th.get\_text(strip=True) for th in table.select("thead th")]
rows = \[]
for tr in table.select("tbody tr"):
cells = \[td.get\_text(strip=True) for td in tr.select("td")]
rows.append(dict(zip(headers, cells)))
return rows

巡回の型:ページネーション→リスト→詳細(2段取り)

まずは直列で実装して正しさを担保。必要になってから並列を検討します(1〜3並列+遅延)。

from urllib.parse import urljoin
import pandas as pd
from scrape.fetch import make_session, get_html
from scrape.parse import parse_list, parse_detail

BASE = "[https://example.com/](https://example.com/)"

def iter\_pages() -> list\[str]:
return \[f"{BASE}list?page={i}" for i in range(1, 4)]

def run() -> pd.DataFrame:
s = make\_session()
items = \[]
for lp in iter\_pages():
html = get\_html(s, lp)
rels = parse\_list(html)
for rel in rels:
url = urljoin(BASE, rel)
detail\_html = get\_html(s, url)
item = parse\_detail(detail\_html, url)
items.append(item.**dict**)
df = pd.DataFrame(items)
return df

if **name** == "**main**":
df = run()
df.to\_csv("data/out/items.csv", index=False, encoding="utf-8")
df.to\_parquet("data/out/items.parquet")

安全な入出力:原子置換で壊さない保存

from pathlib import Path
import tempfile, os

def atomic\_write\_text(path: Path, text: str, encoding: str = "utf-8") -> None:
path.parent.mkdir(parents=True, exist\_ok=True)
with tempfile.NamedTemporaryFile("w", delete=False, dir=path.parent, encoding=encoding, newline="") as tmp:
tmp.write(text)
tmp\_path = Path(tmp.name)
os.replace(tmp\_path, path)

関連記事: [内部リンク:ファイル操作:CSV/JSON/Excelの読み書き]

観測可能性:ログと例外の設計

運用で詰まるのは「何が失敗したかわからない」状態。構造化ログ+相関IDでURL単位に追跡し、429/5xxのみ限定的にリトライ、例外はDataError/ExternalErrorなどに分類して原因を切り分けます。関連記事:[内部リンク:例外処理とログ設計:エラーに強いコードを書く]

テスト:固定HTMLで再現性を担保

from scrape.parse import parse_list, parse_detail

LIST\_HTML = """
  <a class="item-link" href="/d/1">A</a>
  <a class="item-link" href="/d/2">B</a>

"""
DETAIL_HTML = """
  <h1 class="item-title">サンプル商品</h1>
  <div class="price"><span class="amount">1,280</span>円</div>

"""

def test\_parse\_list():
assert parse\_list(LIST\_HTML) == \["/d/1", "/d/2"]

def test\_parse\_detail():
it = parse\_detail(DETAIL\_HTML, "[https://example.com/d/1](https://example.com/d/1)")
assert it.title == "サンプル商品" and it.price == 1280

ユースケース別の第一手

使い始めの指針を3つだけ:

  • 商品一覧→価格表:一覧で詳細URLを抜き、詳細でtitle/price抽出→Parquet保存→[内部リンク:pandas実践]
  • お知らせRSSがないサイトul.news li aからタイトル/日付/URLのみ取得(本文は取らない)
  • 社内ポータルの更新検知:許可と規約を得たログイン不要ページのみを低頻度巡回

今日やること(45分のロードマップ)

  1. 規約/robots.txtを確認(NGなら中止、OKなら進む)
  2. 本記事テンプレでmake_session/get_html/parse_*を作成
  3. 固定HTMLテストtests/test_parse.py)を通す
  4. CSV/Parquet出力→pandasで品質確認

速度・負荷・運用のミニTips

  • If-Modified-Since/ETag:差分だけ取得
  • キャッシュ:学習中はローカル保存で同じURLを再取得しない
  • プロキシ/VPN:組織ポリシー遵守(規約で禁止なら使わない)
  • スケジュールcron/Windowsタスクで夜間にゆっくり回す → [内部リンク:自動化:スケジューリングと業務改善の型]

まとめ:HTMLは最後の手段、やるなら丁寧に

ポイントはこの3つ。(1)合法性(API優先/規約厳守) (2)丁寧なアクセス(遅く/小さく/UA明示/429で停止) (3)再現性(ログ/例外/テスト/原子書き込み)。このテンプレをベースに、小さく始めて安全に育てていきましょう。

伴走のご案内

無料カウンセリング/体験で、あなたのユースケースに合わせて規約確認→実装テンプレ→テスト→運用まで設計をお手伝いします。「やらない判断」も含めて最短で前に進めます。

TechAcademy データサイエンスコース(受講料:174,600円~ ※更に割引あり)

TechAcademy 無料相談

株式会社キカガク AI人材長期育成コース(受講料:237,600円~)

キカガク 無料相談

この記事から次に読むべきもの(内部リンク)

Webスクレイピングの法的リスク
Webスクレイピングの法的リスクと安全運用|“規約→同意→頻度→記録”でトラブルを回避する実務ガイド

「副業や社内でスクレイピングを使いたい。でも、どこまでOKで、何をするとNG?」 結論、“安全運用の型”を最初に決めると迷いません。鍵は規約 → 同意 → 頻度 → 記録。 重要:本記事は一般情報です ...

例外処理
Python実務の型:例外処理と構造化ログでエラーに強いコードを書く

例外処理って、結局どこまでやれば“実務で困らない”の? ログも整えるのって大変そう…最低限の型、ください! この記事は、pythonbunseki.comの実務トーンで「防ぐ→気づく→復旧する」をコー ...

事故防止・効率化
もう事故らせない:PythonでCSV/JSON/Excelを安全に読み書きする実務レシピ

CSV/JSON/Excelの読み書き、どこから気をつければいい? 文字化け・先頭ゼロ欠落・壊れたExcel……もう事故らせたくない! 結論:データ仕事の9割はI/O(入出力)。ここを整えるだけで、桁 ...

スキルアップ
【実務で差がつく】pandas実践:欠損処理・結合・ウィンドウ関数・時系列・品質保証まで“読みやすく速い”型を習得

リード(結論)基礎を終えたら次は実務の現場で頻出する処理を“型”で覚える段階です。本記事は、pandas 2.x を前提に、欠損・外れ値・結合・ウィンドウ関数・時系列・カテゴリ処理・集計の自動化・大規 ...

自動化
自動化:スケジューリングと業務改善の型|「再実行安全×観測可能×静かに動く」を仕組みにする

夜中に動かしているPython、自動で止まってた…ログもなくて原因が追えない…。 「毎朝のレポート」や「在庫監視」を、壊れず静かに回したい…! 業務で落ちない自動化を作る鍵は、(1) 再実行安全(Id ...

最近のコメント

    • この記事を書いた人
    • 最新記事

    ふみと

    このブログでは、データサイエンティストとして市場価値を上げる方法を独自にまとめて発信しています。

    【プロフィール】
    ・大手企業データサイエンティスト/マーケティングサイエンティスト(10年、年収900万円台)/案件100件以上
    ・資格:JDLA E資格(日本ディープラーニング協会主催)/JDLA Community(CDLE会員)/Advanced Marketer/ビジネス統計スペシャリスト/統計検定2級/TOEIC 805
    ・スキル:Python/Tableau/SQL/機械学習/Deep Learning/RPA

    -Python基礎