Travel

bestwehotel.com

Jinjiang WeHotel (`bestwehotel.com`) - Hotel Scraping

Notes an AI agent reads before it works on bestwehotel.com: what to open, what to call, what breaks. Written by browser-harness contributors after real runs, and kept on GitHub under the MIT license.

Give it to your agent

Any coding agent

Start the task with this line. The Markdown version is the same notes without the page around them.

Read https://browser-use.com/skills/wehotel.md, then <your task>

Browser Harness

Save the files into your workspace and turn domain skills on. The code below calls its helpers, such as http_get and js. What Browser Harness is.

d="${BH_AGENT_WORKSPACE:-$HOME/.config/browser-harness/agent-workspace}/domain-skills/wehotel"
mkdir -p "$d" && cd "$d"
curl -sLO https://raw.githubusercontent.com/browser-use/browser-harness/main/agent-workspace/domain-skills/wehotel/hotels.md
export BH_DOMAIN_SKILLS=1

Field-tested 2026-04-29 against www.bestwehotel.com PC web. The official direct-booking portal of the Jin Jiang group, covering ~50 sub-brands (锦江/J酒店/昆仑/丽笙/丽筠/丽柏/维也纳/锦江都城/白玉兰/麗枫/喆啡/希岸/IU/7天/锦江之星 etc.).


TL;DR

Most scrape-friendly of the three Chinese hotel portals tested:

  • Anonymous works on both list and detail pages - no login wall, no parameter-schema gate, no ¥? placeholders.
  • http_get does not work (SPA shell only). Need a browser session.
  • Browser cookies aren't required either - a freshly-opened tab via browser-harness immediately renders 200+ hotels with prices.
  • Detail pages show full rate-plan breakdown (multiple room types × breakfast/cancellation matrix) anonymously.
  • Compared to: ly.com forces login for every price; ctrip works only via a canonical 6-param URL schema or homepage-driven flow; WeHotel just works.

URL patterns

List page:
  https://www.bestwehotel.com/HotelSearch/
    ?checkinDate=YYYY-MM-DD            # NOTE: lower-case  i / d
    &checkoutDate=YYYY-MM-DD            # NOTE: lower-case  o / d
    &cityCode=AR04567                   # WeHotel internal alpha-numeric code
    &cityName=<urlencoded-Chinese>
    &queryWords=                        # optional keyword filter
    &extend=1,2,0,0,0,0                 # rooms,adults,children,...

Hotel detail page:
  https://www.bestwehotel.com/HotelDetail/
    ?hotelId=JJ1888                     # JJ<digits>; "JJ" prefix = Jin Jiang chain
    &checkInDate=YYYY-MM-DD             # NOTE: capital  I and D
    &checkOutDate=YYYY-MM-DD             # NOTE: capital  O and D
    &extend=1,2,0,0,0,0

Param-name capitalization is inconsistent between list and detail. List uses checkinDate/checkoutDate; detail uses checkInDate/checkOutDate. Get this wrong and the page silently falls back to default dates.


How to land on the list page

The home-page form has 上海 as the default destination and reasonable default dates, so two equivalent paths work:

Path A - drive the homepage form

from browser_harness.helpers import new_tab, wait_for_load, click_at_xy, js, type_text
import time
 
new_tab("https://www.bestwehotel.com/")
wait_for_load(timeout=20)
time.sleep(2)
 
# Search button is a <div> (and a sibling <a> with same coords) — coordinate click works
btn = js("""
  const b = Array.from(document.querySelectorAll('button, div, a'))
    .find(el => (el.innerText||'').trim() === '搜索' && el.offsetParent !== null);
  const r = b.getBoundingClientRect();
  return {x: r.x+r.width/2, y: r.y+r.height/2};
""")
click_at_xy(btn["x"], btn["y"])
time.sleep(8)            # XHR list fetch

Path B - build the canonical URL (preferred when you know cityCode)

url = (
    "https://www.bestwehotel.com/HotelSearch/"
    "?checkinDate=2026-04-30&checkoutDate=2026-05-01"
    "&cityCode=AR04567&cityName=%E4%B8%8A%E6%B5%B7"
    "&queryWords=&extend=1,2,0,0,0,0"
)
new_tab(url)
wait_for_load(timeout=20)
time.sleep(8)

