Ctrip (携程)

July 20, 2026 · View on GitHub

Mode: 🌐 Public (search, hotel-suggest) · 🖥️ Browser + Cookie (hotel-search, hotel, flight, flight-round, train, bus, ferry, cruise, tour, package, attraction) Domain: ctrip.com

Public destination + hotel-context suggestion lookup against the m.ctrip.com/restapi/soa2/21881/json/gaHotelSearchEngine endpoint plus browser-driven hotel listing and one-way flight search on hotels.ctrip.com and flights.ctrip.com.

Commands

CommandModeDescription
opencli ctrip searchPublicSuggest cities, scenic spots, railway stations and landmarks
opencli ctrip hotel-suggestPublicSuggest cities, business areas and individual hotels
opencli ctrip hotel-searchBrowser (cookie)List hotels for a city + check-in/out date range
opencli ctrip hotelBrowser (cookie)Single-hotel detail: rating breakdown, facilities, check-in/out policy
opencli ctrip flightBrowser (cookie)One-way flight search by IATA route + departure date
opencli ctrip flight-roundBrowser (cookie)Round-trip flight search by IATA route + depart/return dates
opencli ctrip trainBrowser (cookie)Train ticket search by station/city name + departure date
opencli ctrip busBrowser (cookie)Intercity coach ticket search by city name + departure date
opencli ctrip ferryBrowser (cookie)Passenger ferry sailing search by city name + departure date
opencli ctrip cruiseBrowser (cookie)Cruise package search by departure port name
opencli ctrip tourBrowser (cookie)Group / self-guided tour package search by destination keyword
opencli ctrip packageBrowser (cookie)Flight-plus-hotel (自由行) package search by destination keyword
opencli ctrip attractionBrowser (cookie)Top attractions for a city id (rating, review count, detail link)

Usage Examples

# Destination suggest
opencli ctrip search 苏州 --limit 10

# Hotel-context suggest (cities / business areas / hotels)
opencli ctrip hotel-suggest 陆家嘴 --limit 5

# Hotel listing (city ID from `search` / `hotel-suggest`)
opencli ctrip hotel-search 2 --checkin 2026-05-20 --checkout 2026-05-21 --limit 10

# Single-hotel detail (hotel id from `hotel-suggest`)
opencli ctrip hotel 375539
opencli ctrip hotel 375539 -f json

# One-way flight search
opencli ctrip flight BJS SHA --date 2026-05-20 --limit 20

# Round-trip flight search (depart + return dates)
opencli ctrip flight-round SHA BJS --depart 2026-08-15 --return 2026-08-22 --limit 20

# Train ticket search (station or city names)
opencli ctrip train 北京 上海 --date 2026-05-20 --limit 20
opencli ctrip train 杭州 上海虹桥 --date 2026-05-20 -f json

# Intercity coach ticket search (city names)
opencli ctrip bus 北京 天津 --date 2026-05-20 --limit 20

# Passenger ferry search (city names)
opencli ctrip ferry 大连 烟台 --date 2026-05-20 --limit 20

# Cruise package search (departure port name)
opencli ctrip cruise 上海 --limit 20

# Tour package search (destination keyword)
opencli ctrip tour 北京 --limit 20

# Flight-plus-hotel package search (destination keyword)
opencli ctrip package 三亚 --limit 20

# Top attractions for a city (numeric city id from `ctrip search`, e.g. 1 for 北京)
opencli ctrip attraction 1 --limit 20

# JSON output
opencli ctrip search 上海 -f json

Suggest Columns (search / hotel-suggest)

Both suggest commands share a uniform column shape:

ColumnNotes
rank1-based position in the upstream list
idUpstream entity id (round-trips into URL)
typeRaw type tag (City / Markland / Hotel / BusinessArea / RailwayStation)
displayTypeLocalised label (城市 / 地标 / 酒店 / 商圈 / 火车站)
nameLocalised display name
eNameEnglish name (may be empty)
cityId, cityName, provinceName, countryNameGeo context
lat, lonBest-available coords (gaode → google → flat → null)
scoreFirst non-zero of commentScore / cStar; null if both unrated
urlCanonical Ctrip URL or null if the entity type has no public web page

