ふくふくHukuhuku Inc.
EP.09Data Fetch 9分公開: 2026-09-02

pandas.read_html で表を取る ── 動く場面と、動かない場面

Web ページの表を1行で取れる便利な関数ですが、動かないときの理由がわかりにくい。どういう表なら取れて、取れないときは何が起きているのかを整理します。

#Python#pandas#スクレイピング
執筆 / 監修
松尾 亮合同会社ふくふく 代表社員

データ基盤・データパイプライン構築 / BI / 生成 AI 活用支援を専門とするエンジニア (28 年)。 本記事は AI 利用ポリシーに基づき、生成 AI の補助で執筆 → 人間が監修・編集して公開しています。

プロフィール詳細
シェア

Web ページに載っている表を取りたい、という場面はよくあります。が無く、の配布も無く、画面には表として出ている。こういうとき read_html を使うと、URL を渡すだけで表の一覧が返ってきます。うまくいけば1行で終わります。

問題は、うまくいかないときに理由が分かりにくいことです。「表が見つかりません」という例外だけが出て、画面には確かに表が見えている。この落差でつまずきます。この回では、取れる条件と取れない条件を切り分けます。

取れるのは table タグの表だけ

read_html が探すのは HTML の table 要素です。見た目が表でも、div を並べて格子状に見せているだけなら1つも見つかりません。最近の Web サイトは表を div で組んでいることが多く、これが最初の関門になります。

これは技術的な限界なので、粘っても取れません。確認は簡単で、ブラウザでページのソースを表示して <table を検索すれば分かります。無ければ read_html では取れないので、別の手段を考えることになります。ここで判断が付くので、動かない理由を延々と探すより先に確かめる価値があります。

逆に table が見つかりすぎる場合もあります。ページの装飾やレイアウトに table を使っているサイトだと、目当ての表以外もすべて拾われます。返ってきた一覧から欲しいものを選ぶことになるので、行数や列名で絞り込む処理を書いておくと安定します。

まず table があるかどうかを確かめる。ここで判断できる
Python
import re
import requests
resp = requests.get(url, timeout=(5, 30), headers={"User-Agent": "..."})resp.raise_for_status()html = resp.text
n = len(re.findall(r"<table[\s>]", html, re.I))print(f"table タグ: {n} 個")if n == 0:    print("read_html では取れない。div で組まれているか、後から描画されている")

後から描かれる表は取れない

table タグがあるのに取れない場合、ページを開いた直後の HTML には表が無く、あとから JavaScript が描いている可能性があります。ブラウザで見れば表がありますが、requests が受け取る HTML には入っていません。

見分け方は、受け取った HTML を保存して中身を見ることです。表の中の具体的な値(見えている数字のどれか)を検索して、見つからなければ後から描かれています。この場合はブラウザを動かす道具が要りますが、その前に確かめるべきことがあります。

ブラウザを持ち出す前に、元のデータを探す

画面が JavaScript で表を作っているなら、その裏で API を呼んでいることがほとんどです。ブラウザの開発者ツールで通信を見ると、整った が返っているのが分かる場合があります。そちらを直接叩くほうが、速くて壊れにくいです。

取れたあとの型に注意する

read_html は表を読むだけで、型は推測に任されます。桁区切りのカンマ、全角の数字、単位付きの値、注記の記号。こうしたものが混ざると、数値の列が文字列として読まれます。そのまま集計すると、足し算のつもりが文字列の連結になります。

取得直後に型を確認して、数値にできない値がどれだけあるかを数えてください。想定より多ければ、変換の規則が足りていません。ここでも一緒に済ませておくと、後の処理が楽になります。

数値に直せない値を数えて、想定と合っているか確かめる
Python
import pandas as pd

def to_number(s: pd.Series) -> pd.Series:    """桁区切り・全角・空欄記号を落としてから数値にする。"""    cleaned = (        s.astype(str)        .str.replace(",", "", regex=False)        .str.replace("−", "-", regex=False)     # 全角のマイナス        .str.replace("△", "-", regex=False)     # 会計表記の負数        .str.strip()        .replace({"": None, "-": None, "―": None, "…": None, "n.a.": None})    )    return pd.to_numeric(cleaned, errors="coerce")

def check_conversion(df: pd.DataFrame, col: str) -> None:    converted = to_number(df[col])    lost = converted.isna().sum() - df[col].isna().sum()    print(f"{col}: 数値に直せなかった値 {lost} 件 / 全 {len(df)} 件")    if lost:        bad = df.loc[converted.isna() & df[col].notna(), col].unique()[:10]        print(f"  例: {list(bad)}")

使ってよい場面かを先に考える

技術的に取れることと、取ってよいことは別です。に当たる行為なので、利用規約と を確認するのが前提になります。取得の頻度も、人が見るのと同程度に抑えるのが礼儀です。

  • や CSV の配布が無いか先に探す — たいてい存在する。探す時間のほうが安い
  • 規約で禁止されていないか — 明示的に禁じているサイトは実在する
  • 表の構造が変わったら壊れる — 列の順序に依存した書き方をしない

取得の間隔も決めておいてください。人が画面を見る速さを超えて叩く理由はありません。1ページごとに数秒空けるだけで、相手から見た負荷は大きく変わります。急いで取る必要が本当にあるのかを一度考えると、たいていは急がなくてよいと分かります。

列の位置ではなく列名で参照するようにしておくと、列が1つ増えただけで壊れる事故を減らせます。それでも配信側の都合で表の作りは変わるので、壊れることを前提に監視を入れておく必要があります。次回は、開発中に相手を叩きすぎないための仕組みを扱います。

シェア

この記事の感想を教えてください

あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。

シリーズの外も探す:

まずは、現状を聞かせてください。

要件が固まっていなくて大丈夫です。現状診断と方針提案までを無料でお手伝いします。

無料相談フォームへ hello [at] hukuhuku [dot] co [dot] jp