logo
JP

Webサイトの表を取得したい【Python, Selenium, Pandas】対象要素を正しく選択して取得するには?

PythonでWebサイトの表を取得する方法を解説。pandasのread_htmlで取れない原因(JavaScript描画、iframe、div構造、複数の表、多段ヘッダー、pandas 3のStringIO)ごとの対処と、Seleniumで要素を正しく選ぶ方法、複数サイトの表を1つのCSVにまとめるコードを紹介します。

約16分で読めます

Webサイトの表は、HTMLの<table>がそのまま読み込まれるページなら、pandasのread_htmlで取得できます。取得できない場合の原因は、表がJavaScriptで後から描画される、iframeの中にある、<table>ではなく<div>で組まれている、ページ内に表が複数ある、のいずれかであることが多く、原因ごとに対処が決まっています。JavaScriptやiframeが原因なら、Seleniumでページを開き、表が表示されるまで待ってからHTMLをpandasに渡します。複数サイトの表は、URLの一覧をループで処理し、encoding="utf-8-sig"でCSVにまとめるとExcelでもそのまま開けます。

本記事では、表が取れない原因の見分け方と対処、対象の表や要素を正しく選ぶ方法、複数サイトの表を1つのCSVにまとめるコードまでを解説します。 コードはすべてpandas 3.0、Selenium 4.51で動作を確認しています。

症状主な原因対処
ValueError: No tables found表がJavaScriptで後から描画されるSeleniumで表示を待ってからpandasに渡す
ValueError: No tables found表が<div>で組まれているSeleniumのfind_elementsで行ごとに取得する
NoSuchElementException表がiframeの中にあるswitch_to.frameで切り替えてから探す
取得できたが別の表が入っているページ内に表が複数あるmatchやattrsで対象の表を指定する
列名が「日/日/日」のように重なるヘッダーが多段になっている列名を1段にまとめる
FileNotFoundErrorpandas 3でHTML文字列をそのまま渡しているStringIOで包んでから渡す

まずread_htmlで表が取れるか確認する

最初に試すべきなのは、pandasのread_htmlです。URLを渡すだけで、ページ内のすべての<table>がDataFrameのリストとして返ります。必要なライブラリは次のとおりです。

pip install pandas lxml selenium

気象庁「過去の気象データ」の東京・2026年9月の日別値のページで試すと、次のようになります。

python

import pandas as pd

url = ("https://www.data.jma.go.jp/stats/etrn/view/daily_s1.php"
       "?prec_no=44&block_no=47662&year=2026&month=9&day=&view=")

dfs = pd.read_html(url)
print(len(dfs))  # 2
for i, df in enumerate(dfs):
    print(i, df.shape)
# 0 (30, 21)  ← 日別のデータ表
# 1 (1, 4)    ← ページ内の別の表

返ってくる表の数と順番はページの作りで決まり、データの表が先頭にあるとは限りません。ページのデザインが変わると順番も変わるため、dfs[0]のように番号で選ぶのではなく、次の方法で対象の表を指定します。

matchで表の中の文字を指定する

matchには、目的の表にだけ含まれる文字列を渡します。pandasの公式ドキュメントでも、単純なページ以外ではmatchを指定することが推奨されています。

python

df = pd.read_html(url, match="降水量", na_values=["--"])[0]
print(df.shape)  # (30, 21)

na_values=["--"]は、データのない欄に入っている「–」を欠損値として読み込む指定です。指定しないと、その列全体が数値ではなく文字列として扱われ、集計や並べ替えができません。

attrsで表のidやclassを指定する

表にidやclassが付いている場合は、attrsで直接指定できます。ブラウザーで表の中の値を右クリックして「検証」を選ぶと、開発者ツールが開きます。選択された<td>から上の階層をたどり、<table>タグの行をクリックすると、ページ上で表全体がハイライトされ、属性を確認できます。

気象庁の日別値の表には、id="tablefix1"とclass="data2_s"が付いています。

python

