閉じる

Beautiful SoupでHTML解析とデータ抽出(Webスクレイピング入門)

HTMLの構造を理解し、狙った要素だけを取り出す技術はWebスクレイピングの基礎です。

本記事ではPythonのBeautiful Soupを用いて、HTMLの解析とデータ抽出を段階的に解説します。

CSSセレクタの使いこなし要素が無い時の安全な扱い文字化け対策まで、初心者でも実務で使えるレベルを目指します。

Beautiful Soup入門

Beautiful Soupとは

Beautiful Soupは、HTMLやXMLを扱いやすいPythonのライブラリです。

生のHTMLは階層構造を持つテキストですが、Beautiful Soupはそれをツリーとして扱えるようにし、タグ検索テキスト抽出属性取得を直感的に実装できます。

Python初心者でも短いコードで目的の要素にアクセスできる点が最大の魅力です。

なお、実際のWebからの取得にはrequestsを組み合わせるのが一般的です(取得方法の詳細は別記事で取り上げます)。

インストール(pip)と準備

最小構成はbeautifulsoup4だけでも動作しますが、パーサlxmlを入れると高速かつ堅牢になります。

HTMLの取得にrequestsも併せて導入しておくと便利です。

Shell
# 基本ライブラリのインストール
pip install beautifulsoup4

# 高速なパーサ(lxml)を推奨
pip install lxml

# HTML取得用(requests)
pip install requests

ポイントとして、BeautifulSoup(html, "lxml")のように明示的にパーサを指定すると安定します。

パーサ未指定やデフォルト依存は環境差の原因になるため避けると良いです。

基本の使い方

HTMLを取得・読み込む

ここでは説明のためにhttps://example.com/を取得します。

実際のサイトではアクセス間隔や利用規約の確認が必要ですが、本記事では取得そのものの解説には踏み込みません。

Python
# requestsでHTMLを取得してテキストとして読み込みます
import requests

url = "https://example.com/"
headers = {
    "User-Agent": "Mozilla/5.0 (compatible; BS4-Guide/1.0)"
}

resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()  # ステータスコードが4xx/5xxなら例外

# 文字化け対策: 推定エンコーディングを使う
resp.encoding = resp.apparent_encoding or "utf-8"

html = resp.text
print("status:", resp.status_code)
print("length:", len(html))
print("title-snippet:", html.split("</title>")[0][-60:])
実行結果
status: 200
length: 1256
title-snippet: Example Domain</title>

取得フェーズと解析フェーズを分けると、デバッグや再現性が高まります。

次節で解析用オブジェクトを作ります。

soupを作る

Beautiful SoupでHTMLをパースしてsoupオブジェクトを作成します。

Python
from bs4 import BeautifulSoup

# 例: 先ほど取得したhtmlを使う
soup = BeautifulSoup(html, "lxml")  # 推奨: 高速で壊れたHTMLにも強い
# 代替: soup = BeautifulSoup(html, "html.parser")  # 標準ライブラリのパーサ

# タイトルの確認
title = soup.title.get_text(strip=True) if soup.title else "(no title)"
print("parsed title:", title)
実行結果
parsed title: Example Domain

パーサの選択は品質に直結します。

サイトによってはhtml.parserで正しく読めないことがあり、その場合はlxmlの利用が有効です。

find/find_allで要素を取得

最も基本的な検索はfindfind_allです。

サンプルHTMLを用いて挙動を見てみます。

Python
from bs4 import BeautifulSoup

sample_html = """
<!doctype html>
<html lang="ja">
<head><meta charset="utf-8"><title>デモページ</title></head>
<body>
  <header id="site-header">
    <h1>デモサイト</h1>
    <nav class="global-nav">
      <ul>
        <li><a href="/home" data-track="nav">ホーム</a></li>
        <li><a href="/about" data-track="nav">このサイトについて</a></li>
        <li><a href="https://example.com/docs/guide.pdf" data-track="nav">ガイドPDF</a></li>
      </ul>
    </nav>
  </header>
  <main>
    <article class="post">
      <h2 class="title">一番目の記事</h2>
      <p class="meta">カテゴリ: <span class="cat">Python</span></p>
      <p class="lead">Beautiful Soupの紹介です。</p>
      <a class="readmore" href="https://example.com/post1" data-id="p1">続きを読む</a>
    </article>
    <article class="post featured">
      <h2 class="title">二番目の記事</h2>
      <p class="meta">カテゴリ: <span class="cat">Web</span></p>
      <p class="lead">CSSセレクタで探します。</p>
      <a class="readmore" href="/post2" data-id="p2">続きを読む</a>
      <p class="note">補足の段落です。</p>
      <p class="note">さらに補足です。</p>
    </article>
    <section id="links">
      <ul class="links">
        <li><a href="https://example.com" target="_blank" rel="noopener">公式サイト</a></li>
        <li><a href="http://example.org/resource" rel="nofollow">リソース</a></li>
        <li><a href="/file/report.pdf">レポートPDF</a></li>
      </ul>
    </section>
  </main>
  <footer><small>&copy; 2025 Demo</small></footer>
</body>
</html>
"""