--limit accepts integers in [1, 50]. Out-of-range values raise ArgumentError (no silent clamp).

ColumnNotes
rank1-based position in upstream list
hotelIdRound-trips into https://hotels.ctrip.com/hotels/detail/?hotelid=…
name, enNameLocalised + English (English may be null)
star1-5, null for unrated / 客栈 entries
score, scoreLabele.g. 4.8 / "超棒"; both null if unrated
reviewCountInteger parsed from "13,966条点评"
cityName, district, addressGeo context
lat, lonWGS84 (1) > GCJ02 (2) > BD09 (3) selection; null if all are 0
price, currencyFirst room's quote; null when no rooms remain at the searched date
urlCanonical detail URL or null if hotelId is missing

Args:

  • <city> (positional, required) — numeric Ctrip city ID (discover via ctrip search / ctrip hotel-suggest).
  • --checkin, --checkout (required) — YYYY-MM-DD, validated as real calendar dates with checkin < checkout.
  • --limit (1-30, default 10) — Ctrip's SSR first page ships ~13 entries (10 organic + ~3 promoted). Larger limits are not currently supported because the server ignores the URL pageSize param.

Flight Columns (flight)

ColumnNotes
rank1-based position after filtering incomplete rows
airline, flightNo, aircraftFree-text from the rendered card; flightNo and aircraft may be null (the current .flight-item cards often omit the flight number)
departureTime, arrivalTimeHH:MM strings
departureAirport, arrivalAirport, terminalAirport names + optional T1/T2 chunk
price, currency, cabinFirst quoted fare; cabin is the Chinese suffix (e.g. 经济舱)
urlThe search URL (Ctrip's flight cards don't expose per-row stable deeplinks)

Args:

  • <from>, <to> (positional, required) — 3-letter IATA codes; BJS/SHA metro codes work alongside single-airport codes like PEK/PVG.
  • --date (required) — YYYY-MM-DD.
  • --limit (1-50, default 20).

Rows are extracted from the rendered .flight-item cards (Ctrip migrated the flight list to these; they omit a text flight number, so flightNo is often null) because Ctrip's post-load XHR is not currently captured by the daemon network buffer (see "Caveats" below). Cards with missing departure/arrival/airline are dropped rather than emitted with sentinel values.

Round-Trip Flight Columns (flight-round)

flight-round returns the outbound (去程) leg of a round-trip search with the same column shape as flight (rank, airline, flightNo, aircraft, departureTime, departureAirport, arrivalTime, arrivalAirport, terminal, price, currency, cabin, url), read from the .flight-item cards on the round-<from>-<to> results page.

Args:

  • <from>, <to> (positional, required): 3-letter IATA codes (BJS/SHA metro codes work alongside single-airport codes like PEK/PVG).
  • --depart, --return (required): YYYY-MM-DD, with return on or after depart.
  • --limit (1-50, default 20).

Two differences from the one-way list: price is the round-trip total (往返总价) for the outbound flight shown, and the round-trip cards omit the flight number, so flightNo and aircraft are usually null (never a sentinel). Picking the return leg is a second step in Ctrip's flow and is out of scope here.

Train Columns (train)

ColumnNotes
rank1-based position after filtering incomplete rows
trainNoTrain number (G531 / D701 / K528); the .checi icon suffix is stripped
departureTime, arrivalTimeHH:MM strings
departureStation, arrivalStationStation names (e.g. 北京南, 上海虹桥)
durationTrip length as shown (e.g. 5时56分); null if absent
fromPriceLowest fare shown for the train as a number; null if non-numeric
seatsSeat-class availability joined by / (e.g. 二等座有票 / 一等座17张 / 商务座(抢))
urlThe search URL (train rows share the list page, no per-row deeplink)

Args:

  • <from>, <to> (positional, required): Chinese station or city names (e.g. 北京 / 上海虹桥); the list page resolves them the same way the website search box does.
  • --date (required): YYYY-MM-DD.
  • --limit (1-50, default 20).