df = pd.read_html(url, attrs={"id": "tablefix1"}, na_values=["--"])[0]

なお、read_htmlは「1,200」のような桁区切りのカンマを自動で取り除き、数値として読み込みます。

*出典:気象庁ホームページ

表が取得できない・使えない6つの原因と対処

1.表がJavaScriptで後から描画されている

read_htmlはサーバーから返ってきたHTMLだけを読みます。ページを開いた後にJavaScriptで表を描画するサイトでは、その時点のHTMLに表がないためValueError: No tables foundになります。Seleniumでページを開いた直後にpage_sourceを渡しても、描画が終わる前なら同じエラーです。

表が表示されるまでWebDriverWaitで待ち、そのあとでHTMLをpandasに渡します。

python

from io import StringIO
import pandas as pd
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options)

driver.get("https://example.com/js-table")
WebDriverWait(driver, 10).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "table"))
)
df = pd.read_html(StringIO(driver.page_source), match="価格")[0]
driver.quit()

time.sleepで固定の秒数を待つ方法は、待ち時間が足りなければ失敗し、長すぎれば処理が遅くなります。Seleniumの公式ドキュメントでも、条件を指定して待つ明示的な待機が推奨されています。待機の考え方とSeleniumの基本操作は、PythonでSeleniumを使ってスクレイピングする方法で詳しく解説しています。

2.表がiframeの中にある

iframeは、ページの中に別のページを埋め込む仕組みです。Seleniumは最初、外側のページの要素しか探せないため、iframe内の要素を探すとNoSuchElementExceptionになります。

Seleniumの公式ドキュメントにあるとおり、switch_to.frameでiframeに切り替えてから探します。

python

WebDriverWait(driver, 10).until(
    EC.frame_to_be_available_and_switch_to_it((By.ID, "rateframe"))
)
df = pd.read_html(StringIO(driver.page_source))[0]

driver.switch_to.default_content()  # 外側のページに戻る

iframeの中にあるかどうかは、開発者ツールで確認できます。目的の要素を選択したとき、上の階層に<iframe>タグと#documentがあれば、iframeの中です。画面下部のパンくずリストにhtmlとbodyが2回出てくることでも見分けられます。

*出典:Selenium公式のテスト用ページ

3.表が<table>ではなく<div>で組まれている

見た目は表でも、HTMLが<div>や<span>の組み合わせで作られている場合があります。read_htmlは<table>タグしか探さないため、ValueError: No tables foundになります。

この場合は、Seleniumで行ごとに要素を取り出し、DataFrameに組み立てます。

python

rows = driver.find_elements(By.CSS_SELECTOR, "div.list div.row:not(.head)")
data = [
    {
        "店舗": row.find_element(By.CSS_SELECTOR, ".name").text,
        "電話": row.find_element(By.CSS_SELECTOR, ".tel").text,
    }
    for row in rows
]
df = pd.DataFrame(data)

セレクターには、行や列に付いている意味のあるclass名を使います。「css-1x2y3z」のように自動生成されたclass名は、サイトの更新で変わりやすいため避けてください。XPathで指定する方法は、XPathの書き方を参考にしてください。

4.ページ内に表が複数ある

前章のとおり、read_htmlはページ内のすべての表を返します。ナビゲーションやフッターが<table>で作られているページでは、データの表より先にそれらが入ることがあります。matchかattrsで対象を指定すれば、ページの構成が変わっても目的の表を取得できます。

5.ヘッダーが多段になっている

気象庁の日別値の表は、「降水量(mm)」の下に「合計」「最大」、その下に「1時間」「10分間」と、ヘッダーが4段に分かれています。そのまま読み込むと、列名は('降水量(mm)', '降水量(mm)', '最大', '1時間')のようなタプルになり、扱いにくくなります。

次の関数で、重複を除いて「上段_下段」の1段にまとめます。

python

def flatten_columns(df):
    if df.columns.nlevels > 1:
        df.columns = [
            "_".join(dict.fromkeys(str(x) for x in col if "Unnamed" not in str(x)))
            for col in df.columns
        ]
    return df

