1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

2026年7月28日、熊本県熊本地方を震源とする地震が相次ぎました。気象庁の発表によると、同日19時03分頃の地震は深さ10km、マグニチュード4.2、最大震度5弱(速報値)でした。

地震情報の一覧を眺めていて、「どの時間帯に情報が集中しているのだろう」と気になりました。1件ずつならWebページを見るだけで十分ですが、時間帯別に数えようとすると、まず一覧を表にしなければなりません。

最初はPythonだけで取得することも考えました。ただ、今回はスクレイピング部分をOctoparseに任せ、CSVにした後の整形と可視化をpandas/Matplotlibで行うことにしました。

やることは次の4つです。

  1. Webページ上の繰り返しデータを抽出する
  2. 画像属性を含むデータをCSVに整形する
  3. pandasで欠損値や表記揺れを処理する
  4. 時系列・分布・カテゴリ別件数を可視化する

重要

これは地震を予測する試みではありません。災害時の判断には、気象庁や自治体などの公式情報を利用してください。また、スクレイピング前に対象サイトの利用規約・robots.txt等を確認し、アクセス間隔と取得範囲を適切に制限してください。

完成イメージ

最終的に、次の3種類のグラフを作ります。

  • 時間帯別の地震情報件数
  • マグニチュードの分布
  • 震源地別の件数

処理の流れは以下です。

地震情報の一覧ページ
        ↓
Octoparseで一覧を抽出
        ↓
CSVとしてエクスポート
        ↓
pandasで整形・対象地域を抽出
        ↓
Matplotlibで可視化

使用する環境

手元の検証環境は次のとおりです。

  • Octoparse 8系
  • Python 3.12
  • pandas
  • Matplotlib
  • japanize-matplotlib

Python側のライブラリをインストールします。

python -m pip install pandas matplotlib japanize-matplotlib

Octoparseの画面名やボタン位置は、利用しているバージョンによって異なる場合があります。

データについて

取得対象には、日本気象協会 tenki.jpの過去の地震情報を使用しました。

一覧には、主に以下の項目があります。

項目
発生時刻 2026年07月29日12時08分頃
震源地 熊本県熊本地方
マグニチュード M3.9
最大震度 3
詳細URL 地震ごとの詳細ページURL

サイト全体を集める必要はないので、対象は直近3ページに絞りました。実行は1回だけにし、ページ遷移には待機時間を設定します。

なお、Webの地震情報一覧は「Webページに掲載された情報」の集計に適していますが、研究・防災用途の完全な地震カタログとは限りません。観測条件や掲載基準を含めた厳密な分析が必要な場合は、気象庁が提供する一次データを使用してください。

Octoparseで地震情報を取得する

1. 新しいタスクを作成する

Octoparseを起動し、「新規作成」からカスタムタスクを作成します。

入力するURLは以下です。

https://earthquake.tenki.jp/bousai/earthquake/entries/

ページの読み込み後に自動検出を実行します。Octoparseの自動検出は、ページ内の類似要素やページネーションを認識してワークフローの候補を作成できます。

image.png

2. 地震情報の繰り返しリストを選択する

自動検出されたプレビューで、地震情報の各行が繰り返しデータとして選択されていることを確認します。

image.png

自動検出がうまくいかない場合は、一覧内の異なる2行を順番に選択し、「すべて選択」またはリスト作成に相当する操作を選びます。

抽出フィールドは次の5つに整理します。

フィールド名 取得する値
発生時刻 発生時刻のテキスト
震源地 震源地のテキスト
マグニチュード Mを含むテキスト
最大震度 震度画像のalt属性
詳細URL 発生時刻リンクのURL
画像URL 震度画像リンクのURL

3. 最大震度で少し詰まった

最大震度は普通のテキストとして取れるだろうと思っていました。ところが、画面上では数字に見えても、ページ上では画像として表示されています。ここをテキスト抽出にしてしまうと、期待した値は入りません。

画像URLを取って後から判定する方法もありますが、今回は画像要素の alt属性を抽出する方が簡単でした。

Octoparseで最大震度の画像を選び、抽出するデータの種類を「属性」に変更して、属性名にaltを指定します。

プレビューが次のようになれば成功です。

1
2
3
4
5弱
5強

