はじめに
中古車の新着通知サービスを開発する中で、大手ポータル(カーセンサー、グーネット等)に加えて、自動車メーカー8社の公式中古車サイトも監視対象にしました。
- トヨタ認定中古車
- ホンダ U-Select
- 日産公式中古車
- マツダ公式中古車
- スバル公式(スグダス)
- スズキ公式中古車
- ダイハツ U-CATCH
- 三菱 MC-net
メーカー公式サイトはポータルより先に掲載されることがあり、特に人気車種(GRヤリス、シビックタイプR等)では数時間の差が命取りになります。
しかし、8社それぞれ技術的な罠があり、全て踏み抜きました。これはその記録です。
罠1: EUC-JPの世界(スバル、ダイハツ、スズキ)
2024年に開発を始めて最初に面食らったのが文字コード。
Content-Type: text/html; charset=EUC-JP
スバルのスグダス、ダイハツのU-CATCH、スズキ公式はEUC-JPです。令和の時代に。
Node.jsのfetchはUTF-8を前提としているので、そのまま読むと文字化けします。
const buffer = await response.arrayBuffer();
const decoder = new TextDecoder('euc-jp');
const html = decoder.decode(buffer);
しかも各サイトでContent-Typeヘッダーにcharsetが書いてあったりなかったり。HTMLの<meta>タグにだけ書いてある場合もあります。
教訓: レスポンスのバイナリをTextDecoderで変換する前に、charsetを自動検出する仕組みを入れておく。
罠2: HTMLスクレイプ vs JSON API
8社のうち、半分はHTML、半分はJSON APIを返します。
| メーカー | 形式 | URL例 |
|---|---|---|
| トヨタ | HTML | toyota.jp/ucar/search/... |
| ホンダ | JSON API | ucar.honda.co.jp/Car/List?CarModelCd=... |
| 日産 | HTML | u-car.nissan.co.jp/ucar/search/... |
| マツダ | JSON API | www2.mazda.co.jp/.../SearchResult?mk=10&cn=... |
| スバル | HTML (EUC-JP) | ucar.subaru.jp/php/search/summary.php?... |
| スズキ | HTML (EUC-JP) | www.suzuki.co.jp/ucar/php/search/summary.php?... |
| ダイハツ | HTML (EUC-JP) | u-catch.daihatsu.co.jp/php/search/summary.php?... |
| 三菱 | HTML | ucar.mitsubishi-motors.co.jp/ucar/list/... |
ホンダとマツダはJSON APIで、レスポンスをパースすれば構造化データが直接取れます。一方、HTML組はBeautiful Soup的なパースが必要。
ところがJSON APIのほうが楽かというとそうでもない。マツダのAPIはレスポンスのキー名がcnとかmkとか略語だらけで、ドキュメントは当然ありません。
{
"cn": "25",
"mk": "10",
"dc": "2526A",
"si": "010"
}
dcが何を意味するのか、実際のデータを何十件も見比べて推測するしかありません。
罠3: 詳細ページURLの組み立て(マツダ)
マツダの一覧APIは車両IDを返しますが、詳細ページのURLを組み立てるのに別のパラメータが必要でした。
最初の実装:
detail.html?dc=${shop.id} // 店舗番号 "010"
正しい実装:
detail.html?dc=${car.id.split("@").pop()} // ディストリビューターコード "2526A"
shop.id(店舗番号)とcar.idの末尾(ディストリビューターコード)を取り違えていて、全ての詳細ページが「お取り扱い終了しました」を表示していました。
APIのレスポンスだけ見ていても気づけない。実際にブラウザで詳細ページを開いて、URLのパラメータを一つずつ確認するしかありません。
罠4: ID体系がサイトごとに違う
同じ車両を識別するIDの体系が全く違います。
- カーセンサー:
U00045678901形式 - グーネット:
700050123456789形式 - トヨタ:
TC-{店舗ID}-{車両連番}形式 - ホンダ:
{CarModelCd}-{VIN末尾}形式 - ヤフオク:
auction_{auctionID}形式
cross-site dedup(サイト間の同一車両判定)をするには、IDではなく車両の属性(タイトル+価格 or 所在地+価格)でマッチングするしかありません。
const dedupKey = `${normalize(title)}|${normalize(price)}`;
ただしこれは完璧ではなく、同じ店の同じ価格の同じ車種は誤ってdedupされることがあります。100%の精度は諦めて、95%で運用しています。
罠5: ページネーションが全員違う
// カーセンサー・グーネット: クエリパラメータ
?page=2
// 健美家: パス埋め込み
/tokyo/shinjuku-ku/n-2/
// HOME4U: オフセット方式(パスパラメータ)
/municipality/4/13/102/of=20/
// スバル・ダイハツ・スズキ: PHPパラメータ
?page=2&car_cd=XXX
これをサイトごとにpaginateUrl(url, page)関数で抽象化していますが、罠はページ番号の起点。ほとんどのサイトは1始まりですが、HOME4Uはオフセット0始まり(of=0が1ページ目、of=20が2ページ目)。
罠6: Cloudflareの壁(楽待)
楽待(rakumachi.jp)は通常のHTTPリクエストではCloudflare Challengeで弾かれます。
Just a moment...
Checking your browser before accessing rakumachi.jp
対策として2段階のフォールバックを入れました:
- Playwright — ヘッドレスブラウザでJavaScriptを実行してChallenge突破
- Cloudflare Workers Proxy — 日本のIPアドレスから中継(海外サーバーからのアクセスがブロックされるため)
Cloud Runは東京リージョンですが、それでもブロックされることがあります。Workers経由にすると安定しました。
罠7: JavaScriptレンダリング必須(athome)
athomeはCSR(Client Side Rendering)で、HTTPレスポンスのHTMLには物件データがほぼ含まれていません。
<div id="app"></div>
<script src="/assets/app.js"></script>
Playwright(ヘッドレスブラウザ)でJavaScriptを実行して、DOMが描画されてからHTMLを取得する必要があります。通常のfetchの10倍以上時間がかかりますが、仕方ない。
罠8: レスポンスがHTMLなのにContent-Typeがapplication/json(三菱)
三菱のMC-netは、特定の条件でHTMLを返しているのにContent-Typeがapplication/jsonというレスポンスを返すことがあります。JSONとしてパースしようとすると当然エラー。
try {
return JSON.parse(body);
} catch {
// JSONパース失敗 → HTMLとして処理
return parseHtml(body);
}
Content-Typeを信用してはいけない、という教訓。
罠9: 色の表記ゆれ
車の色(ボディカラー)をDBに格納するために、各サイトから色情報を抽出するのですが:
- カーセンサー:
パールホワイト - グーネット:
ホワイトパール - トヨタ:
プラチナホワイトパールマイカ(089) - 日産:
ボディ色\nブリリアントホワイトパール - マツダ:
スノーフレイクホワイトパールマイカ
同じ「白」でも表記が全く違います。正規化をどこまでやるかは悩みどころ。現時点ではサイトから取得した生テキストをそのまま保存して、表示時に短く切り詰めています。
罠10: 掲載終了の判定
各サイトで掲載が終了した(売れた/取り下げた)車両の検知方法:
- 一覧ページに出てこなくなった → 掲載終了とみなす
- 詳細ページが404/リダイレクト → 確定
問題は1の判定。一覧ページは新着順でソートされるので、古い車両は後ろのページに押し出されます。10ページ分しかスキャンしていないと、11ページ目以降にある車両を「掲載終了」と誤判定してしまう。
対策: last_seen_atを記録して、一定期間(48時間)見かけなくなったものだけを掲載終了と判定。
まとめ
| 罠 | 対策 |
|---|---|
| EUC-JP | TextDecoderでcharset自動検出 |
| HTML vs JSON混在 | サイトごとにパーサーを分離 |
| 詳細URL組み立て | ブラウザで実際に確認 |
| ID体系バラバラ | 属性ベースのdedup key |
| ページネーション差異 | SiteConfig抽象化 |
| Cloudflare | Playwright + Workers Proxy |
| CSR | Playwright必須 |
| Content-Type嘘 | try-catch fallback |
| 色の表記ゆれ | 生テキスト保存、表示時に整形 |
| 掲載終了誤判定 | last_seen_at + 48h閾値 |
8社のメーカー公式サイトを追加したことで、カーセンサー・グーネットにない掘り出し物を検知できるようになりました。技術的には地獄でしたが、ユーザー価値は確実に上がっています。
スクレイピングは「きれいなAPI」を前提にすると破綻します。実際のレスポンスを目で見て、泥臭く対応するのが唯一の正攻法です。
これらの8社を含む15サイトの新着車両をAI分析付きでLINEに通知するサービスが中古車ウォッチです。不動産版の物件ウォッチもあります。