HTML テーブルの構造

テーブルのスクレイピングでは HTML の構造を正確に理解することが最重要です。

HTML — テーブルの基本構造
<table id="sales" class="data-table">
  <caption>月別売上実績</caption>      ← テーブルのタイトル
  <thead>                              ← ヘッダー行グループ
    <tr>                               ← 行(table row)
      <th>月</th>                      ← ヘッダーセル(table header)
      <th>売上</th>
    </tr>
  </thead>
  <tbody>                              ← データ行グループ
    <tr>
      <td>1月</td>                     ← データセル(table data)
      <td>1,200,000</td>
    </tr>
    <tr>
      <td>2月</td>
      <td>980,000</td>
    </tr>
  </tbody>
  <tfoot>                              ← フッター行グループ(省略可)
    <tr>
      <td>合計</td>
      <td>2,180,000</td>
    </tr>
  </tfoot>
</table>

上の HTML をブラウザで表示するとこのようになります:

月別売上実績
売上
1月1,200,000
2月980,000
合計2,180,000

💡 実際のサイトでよくある落とし穴

<thead><tbody> が省略されているケースがある → find_all("tr") で全行を取得することで対応できます。② セルが <th><td> 混在することがある → find_all(["th","td"]) でまとめて取得します。

全テーブルを取得する

まず「このページに何個テーブルがあるか」を把握することから始めます。

Python — 全テーブルの取得と概要確認
from bs4 import BeautifulSoup

html = """
<html><body>
  <table id="users" class="data-table">
    <caption>ユーザー一覧</caption>
    <thead><tr><th>ID</th><th>名前</th><th>年齢</th></tr></thead>
    <tbody>
      <tr><td>1</td><td>田中 太郎</td><td>28</td></tr>
      <tr><td>2</td><td>鈴木 花子</td><td>34</td></tr>
      <tr><td>3</td><td>佐藤 次郎</td><td>22</td></tr>
    </tbody>
  </table>

  <table id="products" class="data-table highlight">
    <caption>商品一覧</caption>
    <thead><tr><th>商品名</th><th>価格</th><th>在庫</th></tr></thead>
    <tbody>
      <tr><td>りんご</td><td>150</td><td>120</td></tr>
      <tr><td>みかん</td><td>80</td><td>0</td></tr>
    </tbody>
  </table>

  <table class="summary">
    <tr><th>項目</th><th>値</th></tr>
    <tr><td>合計ユーザー数</td><td>3</td></tr>
    <tr><td>合計商品数</td><td>2</td></tr>
  </table>
</body></html>
"""
soup = BeautifulSoup(html, "lxml")

# ── 全テーブルを取得 ──────────────────────
tables = soup.find_all("table")
print(f"テーブル数: {len(tables)}")    # 3

# ── 各テーブルの概要を確認 ───────────────
for i, tbl in enumerate(tables):
    caption  = tbl.find("caption")
    tbl_id   = tbl.get("id", "(なし)")
    tbl_cls  = tbl.get("class", [])
    rows     = tbl.find_all("tr")
    print(f"[{i}] id={tbl_id}, class={tbl_cls}, "
          f"caption={caption.text if caption else 'なし'}, "
          f"行数={len(rows)}")
実行結果
テーブル数: 3
[0] id=users,    class=['data-table'],           caption=ユーザー一覧, 行数=4
[1] id=products, class=['data-table','highlight'],caption=商品一覧,   行数=3
[2] id=(なし),  class=['summary'],              caption=なし,       行数=3

特定のテーブルを取得する

複数テーブルの中から目的のものを特定する方法は状況によって異なります。

SCENARIO 1
インデックス指定
ページ内の出現順が固定のとき。シンプルだがサイト変更で壊れやすい。
SCENARIO 2
id 属性で指定
テーブルに id が付いているときの最も確実な方法。
SCENARIO 3
class 属性で指定
複数のテーブルを同じ class でまとめて取得できる。
SCENARIO 4
caption で特定
テーブルにタイトル(caption)がある場合に有効。
SCENARIO 5
ヘッダーで特定
ヘッダー行の特定の文字列からテーブルを逆引きする最も実用的な方法。

① インデックスで指定

Python
tables = soup.find_all("table")

first_table  = tables[0]    # ページ内で最初のテーブル
second_table = tables[1]    # 2番目
last_table   = tables[-1]   # 最後のテーブル

