2026年7月28日、熊本県熊本地方を震源とする地震が相次ぎました。気象庁の発表によると、同日19時03分頃の地震は深さ10km、マグニチュード4.2、最大震度5弱(速報値)でした。
地震情報の一覧を眺めていて、「どの時間帯に情報が集中しているのだろう」と気になりました。1件ずつならWebページを見るだけで十分ですが、時間帯別に数えようとすると、まず一覧を表にしなければなりません。
最初はPythonだけで取得することも考えました。ただ、今回はスクレイピング部分をOctoparseに任せ、CSVにした後の整形と可視化をpandas/Matplotlibで行うことにしました。
やることは次の4つです。
- Webページ上の繰り返しデータを抽出する
- 画像属性を含むデータをCSVに整形する
- pandasで欠損値や表記揺れを処理する
- 時系列・分布・カテゴリ別件数を可視化する
重要
これは地震を予測する試みではありません。災害時の判断には、気象庁や自治体などの公式情報を利用してください。また、スクレイピング前に対象サイトの利用規約・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の自動検出は、ページ内の類似要素やページネーションを認識してワークフローの候補を作成できます。
2. 地震情報の繰り返しリストを選択する
自動検出されたプレビューで、地震情報の各行が繰り返しデータとして選択されていることを確認します。
自動検出がうまくいかない場合は、一覧内の異なる2行を順番に選択し、「すべて選択」またはリスト作成に相当する操作を選びます。
抽出フィールドは次の5つに整理します。
| フィールド名 | 取得する値 |
|---|---|
発生時刻 |
発生時刻のテキスト |
震源地 |
震源地のテキスト |
マグニチュード |
Mを含むテキスト |
最大震度 |
震度画像のalt属性 |
詳細URL |
発生時刻リンクのURL |
画像URL |
震度画像リンクのURL |
3. 最大震度で少し詰まった
最大震度は普通のテキストとして取れるだろうと思っていました。ところが、画面上では数字に見えても、ページ上では画像として表示されています。ここをテキスト抽出にしてしまうと、期待した値は入りません。
画像URLを取って後から判定する方法もありますが、今回は画像要素の alt属性を抽出する方が簡単でした。
Octoparseで最大震度の画像を選び、抽出するデータの種類を「属性」に変更して、属性名にaltを指定します。
プレビューが次のようになれば成功です。
1
2
3
4
5弱
5強
空欄や画像URLになっている場合は、フィールドの抽出設定を再確認します。
4. 詳細ページのURLを取得する
発生時刻には各地震の詳細ページへのリンクが設定されています。

発生時刻を選択し、テキストとは別にリンク先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/...
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と分けると、手早さと再現性のバランスを取りやすいと感じました。
次に試すなら、同じタスクを一定間隔で実行し、前回との差分だけを保存する形にしてみたいです。ただし、リアルタイムの防災通知として使うのではなく、後から傾向を振り返るための収集として考えています。
参考資料
- 気象庁
- 気象庁:2026年7月28日19時03分頃の地震に関する緊急地震速報(予報)発表状況
- 日本気象協会 tenki.jp:過去の地震情報
- Octoparseヘルプセンター:自動検出機能とは?
- Octoparseヘルプセンター:ページネーションの処理(「次へ」ボタンがある場合)
- Octoparseヘルプセンター:抽出データはどの形式でダウンロードできますか?
※ 私はOctoparseに関係する立場です。この記事では、製品紹介そのものではなく、Web上の一覧を分析できる形にする一例をまとめました。