空欄や画像URLになっている場合は、フィールドの抽出設定を再確認します。

4. 詳細ページのURLを取得する

発生時刻には各地震の詳細ページへのリンクが設定されています。
image.png

発生時刻を選択し、テキストとは別にリンク先URLを抽出します。URLは、重複を除くときの識別子や、元データを確認するときに利用できます。

5. ページネーションを設定する

ページ下部の「次へ」に相当するリンクを選択し、ページネーションループを作成します。

欲しいのは分析用の小さなデータセットだけなので、ループ回数は 3回に制限しました。また、連続アクセスを避けるため、ページを開く処理に数秒の待機時間を設定します。

設定後のワークフローは、おおむね次の構造になります。

ページを開く
└─ ページネーション
   └─ リストをループ
      └─ データを抽出

いきなり3ページを回すのではなく、まず1ページだけでテストします。プレビューでは次の点を確認します。

  • 1件が1行になっている
  • 発生時刻と震源地が同じ地震に対応している
  • 最大震度がalt属性から取得できている
  • 詳細URLが欠落していない
  • 同じページを無限に巡回していない

6. CSVにエクスポートする

タスクをローカル実行し、結果をCSV形式でエクスポートします。ファイル名は次のようにしました。

earthquakes.csv

OctoparseはCSV、Excel、HTML、JSONなどへのエクスポートに対応しています。pandasに渡したかったので、今回はCSVを選びました。

CSVの先頭は、概ね次のような形式になります。

発生時刻,震源地,マグニチュード,最大震度,詳細URL
2026年07月29日12時08分頃,熊本県熊本地方,M3.9,3,https://earthquake.tenki.jp/...
2026年07月29日11時37分頃,熊本県天草・芦北地方,M3.0,2,https://earthquake.tenki.jp/...

以下は実際の取得内容です。取得時刻によって変わります。
image.png

pandasでデータを整形する

ここからはPythonで処理します。まず、CSVを読み込み、列名と欠損値を確認します。

from pathlib import Path

import pandas as pd

CSV_PATH = Path("earthquakes.csv")

df = pd.read_csv(CSV_PATH, dtype=str)
df.columns = df.columns.str.strip()

required_columns = {
    "発生時刻",
    "震源地",
    "マグニチュード",
    "最大震度",
    "詳細URL",
}

missing_columns = required_columns - set(df.columns)
if missing_columns:
    raise ValueError(f"CSVに必要な列がありません: {sorted(missing_columns)}")

print(df.head())
print(df.isna().sum())

発生時刻をdatetimeに変換する

発生時刻には「頃」が含まれるため、取り除いてから変換します。

df["発生時刻"] = pd.to_datetime(
    df["発生時刻"]
      .str.strip()
      .str.replace("", "", regex=False),
    format="%Y年%m月%d日%H時%M分",
    errors="coerce",
)

マグニチュードを数値に変換する

M3.9のような文字列から数値部分を抽出します。震源地やマグニチュードが未確定の場合もあるため、変換できない値はNaNにします。

df["マグニチュード"] = pd.to_numeric(
    df["マグニチュード"].str.extract(r"(\d+(?:\.\d+)?)")[0],
    errors="coerce",
)

最大震度を正規化する

震度には5弱6強などがあるため、グラフ上の並び順に使える数値も作ります。

SHINDO_SCORE = {
    "1": 1.0,
    "2": 2.0,
    "3": 3.0,
    "4": 4.0,
    "5弱": 5.0,
    "5強": 5.5,
    "6弱": 6.0,
    "6強": 6.5,
    "7": 7.0,
}

df["最大震度"] = (
    df["最大震度"]
      .str.strip()
      .str.replace("震度", "", regex=False)
      .str.extract(r"(7|6強|6弱|5強|5弱|4|3|2|1)")[0]
)

df["最大震度スコア"] = df["最大震度"].map(SHINDO_SCORE)

このスコアは描画順を決めるための便宜的な値です。計測震度そのものではありません。

重複と必須項目の欠損を除く

詳細URLが取得できている場合はURLを優先して重複判定に使います。

has_url = df["詳細URL"].notna() & df["詳細URL"].ne("")

with_url = df.loc[has_url].drop_duplicates(subset=["詳細URL"])
without_url = df.loc[~has_url].drop_duplicates(
    subset=["発生時刻", "震源地", "マグニチュード", "最大震度"]
)