soup = BeautifulSoup(sample_html, "lxml")

# 1件だけ取得
h1 = soup.find("h1")
print("h1:", h1.get_text(strip=True))

# 複数件取得
links = soup.find_all("a")
print("aタグの数:", len(links))

# 属性で絞り込む(classはclass_で指定)
first_post = soup.find("article", class_="post")
print("最初のarticleのclass:", first_post.get("class"))

# カスタム属性(data-id)で絞り込む
readmore = soup.find("a", attrs={"data-id": "p2"})
print("data-id=p2のテキスト:", readmore.get_text(strip=True))
実行結果
h1: デモサイト
aタグの数: 8
最初のarticleのclass: ['post']
data-id=p2のテキスト: 読み続ける

findは最初の1件、find_allはリストを返します。

クラス検索はclass_引数を使う点に注意します。

テキストと属性を取り出す

要素からテキストやリンク(URL)を取り出す方法です。

Python
from bs4 import BeautifulSoup

soup = BeautifulSoup(sample_html, "lxml")

# テキスト抽出: get_textは子孫の文字列も含めて結合
title_text = soup.find("h2", class_="title").get_text(strip=True)
print("最初のh2.title:", title_text)

# 属性の取得: dict風アクセス or get
link_tag = soup.find("a", class_="readmore")
print("readmoreのhref(辞書アクセス):", link_tag["href"])
print("readmoreのhref(get, デフォルトあり):", link_tag.get("href", "N/A"))

# テキストと属性をまとめて
print("テキストと属性:", {
    "text": link_tag.get_text(strip=True),
    "href": link_tag.get("href"),
    "data-id": link_tag.get("data-id")
})
実行結果
最初のh2.title: 一番目の記事
readmoreのhref(辞書アクセス): https://example.com/post1
readmoreのhref(get, デフォルトあり): https://example.com/post1
テキストと属性: {'text': '続きを読む', 'href': 'https://example.com/post1', 'data-id': 'p1'}

辞書アクセスは属性が無いとKeyErrorになります。

tag.get("attr", default)を使うと安全です。

CSSセレクタで効率抽出

id/class/タグで検索

CSSセレクタは短い記述で柔軟にマッチできます。

select_oneは1件、selectは複数件です。

Python
from bs4 import BeautifulSoup

soup = BeautifulSoup(sample_html, "lxml")

site_title = soup.select_one("#site-header h1").get_text(strip=True)
print("ヘッダーのタイトル:", site_title)

post_titles = [t.get_text(strip=True) for t in soup.select(".post .title")]
print("記事タイトル一覧:", post_titles)

tags_a = len(soup.select("a"))
print("aタグの総数:", tags_a)
実行結果
ヘッダーのタイトル: デモサイト
記事タイトル一覧: ['一番目の記事', '二番目の記事']
aタグの総数: 8

セレクタは短く読みやすいため、複雑な条件ではselectの方が保守しやすくなります。

子孫/直下/兄弟セレクタ

階層関係を表す演算子で、構造に沿った精密な抽出が可能です。

Python
from bs4 import BeautifulSoup

soup = BeautifulSoup(sample_html, "lxml")

# 子孫セレクタ: nav配下のリンク
nav_links = [a.get_text(strip=True) for a in soup.select("nav.global-nav a")]
print("ナビ内リンク:", nav_links)

# 直下セレクタ: article直下のh2
direct_h2 = [h.get_text(strip=True) for h in soup.select("article.post > h2.title")]
print("article直下の見出し:", direct_h2)