② id 属性で指定

Python
# find() に id を渡す
users_table = soup.find("table", id="users")

# CSS セレクタ版
users_table = soup.select_one("table#users")

# 見つからなかった場合の安全処理
if users_table is None:
    print("テーブルが見つかりませんでした")
else:
    print(users_table.find("caption").text)    # ユーザー一覧

③ class 属性で指定

Python
# class="data-table" のすべてのテーブル
data_tables = soup.find_all("table", class_="data-table")
print(len(data_tables))    # 2(users, products)

# class に "highlight" を含むもの(複数クラスの部分一致)
highlight_tables = soup.find_all("table", class_="highlight")
print(len(highlight_tables))    # 1(products のみ)

# CSS セレクタ版(クラスを複数指定)
tables = soup.select("table.data-table.highlight")
print(len(tables))    # 1

④ caption(タイトル)で指定

Python
def find_table_by_caption(soup, caption_text: str):
    """caption テキストが一致する table を返す"""
    caption = soup.find("caption", string=caption_text)
    if caption:
        return caption.find_parent("table")
    return None

products_table = find_table_by_caption(soup, "商品一覧")
print(products_table.get("id"))    # products

⑤ ヘッダー文字列で特定(最も実用的)

id や class が付いておらず、caption もない場合でも、 ヘッダー行の特定のキーワードをもとにテーブルを特定できます。 実務でのスクレイピングで最も使用頻度が高い手法です。

Python — ヘッダー文字列でテーブルを特定
def find_table_by_header(soup, *header_keywords):
    """
    ヘッダー行に指定キーワードをすべて含むテーブルを返す。
    複数のキーワードを AND 条件で絞り込む。
    """
    for tbl in soup.find_all("table"):
        # thead の th / td を取得(thead がない場合は先頭 tr を使う)
        thead = tbl.find("thead")
        header_row = thead.find("tr") if thead else tbl.find("tr")
        if not header_row:
            continue
        headers = [
            cell.get_text(strip=True)
            for cell in header_row.find_all(["th", "td"])
        ]
        # 全キーワードがヘッダーに含まれているか
        if all(kw in headers for kw in header_keywords):
            return tbl
    return None

# 使用例
# 「商品名」「在庫」の両方を列ヘッダーに持つテーブルを探す
tbl = find_table_by_header(soup, "商品名", "在庫")
if tbl:
    print(tbl.get("id"))        # products

# 「ID」「名前」「年齢」を持つテーブル
tbl2 = find_table_by_header(soup, "ID", "名前", "年齢")
print(tbl2.get("id"))           # users

行・列・セルを取得する

テーブルを特定したあと、行(tr)・セル(th/td)を取得する方法です。

Python — 行・列・セルの取得
soup = BeautifulSoup(html, "lxml")
tbl = soup.find("table", id="users")

# ── 全行を取得 ────────────────────────────
all_rows = tbl.find_all("tr")
print(f"全行数(ヘッダー含む): {len(all_rows)}")   # 4

# ── ヘッダー行だけ取得 ────────────────────
thead = tbl.find("thead")
if thead:
    header_row = thead.find("tr")
    headers = [th.get_text(strip=True) for th in header_row.find_all("th")]
    print(headers)     # ['ID', '名前', '年齢']

# ── データ行だけ取得 ──────────────────────
tbody = tbl.find("tbody")
data_rows = tbody.find_all("tr") if tbody else tbl.find_all("tr")[1:]

for row in data_rows:
    cells = row.find_all("td")
    values = [c.get_text(strip=True) for c in cells]
    print(values)
# ['1', '田中 太郎', '28']
# ['2', '鈴木 花子', '34']
# ['3', '佐藤 次郎', '22']

# ── 特定の列だけ取得(例:2列目 = インデックス1) ──
col_index = 1    # 「名前」列
names = []
for row in data_rows:
    cells = row.find_all(["th", "td"])
    if len(cells) > col_index:
        names.append(cells[col_index].get_text(strip=True))
print(names)    # ['田中 太郎', '鈴木 花子', '佐藤 次郎']

# ── 特定のセルをピンポイントで取得 ─────────
# n行目・m列目(0始まり)
def get_cell(table, row_index: int, col_index: int) -> str:
    rows = table.find_all("tr")
    if row_index >= len(rows):
        return ""
    cells = rows[row_index].find_all(["th", "td"])
    if col_index >= len(cells):
        return ""
    return cells[col_index].get_text(strip=True)

