個人開発している中古バイク検索サービス(MotoHub)に、「車種×バッテリー型番」の適合表ページを42車種ぶん公開した。
設計セッションからデータ投入・本番公開まで実質1日。ただし「1日でできた」ことより、スクレイピングという選択肢が最初に消えたことと、投入前のdry-runが2種類の事故を実際に止めたことのほうが書く価値があると思ったので、その話をする。
なぜ「型番」なのか
サイトには症状診断ツール(/trouble)とメンテナンス記事群があって、「バッテリーが上がったらどう交換するか」までは案内できていた。でもユーザーが実際に詰まるのはその手前、**「で、自分のバイクのバッテリーの型番は何なのか」**だった。
「ディオ バッテリー 型番」のような検索は実在するし、SERPを見ると上位はバッテリー通販の車種別ページと個人整備店のブログ。うちには約30万台の車両データと車種マスタがあるので、車種ページに「整備データ」を接ぎ木すれば、検索需要・パーツ価格比較(アフィリエイト)・中古在庫への導線が1ページで繋がる。
データソースの壁:robots.txtと「転載禁止」
適合データの一次ソースは、バッテリーメーカー(GSユアサ等)が公開している適合検索だ。当然「クローラで取れないか」を最初に考える。
結果:robots.txtで自動アクセス拒否。
じゃあ通販サイトの適合表は——と見ると、こちらは利用規約に無断複製・転載・ファイル変換の禁止が明記されている。孫引きもアウト。
ここで整理した(法律の専門家ではないので、あくまで個人開発者としての整理)。
- 「車種→型番」という事実そのものに著作権はない
- ただし他社のデータベースを機械的に丸写しすれば、データベースの著作物・利用規約の問題になり得る
- robots.txtが拒否している以上、技術的に取れたとしても取るべきではない
結論は手動キュレーション一択。人間がメーカーの適合検索を1車種ずつ引いて、独自の表構成(型式×年式×純正/互換/注意事項)に編集し、出典と確認日をページに明記する。地味だが、これが唯一の正攻法だった。
ちなみにGSユアサ自身が適合検索の結果ページで「同一車種・型式でも仕様により搭載バッテリーが異なる場合があるので必ず実車確認を」と免責している。一次ソースですらこの世界なので、うちのページにも免責と「型式(車台番号)の調べ方」セクションを必須装備にした。
スキーマ設計:「型式」の粒度が生命線
手動で集め始めてすぐ分かったのが、モデル名の粒度では絶対にダメということ。実例を出す。
同じ「スーパーカブ110」でも:
| 型式 | 販売開始 | 適合バッテリー |
|---|---|---|
| 8BJ-JA59 / JA61 | 2022〜 | GTZ4V |
| EBJ-JA10 | 2012〜 | YTX4L-BS |
| EBJ-JA07 | 2009〜 | YTZ7S |
3世代で3品番。レブル250も現行MC49はGTZ8V、90年代の旧レブル(MC13)はYTX7L-BSで別物。モデル名で1つの答えを返す設計にしていたら、適合しないバッテリーを買わせる実害が出るところだった。
なのでテーブルは型式(フレームコード)粒度で持つ:
model_fitments
bike_model_id -- 車種マスタFK
task -- 'battery'(将来 'plug' 等)
frame_code -- 'JBH-AA01' 等
year_range -- ソース表記のまま
recommended_part_no
compatible_part_nos (JSON) -- 台湾ユアサ・古河の互換品番
spec (JSON) -- 電圧・容量・タイプ
source_1_name / source_1_url
verified_at -- ★これが入った行だけ公開
note -- '傾斜搭載のため液入充電済必須' 等
ページ側は「即答ボックス(品番ドン)→ 型式×年式の適合表 → 型式の調べ方 → 交換手順 → 免責」の構成。noteには「ヤマハ ギアは液入り充電済タイプのみ対応」みたいな、買い間違いに直結する情報だけを残した。
事故を防ぐ柵を4枚立てた
型番間違いは実害なので、データ投入経路に安全柵を仕込んだ。
1. model_name_check 列
CSVには車種IDと一緒に車種名も書き、インポータがDBの車種名と完全一致しなければその行をskipして警告する。ID誤記で「カブのページにジョグの型番」が載る事故を構造的に防ぐ。
2. モデル×task単位の全置換インポート
CSVに登場する車種は既存行を全削除→挿入。CSVが常にsource of truthになり、再実行は完全冪等。年式表記の修正も差分事故なく反映される。CSV自体はdatabase/data/配下でgit管理(手作業キュレーションの一次成果物なので、履歴と差分レビューに価値がある)。
3. verified_at公開ゲート
検証日が入った行を持つ車種だけルートが解決し、sitemapにも載る。データが無ければ正規の404。「とりあえず空ページを量産してインデックスさせる」をシステムが許さない。
4. dry-run
--dry-runで車種ごとのdelete/insert件数・skip・警告を表形式で出す。DBには触らない。
柵は実際に仕事をした
42車種・100行のCSVを作って、本番でdry-runした結果がこれ(抜粋):
取込行数: 93 / 公開対象(verified): 93 / skip: 10
--- 警告 ---
行16: model_name_check不一致(DB「モンキー」≠CSV「monkey」)→ skip
行39: model_name_check不一致(DB「グロム」≠CSV「grom」)→ skip
行77: model_name_check不一致(DB「レブル250」≠CSV「rebel-250」)→ skip
model #181「ct125ハンターカブ」: slug「ct125」は他モデルと重複
→ slug設定せず(公開URLが曖昧)
2種類の事故を投入前に検出した。
1つ目は名前不一致。CSV作成時、車種マスタの表示が文字化けした箇所をローマ字slugで代用した車種が5つあり、それが全部引っかかった。柵1の想定通り。
2つ目は想定外の収穫で、slugのグローバル重複。車種マスタのslugはメーカー内ユニークで設計されていたが、公開URLは /maintenance/{slug}/battery とslug単独で引く。ct125 というslugを持つ車種が2つあり、そのまま公開するとどちらの車種に解決されるかがルーティング順に依存する状態だった。インポータが「重複slugにはデータを公開させない」判定で止めてくれた。
名前を直し、ct125は今回除外して再dry-run → skip: 0。そこで初めて本実行した。
dry-runなしでいきなり流していたら、5車種が静かに欠けたまま「投入完了」と思い込み、ct125は運が悪ければ別車種の型番を表示していた。投入前に必ず乾いた地面で転ぶ、を仕組みにしておいてよかった。
品番マスタで手入力を潰す
集めた適合データを眺めると、登場する品番は42車種でも18種類に収束していた(YTX4L-BS、YTZ7S、GTZ8V…)。互換品番や電圧・容量は品番から機械的に決まるので、車種ごとに手で書くのは冗長かつ誤入力の温床。
品番→{電圧, 容量, 互換品番}のマスタCSVを1枚作り、本体CSVは「型式・年式・品番・注意」だけ持たせてスクリプトで補完する形にした。マスタに無い品番が出たら警告(=品番タイポ検出の柵がもう1枚増える)。次にプラグ適合を追加するときは、この構造がそのまま効く。
AIとの分業メモ
実装まわりの体制だけ簡単に。設計・CSV構造化・インポータ仕様はチャットのClaudeと詰め、Laravel実装はClaude Codeに指示書を渡して任せた(migration・インポートコマンド・Blade・テスト一式で1セッション)。人間の仕事は、メーカー適合検索を1車種ずつ引いてコピペすること(42車種で30分弱)と、本番へのデプロイ判断。機械が取れないデータを人間が集め、人間が書くと事故る整形を機械がやるという分担が、今回は綺麗にはまった。
結果と次
- 42車種・100行を本番公開、sitemap+IndexNowでBingに通知済み
- 第1号のジャイロキャノピー(自分の愛車)は、表示された型番と実車搭載品番の一致を確認済み
- 効果測定はこれから。GSCで「{車種} バッテリー 型番」系クエリの推移を見る
次にやりたいのは、ガレージ機能(オーナーの整備記録)との連携。バッテリー交換を記録するときに使った品番を任意入力してもらい、承認を通して「オーナーn台で交換実績あり」のバッジを適合表に出す。メーカー公開データを一次ソースにしつつ、実車での検証実績を第2ソースとして積み上げる——ここまでいくと、通販サイトの適合表には構造的に真似できないページになるはずだ。