山岡家Map(非公式)を作った話

山岡家Map(非公式)について
みなさんは、ラーメン山岡家という素晴らしいラーメンチェーンを知っていますでしょうか?
ラーメン山岡家は、1988年に茨城県牛久市で創業した全国展開しているラーメンチェーンです。濃厚な豚骨スープと、自由にカスタマイズできるのが特徴で、24時間営業の店舗も多く、トラックドライバーや深夜に働く方々にも利用されています。私自身も20年以上通い続けるファンで、ホームは札幌の伝説的な名店「南2条店」です。いつも醤油ラーメンのアブラ少なめで注文しています。
今年のAWS Summit Japan 2025をきっかけに、私も自分の専門領域で何か応援できないかと考えていました。
そして、非公式の山岡家Mapというアプリケーションを構築してみました。このマップを利用することで、全国のラーメン山岡家の店舗情報を地図上でシームレスに確認できます。
PWA対応もしているのでスマートフォンのホーム画面にも追加可能です。
追加方法:
・iOS(Safari):共有ボタン → 「ホーム画面に追加」
・Android(Chrome):メニュー → 「ホーム画面に追加」
ラーメン山岡家の店舗タイプ
ラーメン山岡家には、実は4つの店舗タイプがあります。
1. ラーメン山岡家
通常の山岡家で、豚骨ベースの定番メニューを提供しています。全国に150店舗以上展開されています。私はいつも醤油ラーメンを注文します。
2. 煮干しラーメン山岡家
煮干しをベースにしたラーメンを提供する専門店です。通常の山岡家とは異なる味わいが楽しめます。私は煮干しが苦手なので実は1回も行ったことがありません。
3. 味噌ラーメン山岡家
味噌ラーメンに特化した店舗で、濃厚な味噌スープが特徴です。ここでは、あえて醤油ラーメンを注文するのがおすすめです。実は北海道のみに3店舗しかありません。
4. 餃子の山岡家
餃子をメインにした新業態の店舗です。この店舗は日本で1店舗だけで、札幌にあります。実はここにも1回も行ったことがありません。
今回公開したマップでは、この4種類の店舗をアイコン表示し、レイヤ切り替えで表示/非表示できるようにしています。
事前準備
公式サイトに問い合わせ
今回、スクレイピングを行うので事前に公式サイトに確認をしました。事前相談したところ、大変温かいお返事をいただき、またすぐに食べに行きたくなりました。