print(get_cell(tbl, 1, 1))    # 田中 太郎(1行目データ・2列目)
print(get_cell(tbl, 2, 2))    # 34(2行目データ・3列目)

テーブルを2次元リストに変換

テーブル全体を list[list[str]] に変換すると、後処理がしやすくなります。

Python — 2次元リスト変換ユーティリティ
def table_to_list(table) -> list[list[str]]:
    """
    BeautifulSoup の table タグを 2次元リストに変換する。
    thead / tbody / tfoot のどれにも対応。
    """
    rows = table.find_all("tr")
    result = []
    for row in rows:
        cells = row.find_all(["th", "td"])
        result.append([c.get_text(strip=True) for c in cells])
    return result

# ── 使用例 ──────────────────────────────
soup = BeautifulSoup(html, "lxml")

users_tbl = soup.find("table", id="users")
data = table_to_list(users_tbl)

# ヘッダーと本体を分離
headers = data[0]
rows    = data[1:]

print(headers)
# ['ID', '名前', '年齢']

for row in rows:
    print(row)
# ['1', '田中 太郎', '28']
# ['2', '鈴木 花子', '34']
# ['3', '佐藤 次郎', '22']

# dict のリストに変換(ヘッダーをキーにする)
records = [dict(zip(headers, row)) for row in rows]
print(records[0])
# {'ID': '1', '名前': '田中 太郎', '年齢': '28'}

テーブル内から特定の値を持つセル、または行を探す方法です。

Python — セルの条件検索
import re
from bs4 import BeautifulSoup

html = """
<table id="products">
  <thead><tr><th>商品名</th><th>価格</th><th>在庫</th></tr></thead>
  <tbody>
    <tr><td>りんご</td><td>150</td><td>120</td></tr>
    <tr><td>みかん</td><td>80</td><td>0</td></tr>
    <tr><td>ぶどう</td><td>350</td><td>45</td></tr>
    <tr><td>メロン</td><td>1200</td><td>8</td></tr>
  </tbody>
</table>
"""
soup = BeautifulSoup(html, "lxml")
tbl  = soup.find("table", id="products")

# ── ① 完全一致でセルを検索 ──────────────
cell = tbl.find("td", string="みかん")
print(cell.text if cell else "Not found")    # みかん

# ── ② 正規表現でセルを検索 ──────────────
cells = tbl.find_all("td", string=re.compile(r"[ぁ-ん]ご"))  # 「ご」で終わる
print([c.text for c in cells])               # ['りんご']

# ── ③ セルを含む行ごと取得 ───────────────
def find_rows_by_cell(table, col_index: int, pattern):
    """指定列のセルが pattern(文字列 or 正規表現)に一致する行を返す"""
    tbody = table.find("tbody")
    rows  = tbody.find_all("tr") if tbody else table.find_all("tr")[1:]
    result = []
    for row in rows:
        cells = row.find_all("td")
        if len(cells) > col_index:
            text = cells[col_index].get_text(strip=True)
            if isinstance(pattern, str):
                if pattern == text:
                    result.append(row)
            else:
                if pattern.search(text):
                    result.append(row)
    return result

# 商品名列(0列目)が「みかん」の行
rows = find_rows_by_cell(tbl, 0, "みかん")
for r in rows:
    print([c.text for c in r.find_all("td")])
# ['みかん', '80', '0']

# ── ④ 数値条件でフィルタリング ──────────
def find_rows_by_numeric(table, col_index: int, operator, threshold):
    """指定列の数値が条件を満たす行を返す。operator は比較関数。"""
    import operator as op
    tbody = table.find("tbody")
    rows  = tbody.find_all("tr") if tbody else table.find_all("tr")[1:]
    result = []
    for row in rows:
        cells = row.find_all("td")
        if len(cells) > col_index:
            raw = cells[col_index].get_text(strip=True).replace(",", "")
            try:
                val = float(raw)
                if operator(val, threshold):
                    result.append(row)
            except ValueError:
                pass
    return result

import operator as op

# 価格(1列目)が 200 以上の行
expensive = find_rows_by_numeric(tbl, 1, op.ge, 200)
for r in expensive:
    cells = [c.text for c in r.find_all("td")]
    print(cells)
# ['ぶどう', '350', '45']
# ['メロン', '1200', '8']

