脱CSSセレクタ!Gemini 3 Flash × Pythonで実現する「視覚的」スクレイピング完全ガイド【メンテ不要】

本ページはプロモーションが含まれています
【Python】Gemini 3 Flashで「視覚的」スクレイピングしたら、CSSセレクタ地獄から解放された話 〜e-Stat完全攻略〜
国内のAI狂い
やっほー!国内のAI狂いだよ!✨
みんな、スクレイピングしてる? え、まだ BeautifulSoupclass="item_title_xyz123" みたいな謎クラス名を探して消耗してるの?
それ、もう「Gemini 3 Flash」 にスクショ投げつけたら解決するかもよ?🐍💎

今日は、2026年のエンジニアらしく、「WebページをAIに”視覚的に”読ませてデータを引っこ抜く」 次世代スクレイピング術を検証してみたよ!
政府統計の「e-Stat」をターゲットに、Pythonコードで完全攻略していくからついてきて!

💥 なぜ今「視覚的(Visual)」なのか?

ReactとかTailwind CSSが普及したせいで、最近のWebサイトのHTMLって人間が読む用じゃないよね。
クラス名はランダム文字列だし、構造はネスト地獄。メンテナンスコストが半端ない!😡

💡 Gemini 3 Flash なら解決できる!
  1. マルチモーダル: スクショを見て「あ、これExcelのアイコンだ」って人間と同じ認知ができる。
  2. 爆安コスト: Flashモデルなら、エンジニアが正規表現を書く人件費よりAPI代の方が安い。
  3. ロバスト性: デザインが崩れても「見た目」が変わらなければ動く。

これ、試さない理由なくない?

🛠 検証構成:Python × Gemini × Playwright

今回は「完全自動化」を目指して、こんな構成にしたよ!

  • 言語: Python 3.13 (最新!)
  • ブラウザ操作: Playwright (Seleniumより速い!)
  • AIモデル: Gemini 3 Flash Preview (視覚認識用)
  • データ構造化: Pydantic (AIにJSONを強制出力させる神ツール)

🚧 【悲報】検証中に踏み抜いた「地雷」たち(一次情報)

はい、ここからが本題。
技術記事によくある「すんなり動きました✨」なんて嘘は言わないよ!
私が今回の検証で盛大に爆死したリアルな失敗談を共有するね。みんなは同じ轍を踏まないで!😭

1. Windowsの「文字コード」トラップ

.env ファイルにAPIキーを入れて実行したら、まさかの UnicodeDecodeError
犯人は Shift-JIS でした。Windowsのメモ帳で保存すると勝手にANSIになるの、いい加減にして?!

対策: Python側で load_dotenv(encoding="utf-8") を明示する&ファイルもUTF-8で保存!これ絶対!

2. Google GenAI SDKの「サイレント仕様変更」

Geminiの新SDK (google-genai)、開発スピードが速すぎてドキュメントと挙動が違うことが…w
エラーログ:Part.from_text() takes 1 positional argument

対策: from_text() ヘルパーを使わず、types.Part(text=prompt) で直接オブジェクトを作るのが一番確実だったよ!

3. Python 3.12+ で「ライブラリ大量死」事件

グラフ化に使おうとした japanize-matplotlib が、エラー吐いて死にました。
原因は、Python 3.12から標準ライブラリ distutils が削除されたこと。古いライブラリはこれに依存してたりするんだよね…。

対策: 外部ライブラリに頼るのをやめて、platform モジュールでOSを判定し、標準フォント(MS Gothicなど)を直接指定するロジックを自作!これで依存関係フリー!🐍✨

💻 完成した「完全自動」スクリプト解説

数々の屍を越えて完成したのが、この all_in_one_scraper.py だよ!
「環境構築」から「グラフ化」まで、ワンクリックで終わらせる “全部入り” スクリプト。

ポイントを解説するね!

🤖 1. 面倒な環境構築を自動化

Python初心者あるある「ライブラリが入ってなくて動かない」を撲滅するために、スクリプト冒頭で pip install を自動実行するようにしたよ。

# 必要なライブラリを片っ端からチェック&インストール
# japanize-matplotlib は Python 3.12+ で死ぬのでリストラしました🙏
REQUIRED_PACKAGES = [
    ("playwright", "playwright"),
    ("google-genai", "google.genai"),
    ("pydantic", "pydantic"),
    # ...他
]

👁 2. Geminiに「型」を強制する

