この記事は、「MIERUNE Advent Calendar 2025」の15日目の記事です。
昨日は@kntoshiyaさんによる「あなたの町に信号はいくつある?QGISで北海道の市町村ごとの信号数を数えてみた」でした。
https://qiita.com/advent-calendar/2025/mierune

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

今回、スクレイピングにはPythonを利用します。Playwrightとpandasと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に必要なファイルとコードを追記します。
https://github.com/mug-jp/maplibregljs-amazon-location-service-starter
実行環境
全体構成
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パッケージをインストールします。
npminstall以前、書いた記事を参考にAmplify Console(Gen2)でGitHubを利用した公開をします。リポジトリはforkしたスターターを利用します。
https://memo.dayjournal.dev/memo/aws-amplify-016
公式サイトから店舗情報をスクレイピングします。公式サイトは動的にコンテンツを生成しているため、Playwrightを利用しブラウザを操作しながらデータを取得しています。各店舗の詳細ページから、店舗名・住所・電話番号・営業時間・駐車場・座席の種類・シャワー室の有無・詳細ページのURL・店舗の位置情報などを取得しています。
店舗名を取得する例
from playwright.sync_apiimport sync_playwrightimport pandasas pddefscrape_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 urlin 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': nameor'不明'})except Exceptionas 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形式(十進度)に変換します。geopyも利用し、複数の変換パターンに対応しています。
DMS→DD変換例
from typingimport Tuplefrom geopyimport 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変換例
import jsonimport pandasas pddefcreate_geojson_features(df): features=[]for _, rowin df.iterrows(): properties={}for colin df.columns:if colnotin['lat','lon']: value= row[col]if pd.isna(value): properties[col]=Noneelse: properties[col]=str(value) feature={"type":"Feature","geometry":{"type":"Point","coordinates":[row['lon'], row['lat']]},"properties": properties} features.append(feature)return featuresGeoJSONの出力結果
{"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 maplibreglfrom'maplibre-gl';import OpacityControlfrom'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=newmaplibregl.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});店舗タイプごとにレイヤを設定し、カスタムアイコンを設定します。
interfaceLayerConfig{ 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データをレイヤとして追加します。ズームレベルに応じてアイコンサイズが変化するよう設定しています。
functionaddGeoJsonLayer(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,}});}店舗をクリックすると、店舗情報をポップアップで表示します。住所・電話番号・営業時間・駐車場・座席の種類などの情報を表示しています。
functioncreatePopupContent(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=newOpacityControl({ overLayers: overLayers, opacityControl:false});map.addControl(opacityControl,'bottom-left');今回、Playwrightでのスクレイピング、geopyでのDMS→DD変換、CSV→GeoJSON変換、MapLibre GL JS & Amazon Location Serviceでのマップアプリケーションという構成で「山岡家Map(非公式)」を構築しました。地図で可視化してみると新たな発見があります。最北端は稚内、東京は周辺に出店、九州には進出しているのに四国には店舗が無い、そして餃子の山岡家は全国でたった1店舗。ラーメン山岡家の出店戦略が見えてきます。

ぜひ近くの店舗を探したり、旅行先でラーメン山岡家を見つける時にご利用ください!
https://x.com/dayjournal_nori/status/1993092273698226233
あと、最近知ったのですが、山岡家商店という公式通販サイトがありまして、チャーシューを注文してみようと思っています。どんぶりやレンゲも販売しているようです!
他にも記事を書いています。よろしければぜひ。
https://zenn.dev/mierune/articles/try-117-amplify-location
!PodcastとYouTubeやってます。よろしければぜひ。
バッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。

Co-Founder and CEO of MIERUNE|Geospatial Architect|AWS DevTools Hero|Community - MapLibre, Amplify, Notion