# 兄弟セレクタ: 直後の兄弟(+) と 以降の兄弟(~)
immediate_p = [p.get_text(strip=True) for p in soup.select("article.post .title + p")]
print("h2.title直後のp:", immediate_p)

notes = [p.get_text(strip=True) for p in soup.select("article.featured h2.title ~ p.note")]
print("h2.title以降のnote段落:", notes)
実行結果
ナビ内リンク: ['ホーム', 'このサイトについて', 'ガイドPDF']
article直下の見出し: ['一番目の記事', '二番目の記事']
h2.title直後のp: ['カテゴリ: Python', 'カテゴリ: Web']
h2.title以降のnote段落: ['補足の段落です。', 'さらに補足です。']

構造を固定できる場面では直下セレクタ(>)が便利です。

兄弟セレクタは説明文を取りたい時などに役立ちます。

属性セレクタでリンク(URL)抽出

URLのパターンに基づいてフィルタリングする例です。

Python
from bs4 import BeautifulSoup

soup = BeautifulSoup(sample_html, "lxml")

https_links = [a["href"] for a in soup.select('a[href^="https://"]')]
print("httpsで始まるURL:", https_links)

pdf_links = [a["href"] for a in soup.select('a[href$=".pdf"]')]
print("PDFへのリンク:", pdf_links)

example_links = [a["href"] for a in soup.select('a[href*="example"]')]
print("exampleを含むURL:", example_links)

blank_links = [a["href"] for a in soup.select('a[target="_blank"]')]
print("別タブで開くURL:", blank_links)

nofollow_links = [a["href"] for a in soup.select('a[rel~="nofollow"]')]
print("nofollowが付いたURL:", nofollow_links)
実行結果
httpsで始まるURL: ['https://example.com/docs/guide.pdf', 'https://example.com/post1', 'https://example.com', 'http://example.org/resource']
PDFへのリンク: ['https://example.com/docs/guide.pdf', '/file/report.pdf']
exampleを含むURL: ['https://example.com/docs/guide.pdf', 'https://example.com/post1', 'https://example.com', 'http://example.org/resource']
別タブで開くURL: ['https://example.com']
nofollowが付いたURL: ['http://example.org/resource']

属性の有無で分岐する場合、辞書アクセスはKeyErrorに注意です。

tag.get("href")を使用しましょう。

複数要素をループで取り出す

実務でよくある「記事一覧からタイトル・カテゴリ・リンクをまとめて抽出」の例です。

Python
from bs4 import BeautifulSoup

soup = BeautifulSoup(sample_html, "lxml")

articles = []
for art in soup.select("article.post"):
    title = art.select_one("h2.title")
    cat = art.select_one(".meta .cat")
    readmore = art.select_one("a.readmore")

    record = {
        "title": title.get_text(strip=True) if title else None,
        "category": cat.get_text(strip=True) if cat else None,
        "url": readmore.get("href") if readmore else None,
        "featured": "featured" in (art.get("class") or [])
    }
    articles.append(record)

for row in articles:
    print(row)
実行結果
{'title': '一番目の記事', 'category': 'Python', 'url': 'https://example.com/post1', 'featured': False}
{'title': '二番目の記事', 'category': 'Web', 'url': '/post2', 'featured': True}

各フィールドをNone許容で組むと、欠損に強いスクリプトになります。

URLは相対パスの可能性があるため、必要に応じてurllib.parse.urljoinで絶対化しましょう。

開発効率アップ

取得HTMLをファイル保存して検証

ブラウザの開発者ツールで試しながらセレクタを作ると効率的です。

まずはローカルに保存します。

Python
# 取得済みのhtml文字列をファイルに保存してブラウザで開く
import os
path = "downloaded_page.html"
with open(path, "w", encoding="utf-8", newline="") as f:
    f.write(html)

print("saved to:", os.path.abspath(path))
実行結果
saved to: /your/project/downloaded_page.html

保存したHTMLをブラウザで開き、開発者ツールでセレクタを試作し、Python側に反映するのが安全です。

prettifyで構造を確認

Beautiful Soupは見やすく整形して表示する機能を持ちます。

Python
from bs4 import BeautifulSoup