AIスクレイピングで一番怖い「ハルシネーション(幻覚)」を防ぐために、Pydanticでガチガチに型定義してるよ。
これで format_type に勝手な文字列が入るのを防げる!

class StatItem(BaseModel):
    title: str = Field(..., description="統計調査の名称や表のタイトル。")
    # アイコンから形式を推測させる
    format_type: Optional[str] = Field(None, description="データの形式(例: 'DB', 'Excel', 'PDF', 'CSV')。")

📝 3. 全ソースコード

これを all_in_one_scraper.py として保存して、.env にAPIキーを入れるだけで動くよ!

🖱️ クリックしてソースコードを展開する
import subprocess
import sys
import os
import time
import json
import base64
import asyncio
import importlib
import platform
from typing import List, Optional

# ==========================================
# 0. 自動セットアップ・パート (面倒な作業を全部やる)
# ==========================================
def install_and_import(package_name, import_name=None):
    if import_name is None:
        import_name = package_name
    try:
        importlib.import_module(import_name)
    except ImportError:
        print(f"🔧 {package_name} が見つからないからインストールするね... (ちょっと待ってて!)")
        try:
            subprocess.check_call([sys.executable, "-m", "pip", "install", package_name])
            print(f"✅ {package_name} のインストール完了!")
        except Exception as e:
            print(f"🚨 {package_name} のインストールに失敗しちゃった...: {e}")
            sys.exit(1)

# 必要なライブラリを片っ端からチェック&インストール
# japanize-matplotlib は Python 3.12+ で死ぬのでリストラしました🙏
REQUIRED_PACKAGES = [
    ("playwright", "playwright"),
    ("google-genai", "google.genai"),
    ("pydantic", "pydantic"),
    ("python-dotenv", "dotenv"),
    ("pandas", "pandas"),
    ("matplotlib", "matplotlib")
]

print("🚗 自動セットアップ開始!シートベルト締めてね!")
for pkg, imp in REQUIRED_PACKAGES:
    install_and_import(pkg, imp)

# Playwrightのブラウザバイナリも確認してインストール
try:
    print("🌍 Playwrightブラウザの確認中...")
    subprocess.run([sys.executable, "-m", "playwright", "install", "chromium"], check=True)
except Exception as e:
    print(f"⚠️ ブラウザインストールでエラー出たけど、とりあえず進めるね: {e}")

# ここから本番のインポート
from playwright.async_api import async_playwright
from pydantic import BaseModel, Field
from google import genai
from google.genai import types
from dotenv import load_dotenv
import pandas as pd
import matplotlib.pyplot as plt

# 環境変数の読み込み
load_dotenv(encoding="utf-8")

# ==========================================
# 日本語フォント設定 (Python 3.12+ 対応版)
# ==========================================
def configure_japanese_font():
    system = platform.system()
    if system == "Windows":
        font_family = "MS Gothic"
    elif system == "Darwin": # Mac
        font_family = "AppleGothic"
    else: # Linux/Other
        font_family = "Noto Sans CJK JP" # 入ってないかもだけど一応

    try:
        plt.rcParams['font.family'] = font_family
        print(f"🎨 日本語フォントを '{font_family}' に設定したよ!")
    except Exception as e:
        print(f"⚠️ フォント設定に失敗しちゃった (文字化けするかも): {e}")

configure_japanese_font()

# ==========================================
# 1. 定義パート (Geminiに見させる型)
# ==========================================
class StatItem(BaseModel):
    title: str = Field(..., description="統計調査の名称や表のタイトル。")
    ministry: Optional[str] = Field(None, description="担当している府省名(例: 総務省, 厚生労働省)。")
    release_date: Optional[str] = Field(None, description="公表日や更新日。")
    description: Optional[str] = Field(None, description="統計の簡単な説明や、'新着' '更新' などのバッジ情報。")
    format_type: Optional[str] = Field(None, description="データの形式(例: 'DB', 'Excel', 'PDF', 'CSV')。アイコンやリンク文字から推測。")

class StatList(BaseModel):
    items: List[StatItem] = Field(..., description="ページ内で検出された統計情報のリスト")