df = flatten_columns(df)
print(list(df.columns)[:4])
# ['日', '気圧(hPa)_現地_平均', '気圧(hPa)_海面_平均', '降水量(mm)_合計']

*出典:気象庁ホームページのデータをもとに作成(列名を加工)

6.pandas 3でHTML文字列をそのまま渡している

以前の解説記事では、pd.read_html(driver.page_source)のようにHTMLの文字列を直接渡すコードがよく使われていました。pandas 3.0では、この書き方は文字列をファイルのパスとして扱うため、FileNotFoundErrorになります。

文字列はStringIOで包んでから渡してください。

python

from io import StringIO

df = pd.read_html(StringIO(driver.page_source))[0]

対象要素を正しく選択するためのチェックリスト

取得したい表や要素が見つからないときは、開発者ツールで次の順に確認すると、原因をすばやく絞り込めます。

  1. 「ページのソースを表示」で、表の中の文字が見つかるか。見つからなければJavaScriptで描画されている
  2. 開発者ツールで、表の上の階層に<iframe>があるか(パンくずリストにhtmlとbodyが2回出てこないか)
  3. 表のタグが<table>か、<div>か
  4. 同じページに<table>がいくつあるか、目的の表にだけ含まれる文字やidは何か
  5. ヘッダーの行が何段あるか、空欄に「–」などの記号が入っていないか

複数サイトの表をまとめてCSVに出力する方法

対象のサイトが増えたら、URLの一覧をファイルにまとめ、ループで処理します。

Step 1:URLの一覧をCSVで用意する

検索や社内の資料から対象のページを集め、urls.csvに次の形で書き出します。「キーワード」には、目的の表にだけ含まれる文字を入れます。

サイト名,URL,キーワード
東京の日別値,https://www.data.jma.go.jp/stats/etrn/view/daily_s1.php?prec_no=44&block_no=47662&year=2026&month=9&day=&view=,降水量

Step 2:表を取得してまとめるスクリプトを実行する

次のスクリプトは、まずpandasで取得を試し、表が見つからない場合だけSeleniumに切り替えます。取得できなかったページは処理を止めずに飛ばし、errors.csvに記録します。

python

import time
from datetime import date
from io import StringIO

import pandas as pd
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

WAIT_SECONDS = 2  # 1サイトごとの待機時間


def flatten_columns(df):
    """多段ヘッダーを「上段_下段」の1段にまとめる"""
    if df.columns.nlevels > 1:
        df.columns = [
            "_".join(dict.fromkeys(str(x) for x in col if "Unnamed" not in str(x)))
            for col in df.columns
        ]
    return df


def read_with_pandas(url, keyword):
    return pd.read_html(url, match=keyword, na_values=["--"])[0]


def read_with_selenium(driver, url, keyword):
    driver.get(url)
    WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.XPATH, f"//table[contains(., '{keyword}')]"))
    )
    return pd.read_html(StringIO(driver.page_source), match=keyword, na_values=["--"])[0]


def main():
    targets = pd.read_csv("urls.csv")  # 列:サイト名, URL, キーワード
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")
    driver = None
    frames, errors = [], []

    for row in targets.itertuples(index=False):
        name, url, keyword = row.サイト名, row.URL, row.キーワード
        try:
            try:
                df = read_with_pandas(url, keyword)
                how = "pandas"
            except ValueError:  # No tables found / 該当する表なし
                if driver is None:
                    driver = webdriver.Chrome(options=options)
                df = read_with_selenium(driver, url, keyword)
                how = "selenium"
            df = flatten_columns(df)
            df.insert(0, "取得元", name)
            df.insert(1, "取得日", date.today().isoformat())
            frames.append(df)
            print(f"OK   {name}: {len(df)}行 ({how})")
        except Exception as e:
            errors.append({"サイト名": name, "URL": url, "エラー": type(e).__name__})
            print(f"SKIP {name}: {type(e).__name__}")
        time.sleep(WAIT_SECONDS)

    if driver is not None:
        driver.quit()
    if frames:
        pd.concat(frames, ignore_index=True).to_csv(
            "tables.csv", index=False, encoding="utf-8-sig")
    if errors:
        pd.DataFrame(errors).to_csv("errors.csv", index=False, encoding="utf-8-sig")