WeHotel does not redirect-to-login when you skip provinceId/districtId/countryId-style parameters (unlike ctrip). The cityCode alone is sufficient.


List page - extracting hotels

The reliable path: walk back from 查看详情 <a> tags. Each card emits three <a> with the same hotelId href but different innerText: empty, hotel-name, and 查看详情. Walk up to the smallest ancestor containing all three.

return Array.from(document.querySelectorAll("a[href*=HotelDetail]"))
  .filter(a => (a.innerText || "").trim() === "查看详情")
  .slice(0, 30)
  .map(detailA => {
    const id = (detailA.href.match(/hotelId=([A-Z]+\d+)/i) || [])[1];
 
    // Walk up to the smallest container that includes the hotel-name <a>
    // (an anchor sharing the same hotelId href but whose text is NOT
    // "查看详情" — that text lives on the link body, not in the href, so
    // we filter by anchor identity / innerText, not by attribute selector).
    let card = detailA.parentElement;
    const hasNameAnchor = (el) =>
      Array.from(el.querySelectorAll("a[href*='" + id + "']"))
        .some(a => a !== detailA && (a.innerText || "").trim() && (a.innerText || "").trim() !== "查看详情");
    while (card && !hasNameAnchor(card)) {
      card = card.parentElement;
    }
    if (!card) return null;
 
    const text = (card.innerText || "").replace(/\s+/g, " ");
    const name = text.match(/(?:\d+\s+)?([^\s]{2,40}?(?:酒店|宾馆|大酒店|饭店))/)?.[1] || null;
    const score = text.match(/(\d\.\d)\s*\/\s*5/)?.[1] || null;
    const grade = (text.match(/(豪华型|高档型|舒适型|经济型)/) || [])[1] || null;
    const distance = (text.match(/距离市中心\s*([\d.]+)\s*km/) || [])[1] || null;
    const fromPrice = (text.match(/(?:¥|¥)\s*(\d+)\s*起/) || [])[1] || null;
    const address = (text.match(/地址:([^|]+?)距离/) || [])[1]?.trim() || null;
    const amenities = (text.match(/(停车场|餐厅|新店|游泳池|健身房|wifi)/g) || []).slice(0, 5);
 
    return {
      hotelId: id,
      name, score, grade, distance, address,
      price_from: fromPrice ? parseInt(fromPrice) : null,
      amenities,
    };
  })
  .filter(x => x && x.hotelId);

The body shows total via 查询到 N 家酒店.

Card field shape (observed)

<index>
<hotel-name>
地址:<full-address>
距离市中心 X.X km
<rating>/5分
<grade>            # 豪华型/高档型/舒适型/经济型
<amenity tags>     # 停车场/餐厅/新店/...
¥<price>起
查看详情

Detail page - extracting room rates

Detail page shows a flat table: 房型 | 早餐 | 取消政策 | 人数上限 | 房价 | <book-button>. Each row contains the rate plan and a single ¥<price> (no original/discount triplet - list rates are already "from price").

const rows = Array.from(document.querySelectorAll("[class*=room], [class*=Room]"))
  .filter(el => (el.innerText || "").includes("立即预订") || (el.innerText || "").includes("¥"))
  .slice(0, 30);
 
return rows.map(row => {
  const text = (row.innerText || "").replace(/\s+/g, " ");
  return {
    room_type: text.match(/^(\S+(?:大床房|双床房|套房|标间|双人房)\S*)/)?.[1] || null,
    breakfast: text.match(/(无早餐|含早餐|含\d份早餐|\d份早餐)/)?.[1] || null,
    cancel: text.match(/(限时取消|免费取消|不可取消|订单确认后\d+分钟内可免费取消)/)?.[1] || null,
    price: parseInt((text.match(/(?:¥|¥)\s*(\d+)/) || [])[1] || "0"),
    full: text.slice(0, 200),
  };
}).filter(r => r.price > 0);