# 在庫(2列目)が 0 の行
out_of_stock = find_rows_by_numeric(tbl, 2, op.eq, 0)
for r in out_of_stock:
    print([c.text for c in r.find_all("td")])
# ['みかん', '80', '0']

pandas DataFrame に変換

データ分析や CSV 出力には pandas の DataFrame に変換するのが最も効率的です。

Shell — pandas のインストール
pip install pandas

方法 A:pd.read_html()(最も簡単)

pandas.read_html() は HTML 文字列または URL を受け取り、すべてのテーブルを DataFrame のリストとして返します。

Python — pd.read_html()
import pandas as pd
from bs4 import BeautifulSoup

# ── HTML 文字列から直接 ────────────────────
dfs = pd.read_html(html)          # 全テーブルを DataFrame のリストとして取得
print(len(dfs))                   # ページ内のテーブル数

df_users    = dfs[0]              # インデックスで指定
df_products = dfs[1]

print(df_users)
#    ID      名前  年齢
# 0   1  田中 太郎   28
# 1   2  鈴木 花子   34
# 2   3  佐藤 次郎   22

# ── URL から直接取得 ───────────────────────
# dfs = pd.read_html("https://example.com/page", encoding="utf-8")

# ── match で特定テーブルを絞り込む ────────
# caption や th テキストに一致するテーブルだけ取得
dfs = pd.read_html(html, match="商品")    # caption や th に「商品」を含むもの
df_products = dfs[0]
print(df_products)
#   商品名   価格  在庫
# 0  りんご  150   120
# 1  みかん   80     0

# ── 型変換とクリーニング ─────────────────
df_products["価格"] = pd.to_numeric(df_products["価格"], errors="coerce")
df_products["在庫"] = pd.to_numeric(df_products["在庫"], errors="coerce")
print(df_products.dtypes)

# ── CSV に書き出す ────────────────────────
df_products.to_csv("products.csv", index=False, encoding="utf-8-sig")  # UTF-8 BOM付き(Excel対応)

方法 B:BeautifulSoup + 手動変換(細かい制御が必要なとき)

Python — BeautifulSoup → DataFrame
import pandas as pd
from bs4 import BeautifulSoup

def table_to_dataframe(table) -> pd.DataFrame:
    """
    BeautifulSoup の table タグを pandas DataFrame に変換する。
    ヘッダーは thead の th を使用。なければ最初の tr を使用。
    """
    # ヘッダーを取得
    thead = table.find("thead")
    if thead:
        header_row  = thead.find("tr")
        headers     = [th.get_text(strip=True) for th in header_row.find_all(["th","td"])]
        tbody       = table.find("tbody")
        data_rows   = tbody.find_all("tr") if tbody else []
    else:
        all_rows    = table.find_all("tr")
        header_row  = all_rows[0]
        headers     = [th.get_text(strip=True) for th in header_row.find_all(["th","td"])]
        data_rows   = all_rows[1:]

    # データ行を変換
    records = []
    for row in data_rows:
        cells = row.find_all(["th","td"])
        values = [c.get_text(strip=True) for c in cells]
        # 列数が合わない場合は空文字で埋める
        while len(values) < len(headers):
            values.append("")
        records.append(dict(zip(headers, values[:len(headers)])))

    return pd.DataFrame(records)

# ── 使用例 ──────────────────────────────
soup = BeautifulSoup(html, "lxml")

df = table_to_dataframe(soup.find("table", id="products"))
print(df)
#   商品名   価格   在庫
# 0  りんご  150   120
# 1  みかん   80     0

# 数値型に変換
df["価格"] = pd.to_numeric(df["価格"])
df["在庫"] = pd.to_numeric(df["在庫"])

# 在庫 0 を絞り込み
print(df[df["在庫"] == 0])
#   商品名  価格  在庫
# 1  みかん    80     0

複数テーブルを一括処理

ページ内のすべてのテーブルをループで処理するパターンです。

Python — 全テーブルを DataFrame に変換して辞書に格納
import pandas as pd
from bs4 import BeautifulSoup