if __name__ == "__main__":
    main()

実行すると、サイトごとに取得方法と行数が表示されます。静的な表、JavaScriptで描画される表、存在しないページの3件を処理した場合の出力は次のとおりです。

OK   静的な表: 2行 (pandas)
OK   JS描画の表: 2行 (selenium)
SKIP 存在しないページ: HTTPError

Step 3:出力されたCSVを確認する

tables.csvには「取得元」と「取得日」の列が先頭に付くため、どのサイトのいつのデータかを後から確認できます。サイトごとに列が異なる場合、concatは存在しない列を空欄で埋めて1つの表にまとめます。

encoding="utf-8-sig"は、ファイルの先頭にBOMを付けてUTF-8で保存する指定です。Microsoftのサポート情報のとおり、BOM付きのUTF-8で保存したCSVは、ダブルクリックでExcelを開いても日本語が正しく表示されます。

気象庁の東京と大阪の2026年9月の日別値をurls.csvに登録して実行すると、2ページ分の60行が1つのファイルにまとまりました。「–」だった雪の列は、na_valuesの指定により空欄(欠損値)になっています。

*出典:気象庁ホームページのデータをもとに作成(列名を加工)

コードを書かずに表を取得する方法との使い分け

状況向いている方法注意点
1〜2ページの静的な表をExcelに取り込みたいExcelのPower Query(Webから)JavaScriptで描画される表は取得できない
Googleスプレッドシートで表を参照したいIMPORTHTML関数対象は静的な<table>に限られる
表の構造や取得手順を細かく制御したいPython(pandas、Selenium)サイトの変更に合わせてコードを保守する
構造の違う多数のサイトから定期的に取得したいOctoparse多段ヘッダーの表は列名を整えてから出力する
対象サイトがAPIやCSVダウンロードを提供している公式のAPI・ダウンロード利用条件を確認する

Excelでの取り込み方はExcelでWebデータを自動取得する方法、スプレッドシートでの方法はスプレッドシートでWebスクレイピングする方法で解説しています。

Octoparseで表を取得する場合

Octoparse(オクトパース・オクトパス)は、ページを内蔵ブラウザーで開き、クリック操作で取得する項目を選ぶノーコードの収集ツールです。JavaScriptで描画される表も、ブラウザーで表示された状態から取得できます。コードを保守する担当者がいない場合や、対象のサイトが多く構造がばらばらな場合に向いています。

Step 1:URLを入力して自動検出を実行する

ホーム画面の入力欄に表のあるページのURLを貼り付け、「スタート」を押します。自動検出機能がページを解析し、表の行と列を項目として提案します。

前章と同じ気象庁の日別値のページで試すと、ページ内のデータ表が選択され、1日〜30日の30行がデータプレビューに並びました。各日の「日」のリンク先URLも、列として取得されます。

Step 2:列名を整える

データプレビューには、表の値が1日〜30日の30行そろって表示されます。自動検出では、ヘッダー1段目の「気圧(hPa)」「降水量(mm)」「気温(℃)」などをもとに仮の列名が付くため、多段ヘッダーの表では、2段目以降の「現地」「海面」「合計」「最大」といった区分を列名に加えておくと、出力後のデータが読みやすくなります。

データプレビューの列名をクリックし、元の表の見出しと照らし合わせながら「気圧現地平均」「気圧海面平均」「降水量_合計」のように名前を付けていきます。Pythonのflatten_columns関数で行った処理を、画面上の操作で行うイメージです。

Step 3:実行してCSVで出力する

「ワークフローを生成」を押して実行し、終わったら「エクスポート」からCSVかExcelで保存します。エクスポートしたCSVは、ダブルクリックでExcelを開いても日本語が正しく表示されます。

