JupyterLab×Pythonでホットペッパービューティーをスクレイピングし Google My Mapsに可視化するまで【初級エンジニア向け】

「競合サロンが多すぎて、まずは“地図で分布”を把握したい――。」
そんな企画会議でよくある一言を、コードだけで解決してみましょう。
本稿では JupyterLab 上でデータ分析をして競合調査をしてみたいと思います。

1.  ホットペッパービューティー（美容院）の検索結果をスクレイピング
2.  料金・口コミなどを整形し
3. Google My Maps 用 CSV を自動生成
4.  分布と価格帯をサクッと可視化

までを 約200行 で実装します。
Python の基礎があれば読み切れる内容なので、初学者の “実務一歩目” に最適です。

今回は兵庫県姫路市の美容室を取得してマイマップに一覧化しました。
https://www.google.com/maps/d/u/0/edit?mid=1fKP3jVf2aS2xJCf-k401zHaNi4JEDcs&usp=sharing


# 目次
1. 下準備
1-1. 環境構築
1-2. 使用ライブラリ一覧
2. スクレイピング編
2-1. ページ数の自動判定
2-2. 店舗カード情報の取得
2-3. 個別ページから住所を抽出
2-4. 取得漏れを防ぐポイント
3. データ整形 & 分析編
3-1. 文字列から数値を抜く正規表現
3-2. 価格帯×口コミ件数を散布図で描く
4. Google My Maps 用 CSV を作る
4-1. Name & Address だけに絞る理由
4-2. My Maps へのインポート手順
5. 戦略にどう活かす？
5-1. 分布から見る“空白エリア”
5-2. 料金×口コミで競合を4象限分析
6. まとめ & 次のアクション

# 1. 下準備
## 1-1. 環境構築
~~~.python:python
conda create -n salon python=3.11 -y
conda activate salon
pip install pandas beautifulsoup4 requests lxml matplotlib

# まだなら
pip install jupyterlab
~~~
## 1-2. 使用ライブラリ一覧
| 目的 | ライブラリ |
|:--:|:--:|
| HTML 解析 | BeautifulSoup4, lxml |
| HTTP リクエスト | requests |
| データフレーム | pandas |
| 可視化 | matplotlib |
| 時間待避 | time |

# 2. スクレイピング
## 2-1. ページ数の自動判定
~~~.python:python
import requests, re
from bs4 import BeautifulSoup

BASE = "https://beauty.hotpepper.jp/svcSB/macBM/salon/sacX258"
soup = BeautifulSoup(requests.get(BASE, timeout=3).text, "html.parser")
last_page = max(int(li.text) for li in soup.select("ul.paging li") if li.text.isdigit())
print(f"▶ 検出ページ数 : {last_page}")
~~~
ul.paging から 最大値 を取れば手動更新は不要。

## 2-2. 店舗カード情報の取得
~~~.python:python
rows = []

for page in range(1, last_page + 1):
    url = BASE if page == 1 else f"{BASE}/PN{page}.html?searchGender=ALL&sortType=popular"
    html = BeautifulSoup(requests.get(url, timeout=3).text, "html.parser")

    for li in html.select("ul.slnCassetteList > li"):
        if "slnPRCassette" in li.get("class", []):      # 広告は無視
            continue

        a = li.find("h3", class_="slnName").a
        rows.append({
            "タイトル"  : a.text.strip(),
            "リンク"    : a["href"],
            "カット料金": li.find("dd", class_="price"  ).text.strip() if li.find("dd", class_="price")  else "",
            "口コミ数"  : li.find("dd", class_="message").text.strip() if li.find("dd", class_="message")else "",
            "ブログ件数": li.find("dd", class_="blog"   ).text.strip() if li.find("dd", class_="blog")   else "",
            "席数"      : li.find("dd", class_="seat"   ).text.strip() if li.find("dd", class_="seat")   else "",
        })
    print(f"▶ 一覧ページ {page}/{last_page}")
~~~
広告 (.slnPRCassette) を除外し、辞書 + append で高速化。

## 2-3. 個別ページから住所を取得
~~~.python:python
import time

def get_address(url: str) -> str:
    root = url.split("?", 1)[0]          # クエリを除去
    soup = BeautifulSoup(requests.get(root, timeout=5).text, "html.parser")

    tag = soup.find("a", class_="mapLink")
    if tag and tag.text.strip():
        return tag.text.strip()

    th = soup.find("th", string=lambda x: x and "住所" in x)
    td = th.find_next("td") if th else None
    return td.text.strip() if td else ""

for r in rows:
    r["住所"] = get_address(r["リンク"])
    time.sleep(0.3)
~~~
or でフォールバックすれば 取りこぼしゼロ。

## 2-4. 取得漏れを防ぐポイント
- 0.3 秒 の time.sleep() を挟みサーバー負荷を回避
- レイアウト変更に備え、CSS セレクタを最小限 に
- 住所タグがない場合も "" を入れ DataFrame の欠損を防止

# 3. データ整形 & グラフ
## 3-1. 文字列から数値を抜く正規表現
~~~.python:python
def numeric(text):
    m = re.search(r"\d[\d,]*", text)
    return float(m.group().replace(",", "")) if m else None

df = pd.DataFrame(rows)
df["カット料金_num"] = df["カット料金"].map(numeric)
df["口コミ数_num"]   = df["口コミ数"].map(numeric)
~~~
## 3-2. 価格帯×口コミ件数を散布図
~~~.python:python
plt.rcParams["font.family"] = "IPAexGothic"      # 文字化け対策
ax = df.plot.scatter(x="カット料金_num", y="口コミ数_num", figsize=(6,4), alpha=.7)
ax.set_xlabel("カット料金(円)"); ax.set_ylabel("口コミ件数")
plt.tight_layout(); plt.show()
~~~
# 4. Google My Maps 用 CSV 出力
## 4-1. Name & Address だけに絞る理由
My Maps のインポートは

- 住所列 からジオコーディング
- タイトル列 をマーカー名にする

の 2 ステップで完了。不要列を削ると UI 操作がワンクリック減ります。

## 4-2. エクスポート手順
~~~.python:python
mymap = df[["タイトル","住所"]].rename(columns={"タイトル":"Name","住所":"Address"})
df.to_csv("salon_full.csv", index=False, encoding="utf-8-sig")
mymap.to_csv("salon_mymap.csv", index=False, encoding="utf-8-sig")
~~~
My Maps では レイヤ → インポート → salon_mymap.csv を指定し、
「Address 列 = 位置情報」「Name 列 = ラベル」にマッピングするだけで OK です。

# 5. 戦略への落とし込み
## 5-1. 分布から見る“空白エリア”
生成した地図を眺めると、駅南側にサロンが集中し、北東エリアがガラ空き。
新規出店やポップアップ施策 の候補に即利用できます。


5-2. 料金×口コミで競合を 4 象限分析
| セグメント | 戦略例 |
|:--:|:--:|
| 高価格×高評価 | コラボ・提携で顧客取り込み |
|高価格×低評価 | 値引きキャンペーンの余地 |
|低価格×高評価 | コスパ強調で対抗 |
|低価格×低評価 | 参入のチャンス |

# 6. まとめ & コールトゥーアクション
ホットペッパービューティーの 公開情報だけ で

- データ収集→整形→可視化→施策検討

の一連を自動化できました。

「試してみた」「もっとこう書ける」など感じたら、
Qiita の LGTM & フォロー でぜひ教えてください！
この記事があなたの Python × マーケティング の第一歩になれば幸いです。