專案

一般

配置概況

動作

Feature #178

已結束
SC SC

重構搜尋引擎為 GraphQL API 分頁、修正獨立衛浴誤判、強化 UI 互動體驗

Feature #178: 重構搜尋引擎為 GraphQL API 分頁、修正獨立衛浴誤判、強化 UI 互動體驗

是由 Sashiba Chou23 天 前加入. 於 23 天 前更新.

狀態:
Closed
優先權:
Normal
被分派者:
開始日期:
2026-03-12
完成日期:
2026-03-12
完成比例:

100%

預估工時:
12:00 小時
耗用工時:

概述

h1. 背景

上一版(e8be719)為初始版本,僅支援基本文字搜尋與滾動載入。本次迭代針對以下問題進行大幅重構:

  1. 獨立衛浴判斷錯誤(Bug)— "Hello Verona Rooms" 等混合房型飯店被 roomfacility=38 伺服器端篩選直接排除;GraphQL 的 nbBathrooms 欄位不可靠(僅回傳 1 個 unitConfiguration,且值經常為 0)
  2. 搜尋引擎效率不足 — 滾動載入速度慢、無法精確控制分頁
  3. UI 互動不便 — 無法複製飯店名稱/地址、無地圖視覺化、缺少 Google 評分整合

變更範圍
┌─────────────┬─────────────┬────────────────────────────────────┐
│ 檔案 │ 異動統計 │ 說明 │
├─────────────┼─────────────┼────────────────────────────────────┤
│ scraper.py │ +612 / - 127│ GraphQL 引擎重構 + 衛浴判斷修正 │
├─────────────┼─────────────┼────────────────────────────────────┤
│ main.py │ +196 / - 17 │ 管線強化:去重、早期淘汰、多層過濾 │
├─────────────┼─────────────┼────────────────────────────────────┤
│ app.py │ +237 / - 17 │ UI 增強:複製表格、地圖、新篩選項 │
├─────────────┼─────────────┼────────────────────────────────────┤
│ maps_api.py │ +38 │ 新增 Google 評分查詢 │
├─────────────┼─────────────┼────────────────────────────────────┤
│ config.py │ +1 │ 新增 ENABLE_PRIVATE_BATH 設定 │
└─────────────┴─────────────┴────────────────────────────────────┘


一、搜尋引擎重構(scraper.py)

1.1 GraphQL API 分頁

  • 攔截 POST /dml/graphql(operationName: FullSearch)取得 query + variables 模板
  • 透過 page.evaluate(fetch) 在瀏覽器 context 內分頁呼叫,繞過 CORS
  • URL offset 參數已棄用,改用 GraphQL variables.input.pagination.offset
  • GraphQL 直接提供:座標、地址、價格、評分、評論數、房型面積、免費取消政策
  • 保留滾動載入作為 fallback(_scrape_with_scroll)

1.2 座標搜尋(Plan B)

  • 偵測輸入為座標格式時,使用 latitude= + longitude= URL 參數
  • ss 參數為純文字搜尋,座標放入 ss 會回傳錯誤城市的結果

1.3 去重機制

  • GraphQL 層:依 hotel ID 即時去重(all_hotels dict)
  • 管線層:依 hotel ID → URL 路徑 → 飯店名稱三級 fallback

二、獨立衛浴判斷修正(scraper.py + main.py)— Bug Fix

問題根因(經 6 支測試腳本驗證):
┌───────────────────────────────────┬───────────────────────────────────────────────────────────────────────────────┐
│ 方法 │ 問題 │
├───────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤
│ roomfacility=38(伺服器端篩選) │ 過度篩選:混合房型飯店(有私人+共用)被完全排除(約 6% 結果) │
├───────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤
│ nbBathrooms(GraphQL 欄位) │ 不可靠:僅回傳 1 個 unitConfiguration,值經常為 0 │
├───────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤
│ page.content().lower() 關鍵字搜尋 │ 誤判:搜到 <script> 標籤內的樣板文字(如 FAQ 區的「有附私人衛浴的客房嗎?」) │
├───────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤
│ 「私人」單詞比對 │ 假陽性:「私人停車場」「私人保險箱」等非衛浴設施也會匹配 │
└───────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────┘

