5
1

執筆のきっかけ

CMC meetupというイベントがあったらしく、資さんうどんのテックや経営における取り組みが話題だったらしいです。

そういえば資さんうどんがどこにあるのかと調べてみたところ、店舗一覧のページからは地図表示がなかったようなので、自分用に資さんうどんマップを作ってみたいなと思って作成してみました。

作ったもの : 資さんうどんマップ(2023年7月版)

資さんうどんとは?

資さんうどん、福岡は北九州地区を中心に展開する地元に愛されるうどんチェーンで、うどん王国福岡において他の雄たちと日夜しのぎを削っています。

その魅力は美味しさもさることながら、多くの店舗では、闇夜を偲んで生きる吸血鬼<エンジニア>には優しい24時間営業であります。

九州のAEONでは資さんうどんのTシャツが販売され、その人気はAkamaiのUTを凌ぐほどだったとかなんとか…。

image.png

福岡のうどんは有名な香川や伊勢ほどコシのあるうどんではなく、労働者文化に根付いた時短メシであると言われています。

一方で北九州地区が誇る資さんうどんはその中でも歯ごたえがあるタイプの麺が提供され、出汁も濃いめだと思います。

IMG_5682.jpeg

メニューの幅も広く、安価です。

おでんやぼたもち(おはぎ)が名物で、テイクアウトも人気です。こちらも合わせてぜひ。

この記事は2023年7月末の内容で、改稿した2023年12月には店舗数が拡大しており、大阪まで店舗が進出しています。

本題

我々位置情報を扱うエンジニアは、データビジュアライゼーションはもとより、そのデータの前処理(クレンジング)において、日々鼻血を出しながら構造化データに立ち向かっています。

ただ、業務に追われる中で一つ一つの可視化作業に対してそんなにコストをかけていられない ( 鼻血を出したくない | 省力化したい ) というのも事実で、サクッとWebGISで対象物を表示するような手段は追求すべきです。
また、そのノウハウは広く公開すべきだと思い、練習も兼ねて「QGIS2Web」を利用した可視化を行ってみました。


QGISとは ・・・ オープンソース(GNU GPLライセンス)で提供されている地理空間情報分析ソフトウェア(群)です。WindowsでもMacでも問題なく動作する上、拡張プラグインをPythonで作成することができます。

QGIS2Web ・・・ QGISで表示したレイヤー群の構造をOpenLayersやLeafletを用いてブラウザで動作するWeb地図としてエクスポートするプラグインです。


今回の大まかな手順は以下です。

  • htmlを眺める
  • 抽出したいDOMを見つけたら正規表現で抜き出す(pythonでパーサを作る)
  • パーサよりCLIに表示された文字列をコピペしてスプレッドシートでcsvにする
  • QGISでcsvを読み込んでOpenStreetMapのレイヤーの上に表示
  • QGIS2Webでマークアップのwebアプリケーションにする
  • GitHubPagesにデプロイ

手順0 QGISなどのインストール

結構重いので十分なRAMが載ったPCで扱いましょう。

ダウンロード https://qgis.org/ja/site/forusers/download.html

ダウンロードできたら日本語化、及びプラグイン「QGIS2Web」を導入しましょう。

Webプログラミングのために任意のエディターもあると良いかもしれません。

手順1 資さんの店舗一覧ページの構造を把握する

ショップ一覧からボタンを開きつつ、真心を込めて住所を構造化データ(CSV)に落とし込みます。

image.png

ショップ一覧のページのhtmlソースをすべて落としてきて構造を眺めます。

店舗一覧のスクリーンショット(折りたたみ)

image.png

ふむふむ、店舗の周りのDOMはこうなってるのか。

