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?

【旅行価格比較】海外旅行のツアーと個別手配を、同じ条件で並べて比べるツールを作っている話(2026-10-11)

0
Last updated at Posted at 2026-10-11

このプロジェクトについて

新企画です。 旅行の価格を比べる機能の開発を紹介します。

個人で使うツールです。もとは、ローカルLLMを動かすPCやパーツを、中古品の販売サイトで探して価格を追うために作りました。その中に、旅行の価格を比べる機能を足しています。今は自分だけで使っていて、公開はしていません。

前回の記事(2026-10-10公開)はこちら → https://qiita.com/rightcross-ae86/items/796ccac4b785b47e6d55

この記事について

この記事は、旅行の価格を比べる機能の紹介です。何を作っているか、どんな約束を決めて作っているか、どこまで確かめたか、まだ確かめていないことは何かを書きます。作り始めたのは2026-09-30で、開発の記録は2026-10-10までのものです。

金額を比べた結果の報告ではありません。結果を載せていない理由は、「未確認事項」に書きました。

この記事では、次のように呼びます。

  • 「ツアー」: 旅行会社が売る、航空券とホテルのセット
  • 「個別手配」: 航空券とホテルを別々に探して、自分で手配すること
  • 「拡張機能」: ブラウザに追加する小さなプログラム
  • 「出発地」「行き先」: 出発する都市、旅行先の都市
  • 「画面」: ブラウザに表示されている、旅行サイトのページ
  • 「検索」: 出発地・行き先・日付・泊数・人数をそろえて、各サイトを同じ条件で探すこと。この機能の1回分の単位です
  • 「4サイト」: ツアーを探す6つのサイトのうち、旅行会社などが自分で扱うツアーを載せている4つのサイト

先に要点

  • 作っているのは、同じ旅行内容で「ツアーの総額」と「個別手配の合計(航空券の最安+ホテルの最安+現地の交通費)」を並べる機能です
  • 決めている約束は3つです。①この機能のサーバーは、旅行サイトへ自動でアクセスしない ②条件が違う金額は並べない ③分からないことは「分からない」と表示する
  • 範囲は、行き先7都市(東南アジア)・出発地5か所・サイト10種です
  • 確認は、実際の画面と1件ずつ突き合わせる方法と、自動で照合する方法の2本立てです
  • 金額を比べた結果は、この記事に載せていません。確認できていない条件が残っているためです

何を比べるのか

旅行の総額は、買い方で変わります。旅行会社のツアーで買うか、航空券とホテルを別々に手配するかです。この機能は、同じ旅行内容(出発地・行き先・日付・泊数・人数)で、2つの総額を並べます。

  • ツアーの総額: ツアーを探す6つのサイトの最安のうち、いちばん安いもの
  • 個別手配の合計: 航空券を探す2つのサイトの最安と、ホテルを探す2つのサイトの最安、現地の交通費(手入力)の合計
  • 差額: ツアーの総額 − 個別手配の合計。プラスなら、個別手配のほうが安いことを表します

集計は、サーバー側のsummary関数の終わりの部分で行っています。

def kmin(kind):
    # 集計には専用の読み取りができたサイトだけを使う(推定値は参考表示のみ)
    v = [x for x in sites if x["kind"] == kind and x["min_price"] and x["reliable"]]
    return min(v, key=lambda x: x["min_price"]) if v else None

fl, ho, tr = kmin("flight"), kmin("hotel"), kmin("tour")
transport = s["transport_jpy"] or 0
parts_total = (fl["min_price"] + ho["min_price"] + transport) if fl and ho else None
return {"search": dict(s), "sites": sites,
        "flight_min": fl, "hotel_min": ho, "tour_min": tr, "transport_jpy": transport,
        "parts_total": parts_total, "tour_total": tr["min_price"] if tr else None,
        "diff": (tr["min_price"] - parts_total) if (tr and parts_total) else None}

kminは、種類(航空券・ホテル・ツアー)ごとに、最安のサイトを選びます。選ぶのは、サイト専用の読み取りができたもの(reliable)だけです。画面の金額を推定しただけのものは、参考として表示するだけで、集計には使いません。航空券とホテルのどちらかが取れていないときは、個別手配の合計(parts_total)と差額(diff)は出さず、Noneのままにします。

抜粋は実物のままです。共通のインデント(4字)だけを除きました。

約束1: この機能のサーバーは、旅行サイトへ自動でアクセスしない