def scrape_all_tables(html_str: str) -> dict[str, pd.DataFrame]:
    """
    ページ内の全テーブルを辞書にまとめる。
    キーは id > caption > 連番 の優先順で決める。
    """
    soup = BeautifulSoup(html_str, "lxml")
    result = {}
    counter = 0

    for tbl in soup.find_all("table"):
        # キー名を決定
        tbl_id  = tbl.get("id")
        caption = tbl.find("caption")
        if tbl_id:
            key = tbl_id
        elif caption:
            key = caption.get_text(strip=True)
        else:
            key = f"table_{counter}"
            counter += 1

        try:
            df = table_to_dataframe(tbl)   # 前節の関数
            if not df.empty:
                result[key] = df
        except Exception as e:
            print(f"[WARN] {key} の変換失敗: {e}")

    return result

# ── 使用例 ──────────────────────────────
tables_dict = scrape_all_tables(html)

for name, df in tables_dict.items():
    print(f"\n=== {name} ===")
    print(df.to_string(index=False))

# CSV に個別保存
for name, df in tables_dict.items():
    filename = name.replace("/", "_") + ".csv"
    df.to_csv(filename, index=False, encoding="utf-8-sig")
    print(f"保存: {filename}")

colspan / rowspan を処理する

実際の Web サイトのテーブルには、セルが複数列・複数行にまたがる colspanrowspan が使われていることがあります。 これをそのまま処理するとデータがずれるため、補完処理が必要です。

Python — colspan / rowspan を展開する
from bs4 import BeautifulSoup

html_span = """
<table>
  <tr>
    <th>カテゴリ</th>
    <th colspan="2">詳細</th>      <!-- 2列占有 -->
  </tr>
  <tr>
    <td rowspan="2">果物</td>      <!-- 2行占有 -->
    <td>りんご</td>
    <td>150円</td>
  </tr>
  <tr>
    <td>みかん</td>
    <td>80円</td>
  </tr>
</table>
"""

def expand_table(table) -> list[list[str]]:
    """
    colspan / rowspan を展開してフラットな 2D リストを返す。
    rowspan のセルは後続行に同じ値を複製する。
    """
    rows   = table.find_all("tr")
    matrix = []
    rowspan_map: dict[tuple[int,int], str] = {}  # {(行,列): 値}

    for r_idx, row in enumerate(rows):
        col_idx = 0
        cells   = row.find_all(["th", "td"])
        row_data: list[str] = []

        cell_iter = iter(cells)
        while True:
            # rowspan で予約済みの列を先に埋める
            while (r_idx, col_idx) in rowspan_map:
                row_data.append(rowspan_map.pop((r_idx, col_idx)))
                col_idx += 1

            try:
                cell = next(cell_iter)
            except StopIteration:
                break

            text    = cell.get_text(strip=True)
            colspan = int(cell.get("colspan", 1))
            rowspan = int(cell.get("rowspan", 1))

            # colspan の分だけ列を展開
            for c in range(colspan):
                row_data.append(text)
                # rowspan がある場合は後の行に予約
                for rs in range(1, rowspan):
                    rowspan_map[(r_idx + rs, col_idx + c)] = text
            col_idx += colspan

        # 行末に残った rowspan 予約を処理
        while (r_idx, col_idx) in rowspan_map:
            row_data.append(rowspan_map.pop((r_idx, col_idx)))
            col_idx += 1

        matrix.append(row_data)

    return matrix

soup = BeautifulSoup(html_span, "lxml")
result = expand_table(soup.find("table"))
for row in result:
    print(row)
# ['カテゴリ', '詳細', '詳細']
# ['果物', 'りんご', '150円']
# ['果物', 'みかん', '80円']

pd.read_html() は colspan / rowspan を自動展開する

上のような複雑なケースでは pd.read_html() を使うと自動的に展開してくれます。手動実装が必要なのは、read_html では対応しきれない特殊なレイアウトのときに限られます。

実践サンプル — 複数テーブルを含むページを一括スクレイプ

requests でページを取得し、複数テーブルを特定・抽出・CSV保存する完全なスクリプトです。

Python — 実践:複数テーブルのスクレイプ完全版
"""
実践サンプル: 複数テーブルを含む Web ページをスクレイプして CSV 保存する
必要パッケージ: pip install beautifulsoup4 lxml requests pandas
"""
import time
import requests
import pandas as pd
from bs4 import BeautifulSoup
from pathlib import Path

# ── 設定 ─────────────────────────────────────────────────────
URL       = "https://example.com/report"    # ← 対象 URL を変更
OUTPUT_DIR = Path("output")
HEADERS   = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 Chrome/124 Safari/537.36"
    )
}
TARGET_HEADERS = ["商品名", "価格", "在庫"]   # ← 取得したいテーブルの列ヘッダー