店舗一覧のhtmlソース(折りたたみ)
<div class="p-shop__item-wrapper">
    <div class="p-shop__item-dl-wrapper">
        <dl class="p-shop__item-dl">
            <dt class="p-shop__item-dt">所在地</dt>
            <dd class="p-shop__item-dd">
                福岡県北九州市小倉南区上葛原2-18-50
            </dd>
        </dl>
        <dl class="p-shop__item-dl">
            <dt class="p-shop__item-dt">営業時間</dt>
            <dd class="p-shop__item-dd">年中無休・7時~24時(OS23:30)</dd>
        </dl>
    </div>
    <div class="p-shop__item-button">
        <a href="https://www.google.com/maps/dir/?api=1&amp;destination=%E8%B3%87%E3%81%95%E3%82%93%E3%81%86%E3%81%A9%E3%82%93+%E6%9C%AC%E5%BA%97"
            class="c-button p-shop__item-btn" target="_blank" rel="noopener noreferrer nofollow"><svg
                class="svg-inline--fa fa-walking fa-w-10" aria-hidden="true" focusable="false" data-prefix="fas"
                data-icon="walking" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"
                data-fa-i2svg="">
                <path fill="currentColor"
                    d="M208 96c26.5 0 48-21.5 48-48S234.5 0 208 0s-48 21.5-48 48 21.5 48 48 48zm94.5 149.1l-23.3-11.8-9.7-29.4c-14.7-44.6-55.7-75.8-102.2-75.9-36-.1-55.9 10.1-93.3 25.2-21.6 8.7-39.3 25.2-49.7 46.2L17.6 213c-7.8 15.8-1.5 35 14.2 42.9 15.6 7.9 34.6 1.5 42.5-14.3L81 228c3.5-7 9.3-12.5 16.5-15.4l26.8-10.8-15.2 60.7c-5.2 20.8.4 42.9 14.9 58.8l59.9 65.4c7.2 7.9 12.3 17.4 14.9 27.7l18.3 73.3c4.3 17.1 21.7 27.6 38.8 23.3 17.1-4.3 27.6-21.7 23.3-38.8l-22.2-89c-2.6-10.3-7.7-19.9-14.9-27.7l-45.5-49.7 17.2-68.7 5.5 16.5c5.3 16.1 16.7 29.4 31.7 37l23.3 11.8c15.6 7.9 34.6 1.5 42.5-14.3 7.7-15.7 1.4-35.1-14.3-43zM73.6 385.8c-3.2 8.1-8 15.4-14.2 21.5l-50 50.1c-12.5 12.5-12.5 32.8 0 45.3s32.7 12.5 45.2 0l59.4-59.4c6.1-6.1 10.9-13.4 14.2-21.5l13.5-33.8c-55.3-60.3-38.7-41.8-47.4-53.7l-20.7 51.5z">
                </path>
            </svg>ルート検索</a>
        <a href="http://map.sukesanudon.com/shop_list/shop50" class="c-button--brown p-shop__item-btn"><svg
                class="svg-inline--fa fa-arrow-alt-circle-right fa-w-16" aria-hidden="true" focusable="false"
                data-prefix="fas" data-icon="arrow-alt-circle-right" role="img" xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 512 512" data-fa-i2svg="">
                <path fill="currentColor"
                    d="M256 8c137 0 248 111 248 248S393 504 256 504 8 393 8 256 119 8 256 8zM140 300h116v70.9c0 10.7 13 16.1 20.5 8.5l114.3-114.9c4.7-4.7 4.7-12.2 0-16.9l-114.3-115c-7.6-7.6-20.5-2.2-20.5 8.5V212H140c-6.6 0-12 5.4-12 12v64c0 6.6 5.4 12 12 12z">
                </path>
            </svg>店舗詳細</a>
    </div>
</div>

どうやら店舗住所は以下のように<dd>要素で表現されている模様です。

<dd class="p-shop__item-dd">
                福岡県北九州市小倉南区上葛原2-18-50
</dd>

htmlから

<dd class="p-shop__item-dd">ワイルドカード</dd>

って感じで抽出すると全件取れそう!!

...

結果 : 189軒あるらしい。

↑執筆時点で63店舗のはずなので明らかに多すぎる。

手順2 正規表現を用いて店舗の住所を文字列として取り出す

フォーマットかけたhtmlから上記の文字列に合致するように、(LLMに生成させた)正規表現で文字列を抽出します。

<dd class="p-shop__item-dd">\s*([^<]+?)\s*\s*([^<]+?)\s*</dd>

