Webページから必要な情報だけを効率よく抜き出すには、CSSセレクタを使った指定が非常に便利です。
PythonのBeautiful SoupはCSSセレクタを標準でサポートしており、タグ・id・class・属性・位置などを柔軟に組み合わせて目的の要素を的確に取得できます。
この記事では、Python初心者の方に向けて、Beautiful SoupでCSSセレクタを使って要素を取得する方法を、基本から実践まで丁寧に解説します。
Python+Beautiful SoupでCSSセレクタの基本
selectとselect_oneの違い
Beautiful Soupでは、soup.select()
とsoup.select_one()
の2つのAPIでCSSセレクタを使えます。
前者は条件に合う要素のリストを返し、後者は最初の1件だけを返すため、用途に応じて使い分けます。
以下のサンプルでは、共通のデモHTMLからナビゲーションのリンクを取得します。
すべての例が単独で動くよう、先頭にmake_soup()
を定義して同一のHTMLを用意しています。
# 標準ライブラリ以外は bs4 を使用します
from bs4 import BeautifulSoup
def make_soup():
"""学習用のサンプルHTMLからSoupを作る関数"""
html = """
<html>
<head><title>Demo</title></head>
<body>
<div id="main">
<h1 class="title">Python 入門</h1>
<p class="lead">はじめてのWebスクレイピング</p>
<ul class="menu">
<li><a href="/home" class="nav">Home</a></li>
<li><a href="/about" class="nav external" target="_blank" data-track="nav-about">About</a></li>
<li><a href="/contact" class="nav">Contact</a></li>
</ul>
<div class="content">
<article class="post featured" data-id="p1">
<h2>Beautiful Soup 基本</h2>
<p>基本的な使い方を学びます。</p>
</article>
<article class="post" data-id="p2">
<h2>CSS セレクタ 応用</h2>
<p>セレクタで高度に絞り込みます。</p>
</article>
<p class="note">注意A</p>
<p class="note detail">注意Aの詳細</p>
<p class="note">注意B</p>
<section id="links">
<h3>外部リンク</h3>
<ul>
<li><a href="https://example.com" rel="nofollow">Example</a></li>
<li><a href="https://docs.python.org/3/" rel="external">Python Docs</a></li>
<li><a href="https://pypi.org/project/beautifulsoup4/" rel="external">bs4</a></li>
</ul>
</section>
<table id="ranking">
<thead>
<tr><th>Rank</th><th>Name</th><th>Score</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>Alice</td><td>95</td></tr>
<tr><td>2</td><td>Bob</td><td>89</td></tr>
<tr><td>3</td><td>Carol</td><td>82</td></tr>
</tbody>
</table>
</div>
<div class="footer"><p>©2025 Example</p></div>
</div>
</body>
</html>
"""
return BeautifulSoup(html, "html.parser")
# --- ここから「select」と「select_one」の例 ---
soup = make_soup()
# 複数件取得: <ul class="menu"> 内の .nav クラスを持つリンク
links = soup.select("ul.menu a.nav")
print("件数:", len(links))
print("テキスト一覧:", [a.get_text(strip=True) for a in links])
# 1件だけ取得: 最初の .nav リンク
first = soup.select_one("ul.menu a.nav")
print("最初のリンク:", first.get_text(strip=True))
件数: 3
テキスト一覧: ['Home', 'About', 'Contact']
最初のリンク: Home
繰り返しますが、select()
はリスト、select_one()
は要素かNoneです。
1件だけ欲しいときはselect_one()
のほうが楽で、存在チェックもしやすくなります。
最小ステップで要素を取得
本番サイトから取得する最小手順は以下の流れです。
requestsでHTMLを取得して、Beautiful Soupで解析し、CSSセレクタで抜き出すという順番です。
# pip install requests beautifulsoup4
import requests
from bs4 import BeautifulSoup
url = "https://example.com/"
res = requests.get(url, timeout=10)
res.raise_for_status() # 失敗時は例外を出す
soup = BeautifulSoup(res.text, "html.parser")
# 例: ページの見出しを1件取得
title = soup.select_one("h1")
print(title.get_text(strip=True) if title else "見出しが見つかりませんでした")
Example Domain
最小構成はrequests.get()
→BeautifulSoup()
→select_one()
です。
まずはこの流れを確実に押さえましょう。
CSSセレクタの書き方
タグ名/#id/.class
CSSセレクタは、タグ名、id、classを指定するのが基本です。
タグ名はそのまま、idは#main
のように#
、classは.post
のように.
で示します。
soup = make_soup()
# タグ名で取得
h1_list = soup.select("h1")
# idで取得
main_div = soup.select_one("#main")
# classで取得
posts = soup.select(".post")
print("h1件数:", len(h1_list))
print("mainの有無:", main_div is not None)
print("記事件数(.post):", len(posts))
h1件数: 1
mainの有無: True
記事件数(.post): 2
よく使う基本記法の対応表
セレクタ | 意味 | 例 |
---|---|---|
div | divタグを選ぶ | div |
#id | id属性が一致 | #main |
.class | classを含む | .post |
tag.class | タグとかつclass | article.post |
[attr="v"] | 属性が一致 | a[target="_blank"] |
子孫/子を指定
空白は子孫
、>
は直接の子
を意味します。
マッチ範囲が大きく変わるので使い分けが重要です。
soup = make_soup()
# 子孫セレクタ: #main のどこかにある article
desc = soup.select("#main article")
# 子セレクタ: #main 直下の .content 直下の article
child = soup.select("#main > .content > article")
print("子孫(#main article):", len(desc))
print("子(#main > .content > article):", len(child))
子孫(#main article): 2
子(#main > .content > article): 2
兄弟を指定
隣接兄弟+
と後続兄弟~
で、同じ親の中での位置関係を指定できます。
soup = make_soup()
# .note の直後に続く段落を取得
adjacent = soup.select("p.note + p")
# .note の後に続く .note をすべて取得
following_notes = soup.select("p.note ~ p.note")
print("直後のp:", [p.get_text(strip=True) for p in adjacent])
print("後続の.note:", [p.get_text(strip=True) for p in following_notes])
直後のp: ['注意Aの詳細']
後続の.note: ['注意B']
複数条件の組み合わせ
複数の条件を重ねるとさらに絞り込めます。
クラスの積み重ね、属性の同時指定、カンマでの複数選択などが可能です。
soup = make_soup()
# クラスを複数指定し、かつ data-id 属性を持つ記事
featured = soup.select("article.post.featured[data-id]")
# 外部ナビリンク: nav かつ external の両方を持ち、target="_blank"
ext_nav = soup.select('ul.menu a.nav.external[target="_blank"]')
# 複数選択(,): .lead 段落と .footer 内の p をまとめて
multi = soup.select(".lead, .footer p")
print("注目記事:", [a.get_text(strip=True) for a in featured])
print("外部ナビ:", [a.get_text(strip=True) for a in ext_nav])
print("まとめて取得:", [x.get_text(strip=True) for x in multi])
注目記事: ['Beautiful Soup 基本']
外部ナビ: ['About']
まとめて取得: ['はじめてのWebスクレイピング', '©2025 Example']
よく使うセレクタ例
属性で絞る
属性の有無や値で絞り込みます。
[attr]
は存在、[attr="value"]
は一致です。
soup = make_soup()
has_data_id = soup.select("article[data-id]")
blank_target = soup.select('a[target="_blank"]')
print("data-idあり:", [a['data-id'] for a in has_data_id])
print('target="_blank":', [a.get_text(strip=True) for a in blank_target])
data-idあり: ['p1', 'p2']
target="_blank": ['About']
部分一致で絞る
URLなどでよく使うテクニックです。
^=
は前方一致$=
は後方一致*=
は部分一致~=
は空白区切りの単語として一致(class向け)
soup = make_soup()
https_links = soup.select('#links a[href^="https://"]')
trailing_slash = soup.select('#links a[href$="/"]')
has_python = soup.select('#links a[href*="python"]')
external_class = soup.select('a[class~="external"]') # classにexternalを含む
print("httpsリンク:", [a.get_text(strip=True) for a in https_links])
print("末尾/ で終わる:", [a.get_text(strip=True) for a in trailing_slash])
print("hrefにpython含む:", [a.get_text(strip=True) for a in has_python])
print("class=... external ...:", [a.get_text(strip=True) for a in external_class])
httpsリンク: ['Example', 'Python Docs', 'bs4']
末尾/ で終わる: ['Python Docs']
hrefにpython含む: ['Python Docs']
class=... external ...: ['About']
n番目を取る
:nth-child(n)
は兄弟の中のn番目、:nth-of-type(n)
は同じタグの中のn番目です。
この違いは重要です。
soup = make_soup()
second_menu = soup.select_one("ul.menu li:nth-child(2) a")
second_article_by_type = soup.select_one(".content > article:nth-of-type(2) h2")
print("メニュー2番目:", second_menu.get_text(strip=True))
print("articleの2番目の見出し:", second_article_by_type.get_text(strip=True))
メニュー2番目: About
articleの2番目の見出し: CSS セレクタ 応用
先頭/末尾を取る
:first-child
や:last-child
、あるいは:nth-last-child(1)
が使えます。
soup = make_soup()
first_menu = soup.select_one("ul.menu li:first-child a")
last_menu = soup.select_one("ul.menu li:last-child a")
print("先頭:", first_menu.get_text(strip=True))
print("末尾:", last_menu.get_text(strip=True))
先頭: Home
末尾: Contact
リンク一覧からテキストとURLを取得
リンクのリストを走査し、.get_text()
と['href']
で取り出します。
soup = make_soup()
for a in soup.select("#links a[href]"):
text = a.get_text(strip=True)
url = a["href"]
print(f"{text} -> {url}")
Example -> https://example.com
Python Docs -> https://docs.python.org/3/
bs4 -> https://pypi.org/project/beautifulsoup4/
表の行や列を取得
テーブルはtr
やtd
を組み合わせます。
列だけ取りたいときは:nth-child()
が有効です。
soup = make_soup()
# 全行
rows = soup.select("table#ranking tbody tr")
for r in rows:
cols = [c.get_text(strip=True) for c in r.select("td")]
print(cols)
# 2列目(Name列)だけ
names = [td.get_text(strip=True) for td in soup.select("table#ranking tbody tr td:nth-child(2)")]
print("Names:", names)
['1', 'Alice', '95']
['2', 'Bob', '89']
['3', 'Carol', '82']
Names: ['Alice', 'Bob', 'Carol']
取得後の扱いと注意点
テキストと属性を取り出す
get_text(strip=True)
は余分な空白や改行を詰めてくれます。
属性はタグ['属性名']
かタグ.get('属性名', デフォルト)
で参照できます。
soup = make_soup()
a = soup.select_one('ul.menu a.nav.external')
print("テキスト:", a.get_text(strip=True))
# hrefは存在する前提
print("href:", a["href"])
# 存在しない属性に安全にアクセス(getでデフォルト指定)
print("data-unknown:", a.get("data-unknown", "N/A"))
テキスト: About
href: /about
data-unknown: N/A
テキストにはget_text()
、属性には['attr']
か.get()
を使うと覚えておくと迷いません。
要素が無い時の対策
select()
は空リスト、select_one()
はNone
を返すことがあります。
存在しない要素にアクセスするとエラーなので、必ずチェックしましょう。
soup = make_soup()
maybe = soup.select_one("#not-exist")
if maybe is None:
print("要素が見つかりませんでした")
else:
print(maybe.get_text(strip=True))
要素が見つかりませんでした
属性アクセスもKeyErrorに注意です。
tag.get("href")
にしておけば安全です。
CSSセレクタとfind系の使い分け
CSSセレクタは構造や属性での絞り込みが得意です。
一方、find()
/find_all()
はname
やattrs
に加え、text
へ正規表現を使うなど柔軟な条件を記述できます。
テキスト内容の複雑な条件や関数ベースの条件はfind系が便利です。
import re
soup = make_soup()
# CSS: 文字列に「Python」を含むリンクを直感的に取るには :contains() も使える環境がありますが、
# SoupSieveのバージョン次第なので、確実さを優先するなら find で正規表現を使うのが安全です。
a = soup.find("a", string=re.compile(r"Python", re.I))
print("正規表現でテキストマッチ:", a.get_text(strip=True), "->", a["href"])
# 属性や構造で十分に表せるなら select でシンプルに
docs = soup.select_one('#links a[href*="python"]')
print("CSSで属性部分一致:", docs.get_text(strip=True), "->", docs["href"])
正規表現でテキストマッチ: Python Docs -> https://docs.python.org/3/
CSSで属性部分一致: Python Docs -> https://docs.python.org/3/
目安として、構造・属性で表せるならCSS、テキストパターンなど複雑条件はfind系という使い分けがおすすめです。
セレクタが効かない時の確認
思った通りに取れない場合は次の点を順に確認します。
HTMLの実体をsoup.prettify()
で確認し、想定の構造かを目視するようにしましょう。
JavaScriptで後から生成される要素はrequestsでは取得できません。その場合はSeleniumをご検討ください。
空白(子孫)と>
(子)の違いを再確認する。わずかな違いで0件になります。
idやclassの特殊文字に注意。例えば#some.id
はドットを含むため#some.id
のようにエスケープするか、[id="some.id"]
と属性指定に切り替えます。
classの完全一致ではなく包含です。.external
はclass配列にexternalを含む要素にマッチします。
バージョンの確認。Beautiful Soup 4.7以降はSoupSieveによりCSS4相当の多くに対応します。pip show beautifulsoup4
でバージョンを確認しましょう。
パーサーを変更。html.parser
でうまくいかない場合、lxml
を導入してBeautifulSoup(html, "lxml")
にすると改善することがあります。
一気に複雑なセレクタを書くのではなく、階層を一段ずつ狭めながら試すと原因切り分けが楽になります。
まとめ
Beautiful SoupのCSSセレクタは、構造や属性を駆使して目的の要素を素早く正確に取り出すための強力な手段です。
基本はtag
、#id
、.class
で、子孫
や子>
、兄弟+
/~
、属性一致[attr]
、部分一致^=
/$=
/*=
、位置:nth-child()
を組み合わせれば、たいていのケースに対応できます。
要素取得後はテキストget_text()
と属性.get()
を安全に扱い、見つからない場合の分岐も忘れないようにしてください。
うまくいかないときはHTMLの実体とセレクタの粒度を見直し、段階的に検証するのが近道です。
まずは本記事のサンプルをそのまま動かし、必要箇所を自分のページの構造に合わせて置き換えてみてください。