Rows come from .card-white.list-item cards, read by stable class-keyed fields (.from/.mid/.to/.rbox/.surplus-list) rather than positional innerText. Cards missing the train number or endpoint times are dropped rather than emitted with sentinel values.

Bus Columns (bus)

ColumnNotes
rank1-based position after filtering incomplete rows
departureTimeHH:MM departure time
fromStation, toStationDeparture and arrival coach stations
durationTrip length as shown (e.g. 约2时30分); null if absent
priceFare as a number; null if non-numeric
statusAvailability text (e.g. 暂停网售, or a remaining-ticket count)
urlThe results URL (coach rows share the list page, no per-row deeplink)

Args:

  • <from>, <to> (positional, required): Chinese city names (e.g. 北京 / 天津); the results page returns the station-level departures between them.
  • --date (required): YYYY-MM-DD.
  • --limit (1-50, default 20).

The bus.ctrip.com/ landing is a client-only SPA that does not hydrate under the browser bridge, so the command navigates the results route directly via its ?param=<json> deep link. Coach rows arrive through the busListV2 XHR and are read from .list-item-parent cards by stable utility-class fields rather than positional innerText.

Ferry Columns (ferry)

ColumnNotes
rank1-based position after filtering incomplete rows
shipNameVessel name (e.g. 渤海晶珠); null if absent
departureTime, arrivalTimeHH:MM strings
fromPort, toPortDeparture and arrival passenger ports
durationTrip length as shown (e.g. 6时30分); null if absent
priceLowest fare as a number; null if non-numeric
statusAvailability text (e.g. 选择舱位, 售罄)
urlThe results URL (sailings share the list page, no per-row deeplink)

Args:

  • <from>, <to> (positional, required): Chinese city names (e.g. 大连 / 烟台); the results page returns the port-level sailings between them.
  • --date (required): YYYY-MM-DD.
  • --limit (1-50, default 20).

Sibling of bus: the ship.ctrip.com/ landing is a client-only SPA that does not hydrate under the browser bridge, so the command navigates the results route directly via its ?param=<json> deep link. Sailings arrive through the getShipLineV2 XHR and are read from .list-item-parent cards by stable class-keyed fields rather than positional innerText.

Cruise Columns (cruise)

ColumnNotes
rank1-based position after filtering incomplete rows
titleLine, ship, and itinerary (e.g. MSC地中海邮轮·荣耀号·上海-那霸(冲绳)-上海·5天4晚)
starProduct star rating (1-5); null if unrated
boardingBoarding / disembarking note (e.g. 上海登船/离船)
sailingDateNext recommended sailing date; null if absent
tagsFeature tags joined by / (e.g. 免签 / 岸上游 / 船上餐饮)
priceLowest per-person fare as a number; null if non-numeric
urlThe port results URL (cruises share the list page, no per-row deeplink)

Args:

  • <port> (positional, required): a departure cruise port name (e.g. 上海 / 威尼斯 / 罗马), matched against the ports Ctrip lists (上海 carries most China departures, plus international ports).
  • --limit (1-50, default 20).

Cruise results live on the legacy newpackage/search/sN.html pages keyed by an opaque per-port code, so the command first loads the 上海 page (which lists every port as a link) to resolve the requested port name to its code, then loads that port's results and reads the .route_info cards. A listed port with no current sailings raises EmptyResultError. River cruises (三峡) are a separate product and out of scope.

Tour Columns (tour)

ColumnNotes
rank1-based position after filtering incomplete rows
titlePackage title (e.g. 北京5日4晚跟团游)
subtitleHighlights line; null if absent
tagsFeature tags joined by / (e.g. 0购物 / 成团保障 / 亲子甄选)
scoreRating out of 5; null if unrated
soldUnits sold as an integer (from 已售N); null if absent
reviewsReview count as an integer; null if absent
priceLowest per-person fare as a number; null if non-numeric
urlThe search results URL (packages share the list page, no per-row deeplink)

Args:

  • <destination> (positional, required): a destination keyword (e.g. 北京 / 三亚 / 马尔代夫), passed to the vacations search as sv.
  • --limit (1-50, default 20).