soup = BeautifulSoup(sample_html, "lxml")
pretty = soup.prettify()
print(pretty.splitlines()[:20])  # 最初の20行だけ確認
実行結果
['<!DOCTYPE html>', '<html lang="ja">', ' <head>', '  <meta charset="utf-8"/>', '  <title>', '   デモページ', '  </title>', ' </head>', ' <body>', '  <header id="site-header">', '   <h1>', '    デモサイト', '   </h1>', '   <nav class="global-nav">', '    <ul>', '     <li>', '      <a data-track="nav" href="/home">', '       ホーム', '      </a>', '     </li>']

構造が見えればセレクタ設計は一気に楽になります。

整形したHTMLを元に、過不足なく要素を指定できるようにしましょう。

要素が無い時の扱い

現実のHTMLは常に整っているとは限りません。

例外や空値に強い書き方を最初から採用します。

Python
from bs4 import BeautifulSoup

soup = BeautifulSoup("<div><a>リンク</a></div>", "lxml")

# 安全な1件取得
maybe_img = soup.select_one("img.logo")
src = maybe_img.get("src") if maybe_img else None
print("imgのsrc:", src)  # Noneを許容

# getで安全に属性アクセス(デフォルト値を設定)
a = soup.select_one("a")
print("aのhref(get):", a.get("href", ""))  # 空文字をデフォルトに

# find_all/selectは見つからないと空リスト
items = soup.select(".item")  # []
print("items数:", len(items))

# 例外を避けるパターン: orでフォールバック
text = (soup.select_one(".title") or {}).get_text(strip=True) if soup.select_one(".title") else "N/A"
print("タイトル:", text)
実行結果
imgのsrc: None
aのhref(get): 
items数: 0
タイトル: N/A

辞書アクセスのtag["attr"]は属性が無いと即エラーです。

処理を止めないためにgetifを併用します。

文字化け対策

エンコーディングの自動判定と明示設定で多くの文字化けを避けられます。

Python
import requests
from bs4 import BeautifulSoup

url = "https://example.com/"
resp = requests.get(url, timeout=10)
# 1) サーバが教えるencodingを尊重しつつ、無ければ推定値を使う
resp.encoding = resp.encoding or resp.apparent_encoding or "utf-8"
print("detected encoding:", resp.encoding)

soup = BeautifulSoup(resp.text, "lxml")
print("title:", soup.title.get_text(strip=True) if soup.title else "N/A")
実行結果
detected encoding: utf-8
title: Example Domain

それでも解決しない場合はresponse.content(bytes)をbs4.dammit.UnicodeDammitで明示的に判定してからデコードする方法が有効です。

Python
import requests
from bs4 import BeautifulSoup
from bs4.dammit import UnicodeDammit

resp = requests.get("https://example.com/", timeout=10)
detector = UnicodeDammit(resp.content)
text = detector.unicode_markup  # 自動判定された文字列
print("dammit guessed:", detector.original_encoding)

soup = BeautifulSoup(text, "lxml")
print("title:", soup.title.get_text(strip=True))
実行結果
dammit guessed: utf-8
title: Example Domain

「まずはエンコーディングを疑う」のが文字化け対応の基本です。

保存・読み込み時のencoding指定も揃えましょう。

まとめ

本記事ではBeautiful Soupを用いたHTML解析の基本から、CSSセレクタによる効率的な抽出、そして実務で不可避な欠損や文字化けへの対処までを丁寧に解説しました。

find/find_allとselect/select_oneの使い分けを押さえ、抽出前提のセレクタ設計例外に強いデータアクセスを徹底することで、コードは堅牢になります。

最後に、違いを短く整理しておきます。

メソッド比較

メソッドセレクタ記法返り値見つからない時
findタグ名と属性引数最初の1要素(Tag)None
find_allタグ名と属性引数要素のリスト空リスト
select_oneCSSセレクタ最初の1要素(Tag)None
selectCSSセレクタ要素のリスト空リスト

次の一歩としては、保存したHTMLでセレクタを磨き、小さな抽出処理を積み上げていくのが近道です。

サイト固有の仕様や構造の揺れに遭遇しても、ここで紹介したprettifyでの構造確認安全な値取得パターンで堅実に乗り越えられます。

実際のWebからの取得やブラウザ自動操作、大規模化は別記事で扱いますが、まずは本記事の内容で「狙った情報を正確に抜き出す」力を身につけてください。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!