アコーディオンを開いたのに、innerText が空だったり途中までしか取れない……
Seleniumではよくある落とし穴です。
主な原因は、遅延描画・独立スクロール・仮想リストです。
本記事は、段階スクロールとテキスト長の安定化判定を軸にした“観測駆動”テンプレートで、Seleniumにおける「アコーディオンのinnerTextが空/途中までしか取れない問題」を解決します。
見出しから対象を特定→必要なら展開→ページ末尾までの遅延描画促進→コンテナ自身のスクロール→テキスト長が連続して変化しなくなるまで待機、という手順で取りこぼしを実用上ゼロに近づけます。
すぐ使えるコピペ実装とチェックリスト付き。
実サイトではセレクタの粒度だけ最小変更すれば導入できます。
\ テンプレの理解が倍速に。
いちど通読しておくと”取りこぼしゼロ化”がスムーズです /
先に結論(Selenium×アコーディオン×innerTextの要点)
アコーディオンは、開いて見えていても中身がまだ描画されていなかったり、独立スクロール領域や仮想リストでスクロールを重ねるほどテキストが増えることがあります。
ここでは次の流れで、取りこぼしを実用上ゼロに近づけるテンプレートにします。
- 見出しテキストからアコーディオンのコンテナを特定(
<section class="accordion">や<details>を両対応) - 未展開なら展開(
aria-expandedまたは<details open>判定) - ページ全体を末尾まで数ラウンドスクロールして遅延描画を促進
- コンテナ自身を最下部まで段階スクロール(監視ベース描画に効く)
- テキスト長が連続して変化しなくなるまで観測(=安定化判定)。必要に応じて
MutationObserverでDOMアイドルを待機 - 仕上げに再観測し、長い方のテキストを採用
セレクタやXPathは汎用例です。
実DOMではタグ名・クラス名のみ最小変更で合わせてください。
フル実装(段階スクロール+安定化判定+details対応+MutationObserver)
<details>と一般的なアコーディオンを自動分岐- 段階スクロール(IntersectionObserver対策)
- テキスト長は
textContentで軽量計測 → 最後だけinnerText - StaleElement の軽いリトライ
- 任意で
MutationObserverによるDOMアイドル待機
動作環境:Selenium 4系 / Python 3.x / Chromium系ブラウザ
\ 夜間ジョブや長時間観測に。 /
# -*- coding: utf-8 -*-
"""
Seleniumでアコーディオン本文を“取りこぼしなく”抽出する観測テンプレート
- 段階スクロール + テキスト長の安定化判定
- <details> / 一般的なアコーディオン両対応
- DOMアイドル待機(MutationObserver, 任意)
- XPathリテラル安全化(引用符混在に強い)
"""
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import (
TimeoutException,
ElementClickInterceptedException,
NoSuchElementException,
StaleElementReferenceException,
)
import time
WAIT_SEC = 20 # 明示的待機の基準秒数(サイトの重さに応じて調整)
# ---------------------------
# 基本ユーティリティ
# ---------------------------
def safe_click(driver, el, scroll=True):
"""通常クリック → 遮られたら scrollIntoView + JSクリック。"""
try:
if scroll:
driver.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
# 二重 rAF で“描画2フレーム分の待ち”を確実化
driver.execute_async_script(
"requestAnimationFrame(()=>requestAnimationFrame(arguments[arguments.length-1]))"
)
el.click()
except ElementClickInterceptedException:
driver.execute_script("arguments[0].click();", el)
def scroll_window_to_bottom(driver, rounds=4, pause=0.35):
"""ページ全体を最下部へ数ラウンド。遅延描画を促進。"""
last_height = 0
for _ in range(rounds):
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(pause)
new_height = driver.execute_script("return document.body.scrollHeight") or 0
if new_height == last_height:
break
last_height = new_height
# 少し戻して末尾描画をもう一段促進
driver.execute_script(
"window.scrollTo(0, Math.max(0, document.body.scrollHeight - 400));"
)
time.sleep(0.2)
def is_scrollable(driver, el) -> bool:
"""要素自身が独立スクロール可能か。"""
try:
return bool(
driver.execute_script(
"return arguments[0].scrollHeight > arguments[0].clientHeight;", el
)
)
except Exception:
return False
def scroll_element_incremental(driver, el, steps=8, pause=0.12, ratio=0.85):
"""
監視ベース描画に強い段階スクロール。
clientHeight の ratio 分ずつ進めて描画を確実にトリガ。
"""
if not is_scrollable(driver, el):
return
h = driver.execute_script("return arguments[0].clientHeight;", el) or 400
delta = int(h * ratio)
for _ in range(max(1, steps)):
driver.execute_script("arguments[0].scrollTop += arguments[1];", el, delta)
time.sleep(pause)
def scroll_element_to_bottom(driver, el, rounds=6, pause=0.25, stable_hits=2, mode="step"):
"""
コンテナを最下部まで複数回送り続ける。
mode='step' なら段階スクロール、'jump' なら一気に scrollHeight へ。
scrollTop が連続 stable_hits 回変化しなければ終了。
"""
if not is_scrollable(driver, el):
return
last_top, stable = -1, 0
for _ in range(rounds):
if mode == "step":
scroll_element_incremental(driver, el, steps=4, pause=pause / 2, ratio=0.9)
driver.execute_script("arguments[0].scrollTop = arguments[0].scrollHeight;", el)
time.sleep(pause)
cur_top = driver.execute_script("return arguments[0].scrollTop;", el) or 0
if cur_top == last_top:
stable += 1
else:
stable = 0
last_top = cur_top
if stable >= stable_hits:
break
def _text_len_fast(driver, el) -> int:
"""textContent を用いた軽量な長さ計測。"""
try:
return int(
driver.execute_script(
"const n=arguments[0]; return (n.textContent||'').trim().length;", el
)
or 0
)
except Exception:
return 0
def inner_text(driver, el) -> str:
"""最終取得用の innerText(改行やCSS影響を反映)。"""
try:
return (
driver.execute_script(
"const n=arguments[0]; return (n.innerText||'').trim();", el
)
or ""
)
except Exception:
return ""
def wait_dom_idle(driver, el, idle_ms=900, timeout_ms=4000):
"""
MutationObserver で DOM 変化が idle_ms 停止するのを待つ(任意)。
Selenium の execute_async_script を利用。
"""
script = """
const [target, idle, limit, cb] = [arguments[0], arguments[1], arguments[2], arguments[arguments.length-1]];
let done = false, timer = null; const started = Date.now();
let watchdog = null; // 先に宣言してTDZ回避
const finish = () => {
if (!done) {
done = true;
if (watchdog) clearInterval(watchdog);
obs.disconnect();
cb(true);
}
};
const obs = new MutationObserver(() => { clearTimeout(timer); timer = setTimeout(finish, idle); });
obs.observe(target, {subtree: true, childList: true, characterData: true});
timer = setTimeout(finish, idle);
watchdog = setInterval(() => { if (Date.now() - started > limit) { clearInterval(watchdog); finish(); } }, 120);
"""
try:
driver.execute_async_script(script, el, int(idle_ms), int(timeout_ms))
except Exception:
pass
# ---------------------------
# XPath リテラル安全化
# ---------------------------
def _xpath_literal(s: str) -> str:
"""
XPath内で安全に文字列リテラルを生成する。
- 単引号のみ → '...'
- 二重引用符のみ → "..."
- 両方含む → concat('a', "\"", 'b', ...)
"""
if "'" not in s:
return f"'{s}'"
if '"' not in s:
return f'"{s}"'
parts = s.split("'")
return "concat(" + ", \"'\", ".join(f"'{p}'" for p in parts) + ")"
# ---------------------------
# アコーディオン特定 & 展開
# ---------------------------
def find_accordion_container_by_heading(driver, title_text: str):
"""
見出し(h1/h2/h3 の normalize-space 含む)で section 形式のアコーディオンを探す。
見つからなければ <details><summary> の形式も探して返す。
"""
t = _xpath_literal(title_text)
# 1) section + 見出し
xpath1 = (
f"//section[contains(@class,'accordion')]"
f"[.//*[self::h1 or self::h2 or self::h3][contains(normalize-space(), {t})]]"
)
# 2) <details> + <summary>
xpath2 = f"//details[.//summary[contains(normalize-space(), {t})]]"
wait = WebDriverWait(driver, WAIT_SEC)
try:
return wait.until(EC.presence_of_element_located((By.XPATH, xpath1)))
except TimeoutException:
return wait.until(EC.presence_of_element_located((By.XPATH, xpath2)))
def ensure_section_expanded(driver, container):
"""
アコーディオンが閉じていれば開く。
- <details> なら open 属性
- それ以外は aria-expanded / .accordion-toggle / [aria-controls] など汎用トグル
"""
tag = (container.tag_name or "").lower()
if tag == "details":
if container.get_attribute("open") is None:
summary = container.find_element(By.CSS_SELECTOR, "summary")
safe_click(driver, summary)
return
# 汎用トグル
toggle = None
for sel in (".accordion-toggle", "[aria-expanded]", "[aria-controls]"):
try:
toggle = container.find_element(By.CSS_SELECTOR, sel)
break
except NoSuchElementException:
continue
if toggle is None:
# 最低限:container 内の button を当てる(要調整)
try:
toggle = container.find_element(By.CSS_SELECTOR, "button")
except NoSuchElementException:
return
expanded = (toggle.get_attribute("aria-expanded") or "").lower() in ("true", "1")
if not expanded:
safe_click(driver, toggle)
# ---------------------------
# 観測ループ(安定化判定)
# ---------------------------
def text_after_stabilized_scrolls(
driver,
el,
cycles=6,
pause=0.35,
container_scroll_rounds=4,
length_stable_hits=2,
use_dom_idle=False,
) -> str:
"""
“スクロール → 長さ計測”を繰り返し、テキスト長が連続で変わらなくなるまで待つ。
仮想リストや遅延描画で増える本文を観測的に取り切る。
"""
prev_len, stable = -1, 0
for _ in range(max(1, cycles)):
scroll_element_to_bottom(
driver,
el,
rounds=container_scroll_rounds,
pause=pause / 2,
stable_hits=2,
mode="step",
)
cur_len = _text_len_fast(driver, el)
if cur_len == prev_len:
stable += 1
else:
stable = 0
prev_len = cur_len
if use_dom_idle:
wait_dom_idle(driver, el, idle_ms=int(pause * 1000), timeout_ms=3000)
if stable >= length_stable_hits:
break
return inner_text(driver, el) # 最後だけ innerText で確定取得
def get_section_text(
driver,
title_text: str,
*,
wait_visible_timeout=20,
window_scroll_rounds=4,
stabilize_cycles=6,
length_stable_hits=2,
use_dom_idle=False,
) -> str:
"""
指定見出しのアコーディオン本文を“取りこぼし最小化”で抽出する高水準関数。
Stale要素は軽くリトライする。
"""
attempts = 2
text_best = ""
for _ in range(attempts):
try:
container = find_accordion_container_by_heading(driver, title_text)
# 展開(閉じていれば開く)
ensure_section_expanded(driver, container)
# ページ全体の遅延描画を進める
scroll_window_to_bottom(driver, rounds=window_scroll_rounds, pause=0.35)
# container の可視化を軽く確認(不安定なら続行)
try:
WebDriverWait(driver, wait_visible_timeout).until(
lambda d: container.is_displayed()
)
except TimeoutException:
pass
# 観測 1 回目
t1 = text_after_stabilized_scrolls(
driver,
container,
cycles=stabilize_cycles,
length_stable_hits=length_stable_hits,
use_dom_idle=use_dom_idle,
)
# 観測 2 回目(末尾合わせ)
driver.execute_script(
"arguments[0].scrollIntoView({block:'end'});", container
)
time.sleep(0.2)
t2 = text_after_stabilized_scrolls(
driver,
container,
cycles=max(2, stabilize_cycles // 2),
length_stable_hits=length_stable_hits,
use_dom_idle=use_dom_idle,
)
text_best = t2 if len(t2) > len(t1) else t1
break # 成功
except StaleElementReferenceException:
# container が差し替わった場合は 1 回だけ取り直す
time.sleep(0.2)
continue
return text_best使い方(短例)
from selenium import webdriver
driver = webdriver.Chrome() # 既存環境ならこの1行で十分
driver.set_script_timeout(10)
text = get_section_text(
driver,
title_text="仕様",
window_scroll_rounds=4,
stabilize_cycles=6,
length_stable_hits=2,
use_dom_idle=True,
)
print(text)よくある失敗:「innerTextが空」「一部しか取れない」を潰す
- window だけをスクロールしている
独立スクロール領域では本文が増えません。
テンプレはコンテナ自身の scrollTop 操作と段階スクロールを組み合わせています。 - 見た目は開いたのにテキストが空
展開直後は未描画が多いです。
テンプレは全体スクロール → コンテナ段階スクロール → 安定化判定の三段で取り切ります。 - 仮想リストで一度に出ない
連続してテキスト長が変化しないことを“安定”とみなし、必要ならDOMアイドル待機も併用します。 - StaleElementReferenceException
スクロールや再描画で要素が差し替わるUIがあります。テンプレは軽いリトライで再取得します。
\ MutationObserver/IntersectionObserverの仕組みを押さえると
”なぜ空になるか”が腑に落ちます /
発展トピック:クリック遮蔽対策とパフォーマンス最適化
- クリック遮蔽への備え
固定ヘッダーや透明オーバーレイでクリックが遮られることがあります。safe_clickの JSクリック フォールバックを維持し、必要に応じてdocument.elementFromPointで直下要素を確認してください。 - パフォーマンス
安定化判定はtextContentで軽量化し、最終取得だけinnerTextにしています。
描画直後の揺らぎにはrequestAnimationFrameを数回(上の二重rAF待ち)挟むと追従が良くなります。
\ ヘッドレス実行やスケール運用に。 /
チェックリスト(導入前の最終確認)
- 展開判定は
<details open>またはaria-expandedを使っている - 全体スクロール → コンテナ段階スクロール → 安定化判定の流れを実装した
- 安定化はテキスト長の連続不変で判定し、必要に応じて DOMアイドル待機を併用した
- Stale の軽いリトライを入れた
- セレクタとXPathは汎用例を出発点に、実DOMへ最小変更で合わせた
まとめ:アコーディオン本文を確実に取り切る
アコーディオンUIの本文抽出を「開いたら即 innerText」で済ませるのは、遅延描画・独立スクロール・仮想リストの前では脆弱です。
本稿のテンプレは、段階スクロールと安定化判定、必要に応じたDOMアイドル待機で、観測ベースに取り切る設計です。
DOM構造やCSSが違っても、ロジックはそのまま、セレクタの粒度だけを調整すれば使い回せます。