Title pattern: stays 🟢 锦江酒店WeHotel官网 (the harness 🟢 prefix). The hotel name is in the page body, not the title - extract via the page header, which uses generic [class*=name] or [class*=title] selectors.


Brand matrix (under the WeHotel umbrella)

WeHotel covers all Jin Jiang group brands. Useful when filtering or guessing hotelId prefixes:

LUXURY (奢华尊选):    J酒店, 昆仑
PREMIUM (高端甄选):   锦江, 丽笙精选, 丽笙, 丽筠, 丽芮, 暻阁, 郁锦香, 丽柏, Park Plaza
QUALITY (精品优选):   维也纳国际/酒店/智好/3好, 非繁云居, Park Inn, Renjoy, 锦江都城, 凯里亚德, Lavande
ESSENTIALS (舒适智选): 锦江之星(品尚/风尚), 7天酒店, 7天优品, IU酒店, 派酒店, 白玉兰, 康铂, 麗枫, 喆啡, 希岸, 潮漫

All of these are bookable through the same /HotelDetail/?hotelId=JJ<n> URL.


Global state - none useful

window.__INITIAL_STATE__, __NUXT__, __NEXT_DATA__, __APOLLO_STATE__ are all absent. Hotel data is loaded into Vue/React component state via XHR, not exposed on window. Use DOM extraction.


Traps

  • checkinDate (lower-case) on list, checkInDate (capital I) on detail. Mixing them up causes silent fallback to default dates. Always copy from this doc rather than typing.
  • cityCode is alpha-numeric, not numeric. Shanghai = AR04567. There's no obvious mapping - get it once via the homepage form and cache.
  • Card has 3 <a> with the same href. First is empty (image link), second is name, third is 查看详情. Filter by inner text to dedup.
  • Default dates on the homepage shift daily. Don't trust the form's pre-filled dates - set them explicitly via URL or fill the form before clicking 搜索.
  • "价格区间-" filter in the body text is a UI element, not data - don't accidentally extract ¥? from it.

Quick start

import time, json
from browser_harness.helpers import new_tab, wait_for_load, js, cdp
 
url = (
    "https://www.bestwehotel.com/HotelSearch/"
    "?checkinDate=2026-04-30&checkoutDate=2026-05-01"
    "&cityCode=AR04567&cityName=%E4%B8%8A%E6%B5%B7"
    "&queryWords=&extend=1,2,0,0,0,0"
)
tid = new_tab(url)
wait_for_load(timeout=20)
time.sleep(8)
 
hotels = js(r"""
  return Array.from(document.querySelectorAll("a[href*=HotelDetail]"))
    .filter(a => (a.innerText||"").trim() === "查看详情")
    .slice(0, 30)
    .map(detailA => {
      const id = (detailA.href.match(/hotelId=([A-Z]+\d+)/i) || [])[1];
      let card = detailA.parentElement;
      while (card && !card.querySelector(`a[href*='${id}']:not(:where([href*='%E6%9F%A5%E7%9C%8B%E8%AF%A6%E6%83%85']))`)) {
        card = card.parentElement;
        if (!card) break;
      }
      if (!card) return null;
      const text = (card.innerText || "").replace(/\s+/g, " ");
      return {
        hotelId: id,
        name:  text.match(/(?:\d+\s+)?([^\s]{2,40}?(?:酒店|宾馆|大酒店|饭店))/)?.[1] || null,
        score: text.match(/(\d\.\d)\s*\/\s*5/)?.[1] || null,
        grade: (text.match(/(豪华型|高档型|舒适型|经济型)/) || [])[1] || null,
        distance_km: parseFloat((text.match(/距离市中心\s*([\d.]+)/) || [])[1] || "0"),
        price_from: parseInt((text.match(/(?:¥|¥)\s*(\d+)\s*起/) || [])[1] || "0") || null,
      };
    })
    .filter(x => x && x.hotelId && x.name);
""")
print(json.dumps(hotels, indent=2, ensure_ascii=False))
cdp("Target.closeTarget", targetId=tid)

hotels.md · updated May 2, 2026 · 1,125 words

Sites change. These notes describe what worked when they were last updated, not a guarantee they still do. If something broke, the fix is a pull request to browser-harness.