修正方案:

  1. 移除 roomfacility=38(_build_nflt())— 不再在 URL 層面篩選
  2. 移除 nbBathrooms 邏輯(_parse_graphql_results())— private_bathroom 設為 None,交由詳細頁判斷
  3. 改寫 _confirm_private_bathroom() — 使用 JavaScript TreeWalker API 遍歷 DOM 可見文字節點:
    - 排除 SCRIPT / STYLE / NOSCRIPT 標籤
    - 使用複合關鍵字(修飾詞+衛浴詞),避免單詞假陽性

正向關鍵字(有任一即判定 True):

| 繁體中文 | English |
| 私人衛浴、私人浴室 | private bathroom |
| 獨立衛浴、獨立浴室 | ensuite / en suite / en-suite |
| 房內衛浴、房內浴室 | attached bathroom |
| 專屬衛浴、專屬浴室 | in-room bathroom |
| 附設衛浴、附設浴室 | |

負向關鍵字(僅有負向時判定 False):

| 繁體中文 | English |
| 共用衛浴/浴室 | shared bathroom |
| 共享衛浴/浴室 | communal bathroom |
| 公共衛浴/浴室 | common bathroom |

4. 所有飯店都爬詳細頁(main.py)— hotels_needing_detail 改為包含所有有 URL 的飯店
  1. 移除 is None 保護(main.py)— private_bathroom 一律使用詳細頁結果覆寫
    判斷邏輯: 有正向 → True;僅有負向 → False;無關鍵字 → False

三、詳細頁面資料補充(scraper.py)

  • 房間面積:支援 m² 與 ft²(自動轉換)
  • 地址提取:4 種 DOM selector + JSON-LD fallback
  • 座標提取:5 種策略(JSON-LD → b_map_center → data-atlas-latlng → staticmap URL → meta tags)
  • 評分/評論數:JSON-LD → DOM selector → HTML regex fallback

四、管線強化(main.py)

  • 早期淘汰:在詳細頁爬取前,依評分 + 評論數先過濾,減少爬取量
  • 六層過濾鏈:獨立衛浴 → 評分 → 評論數 → 步行上限 → 大眾運輸 → 自駕
  • Google 評分整合:可選啟用,透過 Find Place + Place Details API
  • 進度回呼:progress_callback(stage, message) 供 Streamlit UI 即時顯示

五、Streamlit UI 增強(app.py)

  • 目的地預覽地圖:輸入目的地後即時顯示地圖(Folium)
  • 可點擊複製表格:飯店名稱、地址欄位點擊即複製到剪貼簿
  • 雙檢視模式:「點擊複製」tab + 「排序檢視」tab
  • 飯店結果地圖:搜尋結果在 Folium 地圖上標記,自動調整視角
  • 新篩選選項:獨立衛浴 toggle、Google 評分 toggle、最低評論數
  • 日誌顯示改進:固定高度容器 + 自動捲到底部

六、Google Maps API 擴充(maps_api.py)

  • 新增 get_google_rating(hotel_name, address) 方法
  • 流程:Find Place(取得 place_id)→ Place Details(取得 rating + user_ratings_total)
  • 需啟用 Places API(Legacy)

測試驗證

以下測試腳本已執行並產出 JSON 結果(不納入 commit):
┌───────────────────────────────┬───────────────────────────────────────────────────────────┐
│ 腳本 │ 目的 │
├───────────────────────────────┼───────────────────────────────────────────────────────────┤
│ test_roomfacility38.py │ 驗證 roomfacility=38 會過濾掉 6% 混合房型 │
├───────────────────────────────┼───────────────────────────────────────────────────────────┤
│ test_unit_configs.py │ 驗證 GraphQL 只回傳 1 個 unitConfig、nbBathrooms 不可靠 │
├───────────────────────────────┼───────────────────────────────────────────────────────────┤
│ test_facility38_debug.py │ 驗證 genericfacilityhighlight 在 page.content() 中不存在 │
├───────────────────────────────┼───────────────────────────────────────────────────────────┤
│ test_bath_keywords_collect.py │ 收集 Verona 飯店衛浴關鍵字 │
├───────────────────────────────┼───────────────────────────────────────────────────────────┤
│ test_bath_keywords_broad.py │ 跨城市(巴黎/倫敦/東京/曼谷/台北/愛丁堡)多語系關鍵字收集 │
├───────────────────────────────┼───────────────────────────────────────────────────────────┤
│ test_facility38_verify.py │ 驗證 id:38 + rt-name-link 方案不可行 │
└───────────────────────────────┴───────────────────────────────────────────────────────────┘

動作

匯出至 PDF Atom