※かなりナイーブな実装で、この取り組みを行った当時は『県』にしか出店していなかったので割り切って抽出しました。都道府がある場合でも同様の方法を論理和として条件設定すれば抽出は容易ですが、「岐阜県山県市」に出店した瞬間に破綻します。(参考 : とにかく日本の住所のヤバさをもっと知るべきだと思います: https://note.com/inuro/n/n7ec7cf15cf9c)

今回はhtmlをソースをコピーしてローカルにtxtファイルとして保存しています。
reを用いて正規表現をもちいて文字列の抽出を行います。


import re

# テキストファイル名を指定する
file_name = "your_text_file.txt"

# 正規表現パターンを定義します
pattern = r'<dd class="p-shop__item-dd">\s*([^<]+?)\s*県\s*([^<]+?)\s*</dd>'

# テキストファイル読み込み
with open(file_name, 'r', encoding='utf-8') as file:
    text_data = file.read()

# 正規表現にマッチする部分を抽出
matches = re.findall(pattern, text_data)

# 検出された結果を表示します
for match in matches:
    prefecture = match[0]
    location = match[1]
    print(f"{prefecture}{location}")
ターミナル出力(折りたたみ)
福岡県北九州市八幡西区浅川1-25-2
福岡県糟屋郡志免町大字南里24-5
福岡県北九州市八幡西区御開1-21-3
大分県別府市汐見町10-12
福岡県太宰府市向佐野4-12-17
福岡県北九州市戸畑区東鞘ヶ谷町2-1
福岡県春日市星見ヶ丘6-92
宮崎県都城市下川東一丁目14-7
福岡県遠賀郡岡垣町鍋田1-3-3
福岡県福岡市博多区諸岡2-15-27
福岡県北九州市小倉南区大字朽網3914-89
福岡県北九州市八幡西区町上津役東2-3-12
福岡県糟屋郡粕屋町大字大隈113-1
福岡県北九州市小倉南区上葛原2-18-50
福岡県北九州市小倉南区湯川5-9-5
山口県宇部市則貞5丁目1−5
福岡県北九州市小倉北区魚町2-6-1
福岡県北九州市小倉北区神岳1-2-10
福岡県福岡市西区北原2丁目1-4
熊本県菊池郡菊陽町津久礼2750-2
福岡県福岡市西区橋本1-8-15
鹿児島県霧島市隼人町見次1227
福岡県福岡市博多区千代2-1-24
福岡県京都郡苅田町大字二崎175-1
福岡県北九州市戸畑区新池3-12-3
福岡県古賀市舞の里3-1-1
山口県山口市葵1丁目4-58
福岡県北九州市八幡西区茶売町3-5
福岡県北九州市八幡西区陣山1-3-33
福岡県飯塚市弁分23-2
佐賀県鳥栖市真木町1113-6
大分県中津市大字下池永字松本55番1
宮崎県宮崎市阿波岐原町請田2420
福岡県福岡市東区和白丘2-3-3
福岡県糟屋郡新宮町大字上府牟田668-2
福岡県宗像市稲元2-4-8
山口県下関市稗田西町13-10
福岡県久留米市野伏間1丁目5−16
山口県下関市伊倉新町2-3-6
福岡県糟屋郡志免町別府3-15-7
福岡県北九州市小倉北区竪町2-3-3
佐賀県佐賀市兵庫町藤木 1487-4
福岡県北九州市小倉北区貴船町1-15
佐賀県唐津市町田1766−1
熊本県熊本市中央区新市街6−9柏田ビル1F
福岡県田川市大字川宮710-34
福岡県福岡市早良区野芥3-29-5
大分県大分市明野北5丁目3-5
宮崎県日向市財光寺482-1
福岡県北九州市八幡東区東田3-2-102イオンモール八幡東1F
福岡県北九州市門司区梅ノ木町1-19
熊本県熊本市南区田迎町大字田井島256-1
福岡県中間市中央5-4-12
福岡県福岡市博多区半道橋1-9-18
福岡県北九州市小倉南区徳吉西2-3-15
福岡県北九州市八幡西区八枝4-2-1
福岡県福岡市早良区原3-17-32
熊本県熊本市東区戸島西2丁目6-70
佐賀県佐賀市開成5-13-17
福岡県北九州市戸畑区土取町1-19
福岡県京都郡苅田町磯浜町2-8-16
福岡県北九州市小倉南区葛原東2-14-1

うん、良さそう!

手順3 CSVを作成する

店名があったほうがGIS上でのラベル付けに便利であることに気がついたので、パーサのスクリプトを作り直して、店舗名のDOMも一緒にlistに格納できるように改造。

この時点でBeatfulSuopでスクレイピングかけたほうが楽だったなと後悔し始める。

import re

# 解析するhtml
file_name = "sukesan.html"

# LLMに作らせた正規表現解析パターン
pattern = r'<h4 class="p-shop__item-title">(.+?)</h4>[\s\S]*?<dd class="p-shop__item-dd">([\s\S]+?県[\s\S]+?)</dd>'

# ファイル読み込み
with open(file_name, 'r', encoding='utf-8') as file:
    text_data = file.read()

# 正規表現にマッチする部分を検出しListに格納
matches = re.findall(pattern, text_data)

# 検出された結果をコンソールに表示(コピペしてCSVを作る)
for match in matches:
    title = match[0]
    location = match[1].strip()
    print(f"{title}",","f"{location}")

上記の出力をコピペしてスプレッドシートに貼り付け、CSVファイルとして保存する。

image.png

あるいはCSISのサービスを利用してもよいかもしれません。
https://geocode.csis.u-tokyo.ac.jp/geocode-cgi/geocode.cgi?action=start

手順4 QGISにレイヤーとして

日本語化している場合、 Windows / Mac ともに、

レイヤー > レイヤーの追加 > デリミテッドレイヤの追加

でCSVファイルをレイヤーとして追加します。

image.png

背景地図としてOpenStreetMapをXYZタイルとして読み込んでいます。

手順5 QGIS2Webを用いて表示しているレイヤーをWebGISプロジェクトに変換する

背景地図とCSVファイルから読み込んだレイヤーが表示できたらQGIS2Webを利用してLeafletのプロジェクトに変換します。

image.png

image.png

ちなみに仮想の店舗ごとの†経済圏†は、

ベクタ > 空間演算ツール > バッファ

でバッファーを作成しています。

手抜きのためバッファーの作成半径はdegree単位のまま、緯度33度付近において概ね5km前後の範囲に重なるよう作成しています。

なお、作成したプロジェクトの内容としては、Leafletでよく利用されるプラグイン完備という感じですが、構成がどうしてもシンプルじゃなく、Leaflet自体に慣れていないユーザーにとっては「htmlやJavascriptをちょっとイジってみよう」という感覚で改変しにくいと思います。
DBなどを絡めた動的なアプリケーションに持っていきたい、と思う場合などはQGISでの表示を確かめた時点でgeojson形式などで出力し、Javascript内でオブジェクトとしてデータアクセスを行ったほうが取り回しは良いかもしれません。

image.png

手順6 GitHub Pagesにデプロイする。

GitHubにアクセスし、(無料ユーザの場合)パブリックリポジトリにアップロードし、GitHubPages機能を有効にすると静的なマップアプリケーションを公開することができます。

アップロード例:資さんうどんマップ(2023年7月版)

測地系の変換を忘れており、出力の際にバッファーが南北方向に引き伸ばされたようになっており、解析ミスをしています。
また、マウスオーバー時のラベルの表示周りも調整の余地があります。

結果

image.png

バッファーを描画してみると、どうやら意外なことに直方地域や大分県との県境のエリアなど、自家用車の所持率が高そうなエリアが空白地帯となっているようです。

北九州を本丸とする企業ですが、福岡都市圏にも集中して出店していて、福岡市の近郊技術者は資さんうどんの庇護のもとに生活できていることがわかりますね(?)

長距離ドライブを行う関係上、深夜営業のある店舗が街道沿いやサービスエリアなどにあると嬉しいな...と思いつつ。

ちなみにもっと南の熊本エリアなどにも進出しており、概ね九州の県庁所在地では資さんうどんを堪能できるようです。

やったね!

というわけで平易に資さんうどんの出店概況を可視化してみました。

スクレイピングを行う際はホスト側の負担に注意しましょう。
また、著作物の利用には十分注意して、公序良俗の許される範囲で取り扱いましょう。


追記1

岡山に初進出らしい㊗

追記2

資さんうどんにはサブスクがあるそうです。
https://sukesanstore.com/teikibin.html

追記3

東京にポップアップストアが出店とのこと。

■ 「資さんうどん POP-UPレストラン」
営 業 日:2024年7月13日(土)・14日(日)・15日(月・祝)
時  間:11:00~16:00
    ※商品が無くなり次第、早期終了の可能性がございます。
場  所:Bistrot VIVANT(ビストロ ヴィヴァン)
    東京都千代田区内神田1-18-11 東京ロイヤルプラザ 1F
    JR神田駅から徒歩5分
    ※3日間、Bistrot VIVANTを間借りして営業させていただきます。

引用元: 北九州のソウルフード「資さんうどん」が、東京で待望のPOP-UPレストランをオープン!7/13(土)〜15(月・祝)の3日間限定!!皆さまのお越しをお待ちしております。 | 株式会社資さんのプレスリリース

また、同時に東京に常設店の展開の発表も!

5
1
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
5
1