# ── ユーティリティ ────────────────────────────────────────────
def fetch_soup(url: str) -> BeautifulSoup:
    resp = requests.get(url, headers=HEADERS, timeout=15)
    resp.raise_for_status()
    resp.encoding = resp.apparent_encoding
    return BeautifulSoup(resp.text, "lxml")

def find_table_by_header(soup, *keywords):
    for tbl in soup.find_all("table"):
        thead = tbl.find("thead")
        hr    = thead.find("tr") if thead else tbl.find("tr")
        if not hr:
            continue
        cols = [c.get_text(strip=True) for c in hr.find_all(["th","td"])]
        if all(kw in cols for kw in keywords):
            return tbl
    return None

def table_to_dataframe(table) -> pd.DataFrame:
    thead = table.find("thead")
    if thead:
        header_cells = thead.find("tr").find_all(["th","td"])
        headers      = [c.get_text(strip=True) for c in header_cells]
        tbody        = table.find("tbody")
        data_rows    = tbody.find_all("tr") if tbody else []
    else:
        all_rows  = table.find_all("tr")
        headers   = [c.get_text(strip=True) for c in all_rows[0].find_all(["th","td"])]
        data_rows = all_rows[1:]

    records = []
    for row in data_rows:
        cells  = [c.get_text(strip=True) for c in row.find_all(["th","td"])]
        while len(cells) < len(headers):
            cells.append("")
        records.append(dict(zip(headers, cells[:len(headers)])))
    return pd.DataFrame(records)

# ── メイン処理 ────────────────────────────────────────────────
def main():
    OUTPUT_DIR.mkdir(exist_ok=True)
    print(f"取得中: {URL}")

    try:
        soup = fetch_soup(URL)
    except requests.RequestException as e:
        print(f"取得失敗: {e}")
        return

    # ヘッダー文字列で目的のテーブルを特定
    tbl = find_table_by_header(soup, *TARGET_HEADERS)
    if tbl is None:
        print(f"テーブルが見つかりませんでした: {TARGET_HEADERS}")
        return

    # DataFrame に変換
    df = table_to_dataframe(tbl)
    print(f"取得行数: {len(df)}")
    print(df.head())

    # 型変換(数値列)
    for col in ["価格", "在庫"]:
        if col in df.columns:
            df[col] = pd.to_numeric(df[col].str.replace(",",""), errors="coerce")

    # CSV に保存
    out_path = OUTPUT_DIR / "result.csv"
    df.to_csv(out_path, index=False, encoding="utf-8-sig")
    print(f"保存完了: {out_path}")

    # 在庫 0 の商品を別ファイルに保存
    if "在庫" in df.columns:
        oos = df[df["在庫"] == 0]
        if not oos.empty:
            oos.to_csv(OUTPUT_DIR / "out_of_stock.csv", index=False, encoding="utf-8-sig")
            print(f"在庫切れ: {len(oos)} 件 → out_of_stock.csv")

    time.sleep(1)    # 礼儀として1秒待機

if __name__ == "__main__":
    main()

💡 このシリーズのまとめ

PART 01(環境構築)→ PART 02(BeautifulSoup の基本)→ PART 03(テーブル操作)の3章で、Python による HTML スクレイピングの基礎から実践まで一通りの知識が揃います。次のステップとしては、JavaScript 動的レンダリングが必要なページへの対応(Selenium / Playwright)や、定期実行の自動化(cron / schedule)などがあります。

テーブル操作 メソッド早見表

やりたいことコード
全テーブルを取得soup.find_all("table")
n 番目のテーブルsoup.find_all("table")[n]
id で特定soup.find("table", id="xxx")
class で特定soup.find("table", class_="xxx")
caption で特定soup.find("caption", string="xxx").find_parent("table")
全行を取得table.find_all("tr")
ヘッダー行のセルtable.find("thead").find("tr").find_all(["th","td"])
データ行のセルtable.find("tbody").find_all("tr")
セルのテキストcell.get_text(strip=True)
文字列検索table.find("td", string="xxx")
正規表現検索table.find_all("td", string=re.compile("xxx"))
DataFrame に変換(簡単)pd.read_html(html_str)[n]
DataFrame に変換(細かい制御)table_to_dataframe(table)(本記事の関数)
CSV に保存df.to_csv("file.csv", encoding="utf-8-sig")