予約サイトには、規約やrobots.txtで、自動の収集を認めていないものがあります。海外のホテルの一覧を作るときに調べて、そう判断しました。そこで、この機能は、サーバーが旅行サイトを取りに行く作りにはしていません。かわりに、自分のブラウザで開いた画面にすでに表示されている価格だけを、拡張機能が読んで、自分のサーバーに送ります。

  • この機能のサーバーから旅行サイトへは、何も送りません。送るのは、自分のブラウザです
  • 拡張機能が読むのは、画面に表示されている価格と、その価格の意味を判断するための文字(プラン名・ホテル名・星の数・人数・日数など)です。サイトごとに専用の読み取りを作り、専用のないページは、画面の円の金額を推定する汎用の読み取りにしています。汎用の金額は、参考表示だけです
  • 各サイトの検索結果のURLは、サーバーが条件(出発地・行き先・日付・人数)から組み立てて、ブラウザで開きます。URLに入れる行き先や出発地の番号は、サイトごとに別です。番号は、(1)確認済みの値 (2)実際の検索で覚えた値 (3)一覧から引いた候補、の順に使い、確認済みの値を候補で上書きしません
  • 同じ考え方で、東南アジア4か国(タイ・ベトナム・マレーシア・インドネシア)のホテルの一覧は、予約サイトからではなく、公開されているオープンデータ(OpenStreetMap)から作りました。2026-09-30時点で55,002件です。料金と空室は含まない、施設の一覧だけです。データは © OpenStreetMap contributors(ODbL)です

約束2: 条件が違う金額は並べない

画面に出ている金額が、検索した条件の金額とは限りません。次のようなずれを見つけるたびに、そのずれた金額を最安に使わない決まりを足してきました。

起きること どうしているか
検索した人数と違う人数の総額が、画面に出る 検索と同じ人数の総額だけを採用する。違うときは除外し、画面で人数を直してから送り直すよう備考に出す
検索した日にツアーがなく、別の日のプランが一覧に出る 出発日が検索と違うプランは使わない。別の日の最安は、参考として備考に出すだけ
複数の都市を回るプランが最安になる 行き先が1か所の比較には使えないので、最安から除く。除いた件数と最安は備考に出す
時間貸しの宿が、宿の最安になる 宿の最安から除く
セット予約の画面の旅程が、検索の条件と違う 旅程(出発地・日付・行き先・人数)を読み取り、検索と違えば除外する
日数・出発日の数え方や、燃油・空港諸税の込み/別が、サイトで違う 結果の表に「比較の条件」を出す。確認できていないものは「未確認」と書く

実際にあった例を2つ書きます。

  • 複数都市のプラン: プーケットの検索で、ツアーを扱うサイトの最安が、バンコクとプーケットを回る7日間のプランの1,075,400円でした。プーケットだけのプランは1,086,600円です。画面を見ると、金額の読み取りは正しく、最安のプランが2都市を回るものでした。複数都市のプランは今は除く方針にして(将来取り入れる可能性があるので、スイッチで戻せます)、本番のデータのコピー43検索で変更の前と後を比べました。そのサイトの金額が変わったのは4件で、結果の見出し(ツアーの最安と差額)は43件すべて変わりませんでした
  • 時間貸しの宿: ジャカルタの検索で、4泊の宿の合計が4,020円(1泊1,005円)と出ていました。時間貸しの掲載が最安になっていたためです。除くと4,848円になります

時間貸しを除く部分のコードです。

DAYUSE_RE = re.compile(r"\d+\s*(?:アワーズ|アワー|時間\s*(?:ステイ|利用|滞在))|デイユース|day\s*use|休憩", re.IGNORECASE)
...
if st["kind"] == "hotel":  # 時間貸しの掲載は宿泊の最安に使わない
    cands = [c for c in cands if not DAYUSE_RE.search(c.get("name") or "")]

1行目は、時間貸しを見分ける正規表現です。「6アワーズ ステイ」「デイユース」「休憩」などの言葉に当たる名前を見つけます。...の所は省略しました。3行目からは、宿の候補のうち、名前がこの正規表現に当たるものを外しています。

約束3: 分からないことは「分からない」と表示する

金額が出ていないときは、理由が分かるように、表示を分けています。

表示 意味
金額 サイト専用の読み取りができて、検索の条件と合っている
検索結果0件 画面が「0件」と表示した。読み取りの失敗ではない
対象外 その行き先は、そのサイトで取り扱いがないと確認できた
拒否された サイトが画面の表示を拒否した。迂回せず、時間をおく
読めず 画面は開いたが、金額も0件の文言も読み取れなかった
未確認 金額に含まれるもの(燃油・空港諸税など)を確認できていない

「対象外」には運用の決まりがあります。最初から全部の行き先とサイトの組み合わせを洗い出すことはしません。実際の画面で確認したときや、出発できる日を変えて調べたときに、取り扱いがないと分かったものを、その都度入れます。画面が「0件」と表示していたときは、設定上の「対象外」よりも、実際の画面の表示を優先します。

ジャカルタでは、4サイトが0件でした(1つの日付の検索です)。ただし、1つの日付で0件だっただけで、取り扱い自体は確認できていないものがありました。そのため、「対象外」と言い切らず、「対象外の可能性(取り扱い自体は未確認)」と表示する文に直しました。

確認のしかた

