Python×Gemini APIでスクレイピング → JSON化に挑戦したらハマった話3(画面外の動的ボタンとフローティングメニューの重なりを対処し、Gemini APIに繋ぐまで)
1. はじめに
バックエンド開発もいよいよ一区切りです。前回までに「データ取得」「データ選別」「JSON化」の役割を完全に分離する美しいオブジェクト指向設計(単一責任の原則)を確立し、バックエンドの骨組みは完成しました。
今回は、実際のWebサイト(今回はマリンメッセ福岡のイベントスケジュールページ)を相手に、Seleniumを使った実践的なスクレイピングを実装していく中で遭遇した、数々の凄惨なエラーと対処法をまとめます。
2. 初期DOMにデータが存在しない問題
今回ターゲットにしたページは、初期状態では直近のイベントしか表示されておらず、お目当てのアーティスト(数ヶ月先の公演)の情報が画面(HTML)に存在しない仕様でした。画面下部にある「さらに10件を読み込む」という動的な青いボタンを数回クリックし、DOMを拡張しなければデータを得られません。
そこで、データを取得する前にボタンを複数回クリックするロジックを組み込むことにしました。
最初に書いた直感的なコード(罠への入り口)
for i in range(2):
button = driver.find_element(By.CSS_SELECTOR, '.button.sd.appear').click()
初めはこれでボタンを2回クリックできると思っていました。
一瞬の連打による描画未完了エラー
ブラウザ上でボタンをクリックすると、裏側でJavaScriptが走り、API通信を行って新しいデータを画面にレンダリングします。しかし、上記のコードでは1回目のクリックが終わったミリ秒後に2回目のループ(クリック)に突入してしまいます。
画面の描画が追いついていないため、ボタンが一瞬消失したり、無効化されたりしてエラーが出てきました。見たことのないエラーで、これはAI君を使ってしまいましたねー。
【解決策】
ハッカソンのスピード感と確実性を考慮し、1回クリックするごとに time.sleep(5) の待機処理を導入しました。これにより、JavaScriptによるデータ読み込みを確実に完了させてから、次のアクションへ移る安定性を確保しました。
3. ElementClickInterceptedException
連打問題をクリアし、「これでいける!」と思ったのですが...。
selenium.common.exceptions.ElementClickInterceptedException: Message: element click intercepted: Element <button ... class="button sd appear">...</button> is not clickable at point (706, 906). Other element would receive the click: <a ... href="/messe#seating-chart" class="link sd appear">...</a>
エラーの原因:ヘッダーメニューなどの常に表示されるボタン
Seleniumが自動スクロールした結果、「さらに読み込む」ボタンが、常に画面最下部の固定メニューバーの真裏に隠れてしまうという現象が発生したのです。人間には見えていても、ブラウザの内部的には別のボタンが上に被さっているため、ボタンを狙い撃ちしたつもりが固定メニューを誤クリックしてしまい、例外を吐いて落ちていたのでした。
4. JavaScriptで要素を「画面中央」に引きずり出す
この物理的な重なりを回避するため、通常のSeleniumによるクリックを諦め、JavaScriptをブラウザ側で直接実行してスクロール位置をコントロールするというアプローチを採用しました。
ターゲットのボタンを見つけた後、ブラウザに対して以下のJavaScriptを送り込みます。
# ボタンが画面に現れ、かつクリック可能になるまで安全に待つ
button = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, '.button.sd.appear'))
)
# 第2引数を基準に中央にスクロール
driver.execute_script("arguments[0].scrollIntoView({block:'center'});", button)
button.click()
time.sleep(5)
scrollIntoView({block: 'center'}) を使うことで、ターゲットのボタンを固定メニューに被らない画面のど真ん中まで一気にスクロールさせてから、安全確実にクリックを発動させることができるようになりました。
5. Gemini APIの Blob 罠
ついに全イベントデータをリスト型(配列)として得ることができたのですが、この選別されたリストをそのままGemini API(gemini-2.5-flash)に放り込んだところ、以下のエラーが出てきました。
TypeError: Could not create `Blob`, expected `Blob`, `dict` or an `Image` type... Got a: <class 'list'> Value: [...]
原因:AIは「リスト構造」をそのまま理解できないらしい
Gemini APIの generate_content メソッドに、Pythonのリスト型(['イベント1', 'イベント2', ...])をそのまま渡すと、API側が「これはテキストではなく、画像や音声などのバイナリデータ(Blob)か?」と勘違いしてしまい、型ミスマッチでエラーを起こします。
AIに渡すべきは、構造を持たない純粋な「1つの長い文字列」である必要があります。
【解決策】
APIを叩く直前で、競技プログラミングなどでもお馴染みの配列結合メソッドを使い、リストを改行コードで1つの巨大なテキストにガッチャンコしました。
# リスト型から、改行区切りの1つの長い文字列(String)へ変換
text = "\n".join(filteredText)
# 文字列に変えたデータをプロンプトと共にAPIへ送信
response_gemini = model.generate_content([prompt, text])
6. 上手くいった!
すべての修正を施し、APIキーを環境にセットして、いざ実行。
python Main.py
カタカタカタ……と裏でEdgeブラウザが自動で立ち上がり、画面がど真ん中にスクロールし、「さらに読み込む」ボタンを綺麗に2回クリック。データが完全に露出した瞬間に全テキストを回収してブラウザが終了。
そのミリ秒後、選別されたデータがGemini APIへと送信され、ターミナルに叩き出されたのがこちらの美しい100%純粋な構造化JSONです!
[
{
"date": "2026",
"artist": "ヨルシカ",
"title": "LIVE TOUR 2026「一人称」"
},
{
"date": "2026",
"artist": "緑黄色社会",
"title": "アリーナツアー2026"
}
]
マークダウンの余計な解説文を一切含まない、プログラムがそのまま即座にパースできる完璧な配列型のJSONが返ってきた瞬間、すごい達成感でした!
APIのエラーハンドリングから始まり、Seleniumの非同期の壁、CSSセレクターの罠、UIの重なり、そしてAPIの型問題まで、すべての壁を論理的に切り分けてデバッグし、1から100まで(いくつかギブアップしたものもあったので、実質80ぐらい)自分のコードでコントロールできた時ことは、ほんの少し自信になりました。
7. まとめと次
今回学んだ最大の教訓は、**「どれだけフロントのUIが複雑に変化し、Selenium側でのループやポップアップの切り替えが増えようとも、最終的にスクレイパーの中で1つのリストに集約してから最後に1回だけAPIに投げれば、Gemini APIの消費量は常に1回分で済む」**という設計思想です。役割を綺麗に分離していたからこそ、各クラスを一切汚さずに修正を施せました。
次は「PayPayドーム」が立ちはだかります。
「年度ボタンの選択」「1〜12月という大量のボタンの個別クリック」「クリックするたびに前月のデータが消えるポップアップ仕様」という、マリンメッセの数倍複雑なDOM構造を持つ要塞です。