# ==========================================
# 2. スクレイピング・クラス (Playwright + Gemini)
# ==========================================
class VisualScraper:
    def __init__(self, api_key: str, model_name: str = "models/gemini-3-flash-preview"):
        self.client = genai.Client(api_key=api_key)
        self.model_name = model_name
        print(f"✨ VisualScraper Initialized with {model_name} ✨")

    async def fetch_page_data(self, url: str):
        print(f"🌍 Accessing URL: {url} ...")
        async with async_playwright() as p:
            browser = await p.chromium.launch(headless=True)
            page = await browser.new_page(viewport={"width": 1280, "height": 1200})
            try:
                await page.goto(url, wait_until="networkidle", timeout=60000) # タイムアウト長めに
                await page.evaluate("window.scrollTo(0, 500)")
                await asyncio.sleep(3) 
                screenshot_bytes = await page.screenshot(full_page=False)
                screenshot_b64 = base64.b64encode(screenshot_bytes).decode("utf-8")
                text_content = await page.inner_text("body")
                print("📸 Screenshot captured & Text extracted!")
                return screenshot_b64, text_content
            except Exception as e:
                print(f"❌ Browser Error: {e}")
                return None, None
            finally:
                await browser.close()

    def analyze_with_gemini(self, screenshot_b64: str, text_content: str) -> Optional[StatList]:
        print("💎 Sending data to Gemini...")
        system_instruction = """
        あなたは政府統計データの専門アナリストです。
        提供されたWebページの「スクリーンショット」と「テキスト情報」をもとに、
        主要な統計情報(新着情報やランキングなど)を抽出し、指定されたJSON形式で出力してください。
        視覚的に認識できるアイコン(Excelなど)の情報も重要です。
        """
        prompt = "このページに表示されている統計データをリストアップしてください。"
        
        try:
            response = self.client.models.generate_content(
                model=self.model_name,
                contents=[
                    types.Content(
                        role="user",
                        parts=[
                            types.Part(text=prompt),
                            types.Part(
                                inline_data=types.Blob(
                                    mime_type="image/png",
                                    data=base64.b64decode(screenshot_b64)
                                )
                            )
                        ]
                    )
                ],
                config=types.GenerateContentConfig(
                    system_instruction=system_instruction,
                    response_mime_type="application/json",
                    response_schema=StatList,
                    temperature=0.1
                )
            )
            if response.text:
                return StatList(**json.loads(response.text))
            return None
        except Exception as e:
            print(f"🔥 Gemini API Error: {e}")
            return None

# ==========================================
# 3. 可視化パート (Pandas + Matplotlib)
# ==========================================
def visualize_results(json_file: str, output_dir: str):
    if not os.path.exists(json_file):
        return

    print("📊 グラフを作成中...")
    with open(json_file, "r", encoding="utf-8") as f:
        data = json.load(f)
    
    df = pd.DataFrame(data["items"]) if "items" in data else pd.DataFrame(data)
    os.makedirs(output_dir, exist_ok=True)

    # 省庁ランキング
    if "ministry" in df.columns:
        plt.figure(figsize=(10, 6))
        ministry_counts = df["ministry"].dropna().value_counts()
        if not ministry_counts.empty:
            ministry_counts.sort_values().plot(kind="barh", color="skyblue", edgecolor="black")
            plt.title("省庁別:統計データ公表数ランキング", fontsize=16)
            plt.tight_layout()
            plt.savefig(os.path.join(output_dir, "ministry_ranking.png"))
            print("📈 省庁ランキング保存完了!")

    # ファイル形式円グラフ
    if "format_type" in df.columns:
        plt.figure(figsize=(8, 8))
        format_counts = df["format_type"].dropna().value_counts()
        if not format_counts.empty:
            format_counts.plot(kind="pie", autopct="%1.1f%%", startangle=90, cmap="Pastel1")
            plt.title("統計データのファイル形式内訳", fontsize=16)
            plt.ylabel("")
            plt.savefig(os.path.join(output_dir, "format_pie_chart.png"))
            print("🍕 円グラフ保存完了!")

# ==========================================
# メイン実行 (指揮者)
# ==========================================
async def main():
    TARGET_URL = "https://www.e-stat.go.jp/"
    JSON_FILE = "estat_result.json"
    GRAPH_DIR = "graphs"

    api_key = os.getenv("GEMINI_API_KEY")
    if not api_key:
        # キーがない場合、入力させる親切設計
        print("\n🚨 GEMINI_API_KEY が .env に見つかりません!")
        api_key = input("🔑 ここにAPIキーを直接貼り付けてEnter押してね: ").strip()
        if not api_key:
            print("終了します...")
            return

    # 1. スクレイピング実行
    scraper = VisualScraper(api_key=api_key)
    screenshot, text = await scraper.fetch_page_data(TARGET_URL)
    
    if screenshot:
        result = scraper.analyze_with_gemini(screenshot, text)
        if result:
            with open(JSON_FILE, "w", encoding="utf-8") as f:
                f.write(result.model_dump_json(indent=2, ensure_ascii=False))
            print(f"\n💾 データ取得成功: {len(result.items)}件のデータを {JSON_FILE} に保存したよ!")
            
            # 2. グラフ化実行
            visualize_results(JSON_FILE, GRAPH_DIR)
            print(f"\n🎉 完全勝利! {GRAPH_DIR} フォルダを確認してね!")
        else:
            print("☁️ Geminiの解析に失敗しちゃった...")
    else:
        print("💀 ページが開けなかったよ...")