Octoparseは無料プランから利用できます。プランごとの機能と最新の料金は料金ページで確認してください。

失敗しやすいポイントと運用の注意

ブラウザーとドライバーのバージョンが合わない

Seleniumを起動したときにSessionNotCreatedException: This version of ChromeDriver only supports Chrome version …と表示される場合は、Chromeとドライバーのバージョンが一致していません。Selenium 4.6以降は、Selenium Managerがインストール済みのChromeに合うドライバーを自動で用意します。古いchromedriverを手動でPATHに置いている場合は、それを削除して再実行してください。初回の起動はドライバーのダウンロードを含むため、数十秒かかることがあります。

取得した値をそのまま集計する

「–」「×」「―」のような記号や、「1,200円」のような単位付きの値が残っていると、数値として集計できません。na_valuesで記号を欠損値にし、単位は読み込み後に取り除いてから集計します。

アクセスの間隔と利用条件

  • 対象サイトの利用規約とrobots.txtを確認し、自動取得を禁止しているページは対象から外す
  • 1ページごとに数秒の間隔を空け、必要なページだけを取得する
  • ログインが必要なページや個人情報を含むページは取得しない
  • 取得したデータを公開する場合は、出典の表示など、各サイトの利用条件に従う

今回例に使った気象庁のデータは、気象庁ホームページの利用規約に従い、出典を記載すれば利用できます。加工した場合は、加工したことも明記します。

よくある質問

質問1:ValueError: No tables foundと表示されるのはなぜですか?

読み込んだHTMLに<table>タグがないためです。表がJavaScriptで後から描画されているか、<div>で組まれていることが多いです。ブラウザーの「ページのソースを表示」で表の中の文字を検索し、見つからなければSeleniumで表示を待ってから取得してください。

質問2:なぜStringIOで包む必要があるのですか?

pandas 3.0のread_htmlは、渡された文字列をファイルのパスかURLとして扱います。HTMLの文字列をそのまま渡すとFileNotFoundErrorになるため、StringIOでファイルのように扱える形にしてから渡します。

質問3:chromedriverを手動でダウンロードする必要はありますか?

Selenium 4.6以降では不要です。webdriver.Chrome()を実行すると、Selenium Managerがインストール済みのChromeに合うドライバーを自動で用意します。

質問4:iframeの中の表を取得したあと、元のページの要素を操作するには?

driver.switch_to.default_content()で外側のページに戻ります。戻らずに外側の要素を探すと、NoSuchElementExceptionになります。

質問5:ヘッダーが複数行ある表はどう扱えばよいですか?

read_htmlは多段のヘッダーを複数レベルの列名として読み込みます。本文のflatten_columns関数のように、各レベルを「_」でつないで1段にまとめると、CSVやExcelで扱いやすくなります。

質問6:Seleniumは処理が遅いのですが、速くする方法は?

まずpandasのread_htmlだけで取得できないかを試し、表が見つからないページだけSeleniumを使います。本文のスクリプトもこの順で処理しています。ブラウザーはサイトごとに起動し直さず、1つを使い回してください。

まとめ

この記事では、Pythonでwebサイトの表を取得する方法と、取得できないときの対処を紹介しました。静的な<table>ならpandasのread_htmlだけで取得でき、matchやattrsで対象の表を指定すれば、ページ内に表が複数あっても目的の表を選べます。JavaScriptで描画される表はWebDriverWaitで表示を待ち、iframeの中の表はswitch_to.frameで切り替え、<div>で組まれた表はfind_elementsで行ごとに取り出します。

複数サイトの表は、URLの一覧をループで処理してutf-8-sigでCSVに出力すれば、Excelでそのまま開ける1つのファイルにまとまります。コードの保守が難しい場合や対象サイトが多い場合は、Octoparseのようなノーコードツールも選択肢になります。

クリックだけでウェブ データを取得
無料ダウンロード

今すぐOctoparseを始めて、業務効率化を実現しましょう。

ダウンロード

関連記事