Results render server-side into .list_product_item cards; the price / score / sold fields lazy-load a moment after the titles, so the command waits until every rendered card carries a price before reading. A destination with no packages raises EmptyResultError.

Package Columns (package)

package searches the 自由行 (flight-plus-hotel) tab of the same vacations search as tour and returns the identical column shape (rank, title, subtitle, tags, score, sold, reviews, price, url); it differs only in the product section (freetravel rather than whole).

Args:

  • <destination> (positional, required): a destination keyword (e.g. 三亚 / 北京 / 曼谷).
  • --limit (1-50, default 20).

Attraction Columns (attraction)

ColumnNotes
rank1-based position in the rendered list
nameAttraction name
ratingGuest rating out of 5; null when the card lists only a review count
reviewsReview count as an integer (19.7w / 1.3万 expanded to thousands); null if absent
urlThe attraction's you.ctrip.com/sight/<city>/<id>.html detail page

Args:

  • <city> (positional, required): a numeric Ctrip city id (discover via ctrip search; e.g. 1 for 北京, 2 for 上海).
  • --limit (1-50, default 20).

The you.ctrip.com place page routes by the trailing numeric city id (redirecting any slug to the canonical one) and lists a destination's top-rated attractions as /sight/<city>/<id>.html links carrying the name, rating, and review count. Rows anchor on those stable links, deduped by sight id, and drop links without a name. Per-attraction ticket prices sit on each sight's own detail page and are out of scope here.

Hotel Detail Columns (hotel)

ColumnNotes
hotelIdEchoes the requested id
name, enNameLocalised + English (English may be null)
star1-5, null for unrated / 客栈 entries
score, scoreLabelOverall rating (e.g. 4.8 / "超棒"); both null if unrated
reviewCountTotal review count as an integer
ratingBreakdownThe four sub-scores joined by / (e.g. 卫生 4.8 / 设施 4.8 / 环境 4.8 / 服务 4.8)
facilitiesHot facilities joined by / (e.g. 接机服务 / 无线WIFI免费 / 行李寄存)
checkInOutCheck-in / check-out policy lines joined by /
cityName, addressGeo context
lat, lonCoordinates from the detail page; null if absent
urlCanonical detail URL

Args:

  • <id> (positional, required): numeric Ctrip hotel id (discover via ctrip hotel-suggest; e.g. 375539).

The profile is read from __NEXT_DATA__.props.pageProps.hotelDetailResponse (the same SSR source style as hotel-search), surfacing the fields the listing row does not carry. Room-level nightly prices load via a post-SSR XHR and are out of scope here, the same way flight's post-load price XHR is; hotel-search already reports a representative nightly price per hotel.

Notes

  • Suggest endpoint discriminator: searchType=D (search) vs searchType=H (hotel-suggest). Hotel and BusinessArea rows only appear in the H flavour.
  • Mainland China suggest rows ship gdLat/gdLon (gaode). International rows ship gLat/gLon (wgs84). The adapter picks the first non-zero pair.
  • Suggest in-band Result: false envelopes are surfaced as COMMAND_EXEC typed errors; HTTP non-2xx becomes FETCH_ERROR.

Caveats (browser-mode commands)

  • Cookie required: hotel-search / flight use Strategy.COOKIE against hotels.ctrip.com / flights.ctrip.com. If Ctrip serves a captcha redirect (suspected bot), an AuthRequiredError is raised — complete the captcha in your live browser session and retry.
  • No per-flight deeplink: Ctrip's flight cards funnel every row through a shared booking handoff. Until a stable per-flight bookingId surfaces, all rows share the search URL.
  • Round-trip + airline-filter unsupported: flight is one-way only and passes cabin=Y_S_C_F (all cabins) in v1. Round-trip + advanced filters tracked in the #1481 follow-up.
  • Hotel SSR page size is server-fixed: passing &pageSize=N is ignored upstream — first page returns ~13 rows. Larger result sets would need scroll-paginated DOM extraction (not implemented in v1).