if __name__ == "__main__":
    asyncio.run(main())

📊 結果:e-Statを「視覚」で攻略できたのか?

論より証拠!
スクリプトを実行して 19秒 で取得できたデータがこれだ!

取得できた構造化データ (JSON)

見てこの精度!HTML上は画像アイコンだった「DB」や「ファイル」という情報が、ちゃんとテキストデータとして認識されてる!
しかも、ページ下部の「ランキング」エリアもバッチリ区別されてるよ。

{
  "items": [
    {
      "title": "造船造機統計調査",
      "ministry": "国土交通省",
      "release_date": "2026-01-09",
      "description": "新着",
      "format_type": "ファイル"
    },
    {
      "title": "地方公共団体の勤務条件等に関する調査",
      "ministry": "総務省",
      "release_date": "2026-01-09",
      "description": "新着",
      "format_type": "ファイル"
    },
    {
      "title": "景気動向指数",
      "ministry": "内閣府",
      "release_date": "2026-01-09",
      "description": "新着",
      "format_type": "DB, ファイル"
    },
    {
      "title": "小売物価統計調査",
      "ministry": "総務省",
      "release_date": "2026-01-09",
      "description": "新着",
      "format_type": "ファイル"
    },
    {
      "title": "家計調査 家計収支編(2025年11月分 二人以上の世帯)",
      "ministry": "総務省",
      "release_date": "2026-01-09 08:30",
      "description": "公表予定",
      "format_type": null
    },
    {
      "title": "国勢調査",
      "ministry": null,
      "release_date": null,
      "description": "ランキング1位",
      "format_type": null
    },
    {
      "title": "アイスクリーム",
      "ministry": null,
      "release_date": null,
      "description": "ランキング2位",
      "format_type": null
    },
    {
      "title": "人口",
      "ministry": null,
      "release_date": null,
      "description": "ランキング3位",
      "format_type": null
    },
    {
      "title": "人口推計",
      "ministry": null,
      "release_date": null,
      "description": "ランキング4位",
      "format_type": null
    },
    {
      "title": "経済センサス",
      "ministry": null,
      "release_date": null,
      "description": "ランキング9位",
      "format_type": null
    }
  ]
}

📈 生成されたインサイト(グラフ)

JSONデータから自動生成されたグラフがこちら。
一瞬で「どの省庁がアクティブか」「どんな形式で公開されているか」が可視化されたよ!

省庁別統計データ公表数ランキング
▲ 総務省と厚労省が活発なのが一目瞭然!
統計データのファイル形式内訳
▲ まだまだ「ファイル」形式が主流みたいだね…

💰 気になるお値段(コスト試算)

「でも、AI使うとお高いんでしょ?」
ノンノン!☝️

💸 Gemini 3 Flash のコスト感

なんと 1日あたりの無料枠(Free Tier) があるんだよ!
※2026年1月現在、1日20回程度のリクエストなら無料で試せる枠があることが多いよ!(正確な最新情報は公式サイトを見てね!)

つまり、個人開発や小規模なデータ収集なら、実質タダで運用できる可能性が高いってこと。
有料枠になったとしても、1画像あたり数円レベル。
「サイトの仕様変更で深夜に対応させられるエンジニアの残業代」と比べたら、実質無料みたいなもんじゃない?💸

📝 まとめ:AIに「見させる」時代が来た

CSSセレクタと正規表現で戦う時代は終わったよ。
これからは、「人間が見ている画面を、そのままAIに渡す」 のが最適解。

PythonとGeminiがあれば、どんな複雑なサイトも怖くない!
みんなも UnicodeDecodeError には気をつけつつ、良きスクレイピングライフを!👋

国内のAI狂いでした!またね〜!🐍💎

よかったらシェアしてね!
  • URLをコピーしました!
目次