df = pd.concat([with_url, without_url], ignore_index=True)
df = df.dropna(subset=["発生時刻", "震源地"])
df = df.sort_values("発生時刻").reset_index(drop=True)

print(f"整形後の件数: {len(df)}")
print(f"取得期間: {df['発生時刻'].min()}{df['発生時刻'].max()}")

熊本県周辺のデータを抽出する

今回は、以下の3地域を対象にします。

TARGET_EPICENTERS = [
    "熊本県熊本地方",
    "熊本県天草・芦北地方",
    "熊本県阿蘇地方",
]

START = pd.Timestamp("2026-07-28 00:00:00")
END = pd.Timestamp("2026-07-30 00:00:00")

target = df[
    df["震源地"].isin(TARGET_EPICENTERS)
    & df["発生時刻"].between(START, END, inclusive="left")
].copy()

print(f"対象データ: {len(target)}")
print(target.groupby("震源地").size().sort_values(ascending=False))

取得ページ数によっては、指定期間の全件をカバーできないことがあります。最古の取得時刻がSTARTより新しい場合は、利用規約とアクセス負荷を確認したうえで、必要最小限だけページネーション回数を増やしてください。

可視化する

グラフを保存するディレクトリを作成します。

from pathlib import Path

import japanize_matplotlib  # noqa: F401
import matplotlib.pyplot as plt

OUTPUT_DIR = Path("output")
OUTPUT_DIR.mkdir(exist_ok=True)

plt.style.use("ggplot")

1. 時間帯別の地震情報件数

hourly = (
    target.set_index("発生時刻")
          .resample("h")
          .size()
          .rename("件数")
)

fig, ax = plt.subplots(figsize=(12, 5))
hourly.plot(
    ax=ax,
    marker="o",
    linewidth=1.8,
    color="#d35400",
)

ax.set_title("熊本県周辺:時間帯別の地震情報件数")
ax.set_xlabel("発生時刻")
ax.set_ylabel("件数")
ax.grid(alpha=0.3)

fig.tight_layout()
fig.savefig(OUTPUT_DIR / "earthquakes_hourly.png", dpi=160)
plt.show()

最新の1時間は集計途中である可能性があります。継続取得したデータを比較するときは、未完了の時間帯を除いてください。

2. マグニチュードの分布

magnitude = target["マグニチュード"].dropna()

fig, ax = plt.subplots(figsize=(9, 5))
ax.hist(
    magnitude,
    bins=range(0, 9),
    edgecolor="white",
    color="#2980b9",
    align="left",
)

ax.set_title("熊本県周辺:マグニチュードの分布")
ax.set_xlabel("マグニチュード")
ax.set_ylabel("件数")
ax.set_xticks(range(0, 9))
ax.grid(axis="y", alpha=0.3)

fig.tight_layout()
fig.savefig(OUTPUT_DIR / "magnitude_distribution.png", dpi=160)
plt.show()

3. 震源地別の件数

by_epicenter = (
    target["震源地"]
    .value_counts()
    .sort_values()
)

fig, ax = plt.subplots(figsize=(9, 5))
by_epicenter.plot.barh(ax=ax, color="#16a085")

ax.set_title("熊本県周辺:震源地別の地震情報件数")
ax.set_xlabel("件数")
ax.set_ylabel("震源地")
ax.grid(axis="x", alpha=0.3)

fig.tight_layout()
fig.savefig(OUTPUT_DIR / "earthquakes_by_epicenter.png", dpi=160)
plt.show()

4. 最大震度別の件数

震度は単純な文字列順に並べると順序が崩れるため、カテゴリの順番を明示します。

SHINDO_ORDER = ["1", "2", "3", "4", "5弱", "5強", "6弱", "6強", "7"]

shindo_counts = (
    target["最大震度"]
    .value_counts()
    .reindex(SHINDO_ORDER, fill_value=0)
)

fig, ax = plt.subplots(figsize=(9, 5))
shindo_counts.plot.bar(ax=ax, color="#8e44ad")

ax.set_title("熊本県周辺:最大震度別の地震情報件数")
ax.set_xlabel("最大震度")
ax.set_ylabel("件数")
ax.tick_params(axis="x", rotation=0)
ax.grid(axis="y", alpha=0.3)

