今日は、2026年のエンジニアらしく、「WebページをAIに”視覚的に”読ませてデータを引っこ抜く」 次世代スクレイピング術を検証してみたよ!
政府統計の「e-Stat」をターゲットに、Pythonコードで完全攻略していくからついてきて!
💥 なぜ今「視覚的(Visual)」なのか?
ReactとかTailwind CSSが普及したせいで、最近のWebサイトのHTMLって人間が読む用じゃないよね。
クラス名はランダム文字列だし、構造はネスト地獄。メンテナンスコストが半端ない!😡
- マルチモーダル: スクショを見て「あ、これExcelのアイコンだ」って人間と同じ認知ができる。
- 爆安コスト: Flashモデルなら、エンジニアが正規表現を書く人件費よりAPI代の方が安い。
- ロバスト性: デザインが崩れても「見た目」が変わらなければ動く。
これ、試さない理由なくない?
🛠 検証構成: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使うとお高いんでしょ?」
ノンノン!☝️
なんと 1日あたりの無料枠(Free Tier) があるんだよ!
※2026年1月現在、1日20回程度のリクエストなら無料で試せる枠があることが多いよ!(正確な最新情報は公式サイトを見てね!)
つまり、個人開発や小規模なデータ収集なら、実質タダで運用できる可能性が高いってこと。
有料枠になったとしても、1画像あたり数円レベル。
「サイトの仕様変更で深夜に対応させられるエンジニアの残業代」と比べたら、実質無料みたいなもんじゃない?💸
📝 まとめ:AIに「見させる」時代が来た
CSSセレクタと正規表現で戦う時代は終わったよ。
これからは、「人間が見ている画面を、そのままAIに渡す」 のが最適解。
PythonとGeminiがあれば、どんな複雑なサイトも怖くない!
みんなも UnicodeDecodeError には気をつけつつ、良きスクレイピングライフを!👋
国内のAI狂いでした!またね〜!🐍💎