確認は、4つの方法を組み合わせています。

  1. 実際の画面との突き合わせ: 条件をそろえて検索し、各サイトの画面のタイトルとURLが、検索の条件(行き先・日付・人数)と合っているかを、1件ずつ見ます。金額は、画面の最安と一致するかを目で見て確かめた例があります
  2. 自動の照合: 開いたURLと、画面が最後に開いていたURLを比べ、出発地・日付・人数・行き先の項目が、サイトに書き換えられていないかを見ます。結果は、金額・0件・拒否・読めず・未受信・開かず、のどれかに分けます。限界もあります。見るのはURLと送られた内容までで、画面の見た目(たとえば出発地の表示)は確かめられません
  3. まとめて確認(自動): 出発地と行き先を複数選んで、検索を順に作り、拡張機能がサイトのタブを開いて、結果が揃ったらタブを閉じます。開始は、ボタンを押したときだけです。1回の実行は最大20件、検索と検索の間は60秒あけます。拒否されたサイトは、その実行の間は以降送らず、ほかのサイトは続けます
  4. 実行の依頼: サーバーに「この条件で確認して」という依頼を、コマンドで出します。自分のブラウザのダッシュボードのタブが、その依頼を受けて、まとめて確認を実行します。サイトへ送るのは、このブラウザです。依頼を受け付ける設定は、初期値が「受け付けない」で、タブごとにオン/オフできます。1回の上限(20件)と間隔(60秒)は、依頼では緩められません。同じサイトを自分の手で確認する直前には、依頼を出しません

ここまでの範囲

項目 内容
行き先 7都市: バンコク・ジャカルタ・ホーチミン・ハノイ・クアラルンプール・バリ島・プーケット
出発地 5か所: 東京・大阪・名古屋・福岡・札幌
サイト 10種: ツアーを探す6・航空券を探す2・ホテルを探す2
画面との突き合わせ ホーチミン・ハノイ・クアラルンプール・バリ島・プーケットを東京発で確認。バリ島は、東京・大阪・名古屋・福岡・札幌の5出発地で確認。バンコクとジャカルタは、それ以前に確認
ホテルの一覧 東南アジア4か国で55,002件(2026-09-30時点)。料金と空室は含まない

見えてきたこと

出発地によって、ツアーを扱うサイトの取り扱いが変わります。バリ島行き(12月末出発の5泊・大人2名の1つの条件)で見ると、次のとおりでした。

  • 東京発と大阪発: 4サイトすべてで、ツアーがありました
  • 名古屋発: 4サイトのうち、1つのサイトが0件、もう1つのサイトは「該当する商品がない」という表示でした
  • 福岡発: 4サイトのうち、1つのサイトが0件でした
  • 札幌発: 4サイトすべてが0件でした

1つの日付、1つの条件の結果です。ほかの日付では違うかもしれません。

未確認事項

  • 金額を比べた結果を載せていない理由: 確認できた条件が一部で、1つの日付の結果から言えることが限られるためです。ツアーと個別手配のどちらが安いかは、この記事では書いていません
  • 燃油・空港諸税の込み/別: サイトによって違います。航空券を探す2つのサイトと、セット予約の1つのサイトは、空港諸税の扱いを確認できていないため、表に「未確認」と出しています。ツアーを探す別の1つのサイトも、画面に空港諸税の記載がありません
  • 個別手配の合計は「見えている範囲の最安」: 航空券の最安は乗り継ぎ便のことがあります。ホテルの最安は、格安の宿になることがあります
  • 現地の交通費: 自動では取れません。手入力です
  • 確認していない組み合わせ: 7つの行き先と10の出発地の全組み合わせは、確認していません。比べたい組み合わせが決まってから確かめる方針です
  • 自動の照合の限界: 画面の見た目は確かめられません。新しい種類の組み合わせの最初の数回は、目で見て並べます
  • サイトの画面の変更: 画面の作りが変わると、読めなくなることがあります。その場合は「読めず」と表示されます
  • 公開: 今は個人用で、公開していません。他の人が使う形にするときは、各サイトの規約を確認し直す必要があります

これから

  • 確認していない行き先と出発地の組み合わせを、比べたい組み合わせが決まってから確かめます
  • 行き先を増やすときは、候補を決めてから、サイトごとの行き先の番号で足りないものを調べて足します
  • 国内旅行にも、同じ作りを広げました。海外とは分けて表示していて、この記事では扱いません

まとめ

  • 海外旅行の「ツアーの総額」と「個別手配の合計」を、同じ条件で並べる個人用の機能を作っています。作り始めたのは2026-09-30です
  • 約束は3つです。この機能のサーバーは旅行サイトへ自動でアクセスしない、条件が違う金額は並べない、分からないことは「分からない」と表示する
  • 行き先7都市・出発地5か所・サイト10種を対象にしています
  • 確認は、実際の画面との突き合わせと、自動の照合の2本立てです
  • 燃油・空港諸税の込み/別が確認できていないサイトがあるなど、未確認事項が残っています。金額を比べた結果は、この記事には載せていません

気になる点・ご質問があればコメントでお知らせください。

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?