fig.tight_layout()
fig.savefig(OUTPUT_DIR / "earthquakes_by_shindo.png", dpi=160)
plt.show()

1ファイルで実行できる完全版

ここまでの処理をまとめたコードです。

from pathlib import Path

import japanize_matplotlib  # noqa: F401
import matplotlib.pyplot as plt
import pandas as pd


CSV_PATH = Path("earthquakes.csv")
OUTPUT_DIR = Path("output")
OUTPUT_DIR.mkdir(exist_ok=True)

TARGET_EPICENTERS = [
    "熊本県熊本地方",
    "熊本県天草・芦北地方",
    "熊本県阿蘇地方",
]

START = pd.Timestamp("2026-07-28 00:00:00")
END = pd.Timestamp("2026-07-30 00:00:00")

SHINDO_ORDER = ["1", "2", "3", "4", "5弱", "5強", "6弱", "6強", "7"]
SHINDO_SCORE = {
    "1": 1.0,
    "2": 2.0,
    "3": 3.0,
    "4": 4.0,
    "5弱": 5.0,
    "5強": 5.5,
    "6弱": 6.0,
    "6強": 6.5,
    "7": 7.0,
}


def load_and_clean(path: Path) -> pd.DataFrame:
    df = pd.read_csv(path, dtype=str)
    df.columns = df.columns.str.strip()

    required = {
        "発生時刻",
        "震源地",
        "マグニチュード",
        "最大震度",
        "詳細URL",
    }
    missing = required - set(df.columns)
    if missing:
        raise ValueError(f"CSVに必要な列がありません: {sorted(missing)}")

    df["発生時刻"] = pd.to_datetime(
        df["発生時刻"]
          .str.strip()
          .str.replace("", "", regex=False),
        format="%Y年%m月%d日%H時%M分",
        errors="coerce",
    )

    df["マグニチュード"] = pd.to_numeric(
        df["マグニチュード"].str.extract(r"(\d+(?:\.\d+)?)")[0],
        errors="coerce",
    )

    df["最大震度"] = (
        df["最大震度"]
          .str.strip()
          .str.replace("震度", "", regex=False)
          .str.extract(r"(7|6強|6弱|5強|5弱|4|3|2|1)")[0]
    )
    df["最大震度スコア"] = df["最大震度"].map(SHINDO_SCORE)

    has_url = df["詳細URL"].notna() & df["詳細URL"].ne("")
    with_url = df.loc[has_url].drop_duplicates(subset=["詳細URL"])
    without_url = df.loc[~has_url].drop_duplicates(
        subset=["発生時刻", "震源地", "マグニチュード", "最大震度"]
    )

    df = pd.concat([with_url, without_url], ignore_index=True)
    df = df.dropna(subset=["発生時刻", "震源地"])
    return df.sort_values("発生時刻").reset_index(drop=True)


def save_charts(target: pd.DataFrame) -> None:
    if target.empty:
        raise ValueError(
            "対象データが0件です。取得ページ数、期間、震源地名を確認してください。"
        )

    plt.style.use("ggplot")

    hourly = (
        target.set_index("発生時刻")
              .resample("h")
              .size()
              .rename("件数")
    )
    fig, ax = plt.subplots(figsize=(12, 5))
    hourly.plot(ax=ax, marker="o", linewidth=1.8, color="#d35400")
    ax.set(
        title="熊本県周辺:時間帯別の地震情報件数",
        xlabel="発生時刻",
        ylabel="件数",
    )
    ax.grid(alpha=0.3)
    fig.tight_layout()
    fig.savefig(OUTPUT_DIR / "earthquakes_hourly.png", dpi=160)
    plt.close(fig)

    magnitude = target["マグニチュード"].dropna()
    if not magnitude.empty:
        fig, ax = plt.subplots(figsize=(9, 5))
        ax.hist(
            magnitude,
            bins=range(0, 9),
            edgecolor="white",
            color="#2980b9",
            align="left",
        )
        ax.set(
            title="熊本県周辺:マグニチュードの分布",
            xlabel="マグニチュード",
            ylabel="件数",
        )
        ax.set_xticks(range(0, 9))
        ax.grid(axis="y", alpha=0.3)
        fig.tight_layout()
        fig.savefig(OUTPUT_DIR / "magnitude_distribution.png", dpi=160)
        plt.close(fig)

    by_epicenter = target["震源地"].value_counts().sort_values()
    fig, ax = plt.subplots(figsize=(9, 5))
    by_epicenter.plot.barh(ax=ax, color="#16a085")
    ax.set(
        title="熊本県周辺:震源地別の地震情報件数",
        xlabel="件数",
        ylabel="震源地",
    )
    ax.grid(axis="x", alpha=0.3)
    fig.tight_layout()
    fig.savefig(OUTPUT_DIR / "earthquakes_by_epicenter.png", dpi=160)
    plt.close(fig)

    shindo_counts = (
        target["最大震度"]
        .value_counts()
        .reindex(SHINDO_ORDER, fill_value=0)
    )
    fig, ax = plt.subplots(figsize=(9, 5))
    shindo_counts.plot.bar(ax=ax, color="#8e44ad")
    ax.set(
        title="熊本県周辺:最大震度別の地震情報件数",
        xlabel="最大震度",
        ylabel="件数",
    )
    ax.tick_params(axis="x", rotation=0)
    ax.grid(axis="y", alpha=0.3)
    fig.tight_layout()
    fig.savefig(OUTPUT_DIR / "earthquakes_by_shindo.png", dpi=160)
    plt.close(fig)