データ取得と加工
今回、スクレイピングにはPythonを利用します。Playwrightとpandasとgeopyを組み合わせてデータ取得と加工をします。
- スクレイピング: Playwright
- データ加工: pandas
- DMS→DD変換: geopy
全体構成
yamaokaya-data
└── script
├── scrape_yamaokaya.py # スクレイピング
├── latlon_yamaokaya.py # DMS→DD変換
├── column_yamaokaya.py # カラム名変更
├── csv2geojson.py # CSV→GeoJSON変換
マップアプリケーション
事前に、Amazon Location Service v2のスターターをforkします。その後、山岡家Mapに必要なファイルとコードを追記します。
実行環境
- node v24.4.1
- npm v11.4.2
全体構成
yamaokaya-map
├── LICENSE
├── README.md
├── dist
│ └── index.html
├── img
│ ├── README01.gif
│ ├── README02.png
│ └── README03.png
├── index.html
├── package-lock.json
├── package.json
├── public
│ ├── manifest.json
│ ├── data
│ │ ├── yama.geojson
│ │ ├── niboshi.geojson
│ │ ├── miso.geojson
│ │ └── gyouza.geojson
│ └── icons
│ ├── yama.png
│ ├── niboshi.png
│ ├── miso.png
│ └── gyouza.png
├── src
│ ├── main.ts
│ ├── style.css
│ └── vite-env.d.ts
├── tsconfig.json
└── vite.config.ts
パッケージをインストールします。
npm install
Amplify Gen2で公開設定
以前、書いた記事を参考にAmplify Console(Gen2)でGitHubを利用した公開をします。リポジトリはforkしたスターターを利用します。
データ取得と加工
スクレイピング
公式サイトから店舗情報をスクレイピングします。公式サイトは動的にコンテンツを生成しているため、Playwrightを利用しブラウザを操作しながらデータを取得しています。各店舗の詳細ページから、店舗名・住所・電話番号・営業時間・駐車場・座席の種類・シャワー室の有無・詳細ページのURL・店舗の位置情報などを取得しています。
店舗名を取得する例
from playwright.sync_api import sync_playwright
import pandas as pd
def scrape_yamaokaya_shops():
shops = []
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
)
context.set_default_timeout(10000)
context.set_default_navigation_timeout(10000)
page = context.new_page()
main_url = "https://www.yamaokaya.com/shops/"
page.goto(main_url, wait_until='networkidle', timeout=10000)
page.wait_for_timeout(5000)
shop_links = page.eval_on_selector_all(
'a[href*="/shops/"]',
'els => [...new Set(els.map(el => el.href).filter(href => /shops\\/\\d+/.test(href)))]'
)
for url in shop_links:
try:
page.goto(url, wait_until='domcontentloaded', timeout=10000)
page.wait_for_timeout(5000)
name = page.evaluate("""() => {
const h = document.querySelector('h2, h1, .shop-name');
return h?.innerText?.trim() || document.title.split('|')[0].trim();
}""")
shops.append({'url': url, 'name': name or '不明'})
except Exception as e:
shops.append({'url': url, 'name': 'エラー'})
browser.close()
return pd.DataFrame(shops)
if __name__ == "__main__":
df = scrape_yamaokaya_shops()
df.to_csv('yamaokaya_shops.csv', index=False, encoding='utf-8-sig')
DMS→DD変換
スクレイピングで取得した位置情報は、DMS形式(度分秒)で取得されているため、マップライブラリで表示するためにDD形式(十進度)に変換します。geopyも利用し、複数の変換パターンに対応しています。
DMS→DD変換例
from typing import Tuple
from geopy import Point
# 変換前 "43°03'28.6""N 141°21'22.2""E"
def _convert_with_geopy(dms_string: str) -> Tuple[float, float]:
cleaned = dms_string.replace('""', '"')
point = Point(cleaned)
return point.latitude, point.longitude
カラム名変更
データをGeoJSONに変換する前に、日本語のカラム名を英語に変更します。
カラム名変更例
column_mapping = {
'店舗名': 'store_name',
'住所': 'address',
'電話番号': 'phone_number',
'営業時間': 'business_hours',
'駐車場': 'parking',
'座席の種類': 'seating_types',
'シャワー室': 'shower_room',
'その他': 'other_info'
}
df_renamed = df.rename(columns=column_mapping)
CSV→GeoJSON変換
最後に、CSVをGeoJSON形式に変換します。店舗タイプごとにファイルを分けて出力しています。
CSV→GeoJSON変換例
import json
import pandas as pd
def create_geojson_features(df):
features = []
for _, row in df.iterrows():
properties = {}
for col in df.columns:
if col not in ['lat', 'lon']:
value = row[col]
if pd.isna(value):
properties[col] = None
else:
properties[col] = str(value)
feature = {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [row['lon'], row['lat']]
},
"properties": properties
}
features.append(feature)
return features
GeoJSONの出力結果
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
141.3561,
43.0579
]},
"properties": {
"store_name": "ラーメン山岡家 南2条店",
"details": "https://www.yamaokaya.com/shops/1102/",
"address": "札幌市中央区南2条西1丁目6-1",
"phone_number": "(011) 242-4636",
"business_hours": "5:00-翌4:00",
"parking": "なし",
"seating_types": "カウンター席: 13",
"shower_room": "なし",
"other_info": "まちなかのちいさなお店です。"
}
},
マップアプリケーション作成
背景地図設定
今回、マップライブラリはMapLibre GL JS、背景地図はAmazon Location Serviceを利用し組み合わせています。
import './style.css';
import 'maplibre-gl/dist/maplibre-gl.css';
import 'maplibre-gl-opacity/dist/maplibre-gl-opacity.css';
import maplibregl from 'maplibre-gl';
import OpacityControl from 'maplibre-gl-opacity';
const region = import.meta.env.VITE_REGION;
const mapApiKey = import.meta.env.VITE_MAP_API_KEY;
const mapName = import.meta.env.VITE_MAP_NAME;
const map = new maplibregl.Map({
container: 'map',
style: `https://maps.geo.${region}.amazonaws.com/maps/v0/maps/${mapName}/style-descriptor?key=${mapApiKey}`,
center: [138.0000, 38.5000],
zoom: baseZoom,
maxZoom: 20
});
レイヤ設定
店舗タイプごとにレイヤを設定し、カスタムアイコンを設定します。
interface LayerConfig {
name: string;
iconPath: string;
iconId: string;
visible: boolean;
}
const layerConfigs: Record<string, LayerConfig> = {
'gyouza': {
name: '餃子の山岡家',
iconPath: 'icons/gyouza.png',
iconId: 'gyouza-icon',
visible: true
},
'miso': {
name: '味噌ラーメン山岡家',
iconPath: 'icons/miso.png',
iconId: 'miso-icon',
visible: true
},
'niboshi': {
name: '煮干しラーメン山岡家',
iconPath: 'icons/niboshi.png',
iconId: 'niboshi-icon',
visible: true
},
'yama': {
name: 'ラーメン山岡家',
iconPath: 'icons/yama.png',
iconId: 'yama-icon',
visible: true
}
};
GeoJSONレイヤ追加
GeoJSONデータをレイヤとして追加します。ズームレベルに応じてアイコンサイズが変化するよう設定しています。
function addGeoJsonLayer(id: string, config: LayerConfig, data: GeoJSONData): void {
map.addSource(id, {
type: 'geojson',
data: data
});
map.addLayer({
id: id,
type: 'symbol',
source: id,
layout: {
'icon-image': config.iconId,
'icon-size': [
'interpolate',
['linear'],
['zoom'],
6, baseIconSize * 0.5,
10, baseIconSize * 0.6,
14, baseIconSize * 0.7,
18, baseIconSize * 0.8
],
'icon-allow-overlap': true,
'icon-ignore-placement': false,
},
paint: {
'icon-opacity': 1.0,
}
});
}
ポップアップ実装
店舗をクリックすると、店舗情報をポップアップで表示します。住所・電話番号・営業時間・駐車場・座席の種類などの情報を表示しています。
function createPopupContent(props: StoreProperties): string {
const contentParts: string[] = [];
if (props.store_name) {
contentParts.push(`<h3>${props.store_name}</h3>`);
}
const details: string[] = [];
if (props.address) {
details.push(`<strong>住所:</strong> ${props.address}`);
}
if (props.phone_number) {
details.push(`<strong>電話:</strong> <a href="tel:${props.phone_number}">${props.phone_number}</a>`);
}
// ...
}
レイヤ切り替え
maplibre-gl-opacityを利用し、レイヤの表示/非表示を実装しています。
const overLayers = {
'yama': 'ラーメン山岡家',
'niboshi': '煮干しラーメン山岡家',
'miso': '味噌ラーメン山岡家',
'gyouza': '餃子の山岡家',
};
const opacityControl = new OpacityControl({
overLayers: overLayers,
opacityControl: false
});
map.addControl(opacityControl, 'bottom-left');
まとめ
今回、Playwrightでのスクレイピング、geopyでのDMS→DD変換、CSV→GeoJSON変換、MapLibre GL JS & Amazon Location Serviceでのマップアプリケーションという構成で「山岡家Map(非公式)」を構築しました。地図で可視化してみると新たな発見があります。最北端は稚内、東京は周辺に出店、九州には進出しているのに四国には店舗が無い、そして餃子の山岡家は全国でたった1店舗。ラーメン山岡家の出店戦略が見えてきます。

ぜひ近くの店舗を探したり、旅行先でラーメン山岡家を見つける時にご利用ください!
あと、最近知ったのですが、山岡家商店という公式通販サイトがありまして、チャーシューを注文してみようと思っています。どんぶりやレンゲも販売しているようです!


Discussion