0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

自動車メーカー公式中古車サイト8社のスクレイピングで踏んだ罠まとめ

0
Posted at

はじめに

中古車の新着通知サービスを開発する中で、大手ポータル(カーセンサー、グーネット等)に加えて、自動車メーカー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段階のフォールバックを入れました:

  1. Playwright — ヘッドレスブラウザでJavaScriptを実行してChallenge突破
  2. 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: 掲載終了の判定

各サイトで掲載が終了した(売れた/取り下げた)車両の検知方法:

  1. 一覧ページに出てこなくなった → 掲載終了とみなす
  2. 詳細ページが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に通知するサービスが中古車ウォッチです。不動産版の物件ウォッチもあります。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?