df = load_and_clean(CSV_PATH)

target = df[
    df["震源地"].isin(TARGET_EPICENTERS)
    & df["発生時刻"].between(START, END, inclusive="left")
].copy()

print(f"全取得データ: {len(df)}")
print(f"対象データ: {len(target)}")
print(target.groupby("震源地").size().sort_values(ascending=False))

save_charts(target)
print(f"グラフを保存しました: {OUTPUT_DIR.resolve()}")

グラフを見るときに気をつけたいこと

時間帯別のグラフに山ができると、つい理由まで説明したくなります。ただ、今回数えているのは、取得したWebページに掲載された地震情報の件数です。次のことは、このグラフだけから判断できません。

  • 地震活動が今後増えるか減るか
  • 大きな地震の前兆であるか
  • 地域間の地震リスクが高いか低いか
  • 地震同士に直接的な因果関係があるか

ここで分かるのは「いつ、どのような情報が掲載されたか」までです。地震の予測や因果関係の推定には使えません。

Octoparseを使ってみて感じた向き・不向き

今回のような繰り返しの一覧は、Octoparseに任せるとHTML解析やページネーションを一から書かずに済みます。特に、最大震度のような少し変わったフィールドを画面上で確認しながら調整できる点は便利です。

一方、公式APIがあるなら、私はそちらを先に検討します。緊急地震速報のようにリアルタイム性が必要な用途や、完全性が求められる研究用途にも、この方法は向きません。

たとえば、次のような場面なら使いやすそうです。

  • Webページに一覧はあるが、CSVダウンロード機能がない
  • 一度だけ、または小規模にデータを整形したい
  • Seleniumの実装や保守に時間をかけたくない
  • 抽出結果をExcelやCSVから分析したい

逆に、次の条件では別の方法を選びます。

  • 公式APIや公式CSVが提供されている
  • 緊急地震速報のようなリアルタイム性が必要
  • 防災システムや研究で、完全性・継続性が保証されたデータが必要
  • 対象サイトの規約で自動取得が認められていない

Octoparseを「APIの代わり」と考えるより、公開Webページしか入口がないときにデータ取得を補助する道具と考える方がしっくりきます。

おわりに

今回いちばん面白かったのは、画面上では数字に見える最大震度が、実際には画像だったことです。alt属性をCSVに入れてしまえば、後は普通のカテゴリデータとしてpandasで扱えます。

全部をノーコードで完結させるのではなく、取得はOctoparse、加工と確認はPythonと分けると、手早さと再現性のバランスを取りやすいと感じました。

次に試すなら、同じタスクを一定間隔で実行し、前回との差分だけを保存する形にしてみたいです。ただし、リアルタイムの防災通知として使うのではなく、後から傾向を振り返るための収集として考えています。

参考資料


※ 私はOctoparseに関係する立場です。この記事では、製品紹介そのものではなく、Web上の一覧を分析できる形にする一例をまとめました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?