车来了小程序 API 接口文档

微信小程序「车来了」实时公交数据接口逆向提取报告
小程序 AppID: wx71d589ea01ce3321 域名: web.chelaile.net.cn 接口数: 15 提取日期: 2026-08-20

概览

「车来了」是中国最大的实时公交信息平台,覆盖中国大陆 488 座城市及城镇,累计用户约 3.34 亿,提供实时公交到站查询、路线规划、上下车提醒等功能[1]

用户分享的小程序路径为 #小程序://车来了/XVgIUdrGgMSdkpG,其对应的微信小程序 AppID 为 wx71d589ea01ce3321。通过分析开源项目 chelaile-mcp[2]的源码以及早期 PHP 接口提取文章[3],本文档完整提取了该小程序使用的全部后端 API 接口。

提取方法

微信小程序运行在微信客户端内部的 双线程模型 中(渲染线程 WebView + 逻辑线程 JSCore/V8),无法通过浏览器 DevTools 直接查看网络请求[4]。提取小程序接口通常采用以下三种方式之一:

  • 反编译 wxapkg — 从设备缓存获取小程序包文件(.wxapkg),使用 wxappUnpackerGwxapkg 等工具[5]反编译后静态分析 JS 源码中的 wx.request 调用
  • 动态抓包 — 使用 Charles / mitmproxy 等工具在运行小程序时拦截 HTTPS 流量,需处理 SSL Pinning 等对抗措施
  • 开源项目分析 — 从 GitHub 上的开源封装项目中直接提取已逆向的接口定义(本文档采用此方式)
数据来源说明

本文档接口信息主要来源于 GitHub 开源项目 PeanutSplash/chelaile-mcp,该项目基于 TypeScript 实现了车来了完整 API 的 MCP 服务端封装,包含了签名算法、AES 解密密钥、请求头伪装等完整实现。补充信息来自 2019 年的 PHP 接口提取文章(旧版 API 域名 api.chelaile.net.cn)。

技术架构

域名与基础路径

项目说明
BASE_DOMAINhttps://web.chelaile.net.cn主域名
BASE_URLhttps://web.chelaile.net.cn/apiAPI 基础路径(加密接口)
旧版域名https://api.chelaile.net.cn2019 年 PHP 文档中的旧版域名,部分接口路径仍兼容

默认请求参数

所有接口请求均携带以下默认参数(作为 URL Query String 拼接):

{
  s:            "h5",
  wxs:          "wx_app",
  sign:         "1",
  h5RealData:   "1",
  v:            "3.11.28",        // 小程序版本号
  src:          "weixinapp_cx",   // 来源标识
  ctm_mp:       "mp_wx",          // 渠道标识
  vc:           "2",
  favoriteGray: "1",
  gpstype:      "wgs",            // GPS 坐标系类型
  geo_type:     "wgs",
  scene:        "1256"            // 微信场景值
}

请求头伪装

车来了服务端会校验请求头指纹,篡改会导致 status:400 错误。必须使用以下请求头:

Host:            web.chelaile.net.cn
Connection:      keep-alive
User-Agent:      Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090a13) UnifiedPCWindowsWechat(0xf254160a) XWEB/18055
xweb_xhr:        1
Content-Type:    text
Accept:          */*
Sec-Fetch-Site:  cross-site
Sec-Fetch-Mode:  cors
Sec-Fetch-Dest:  empty
Referer:         https://servicewechat.com/wx71d589ea01ce3321/814/page-frame.html
Accept-Encoding:  gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
关键指纹

Referer 头中的 wx71d589ea01ce3321 是车来了小程序的 AppID,814 是小程序版本号。服务端通过此头部验证请求是否来自合法的微信小程序环境。

签名机制(cryptoSign)

除城市列表接口外,所有请求参数需要附加 cryptoSign 签名参数。签名算法为 MD5

签名算法

function cryptoSign(params):
    // 1. 将所有参数按 key 排列,格式为 "key"="value",用 & 连接
    str = params.entries()
        .map(([k, v]) => `"${k}"="${v}"`)
        .join("&")
    // 2. 末尾拼接盐值
    str += SIGN_SALT  // 盐值 = "qwihrnbtmj"
    // 3. 计算 MD5
    return md5(str)

盐值(SIGN_SALT)为:qwihrnbtmj

签名示例

原始参数: { cityId: "034", s: "h5", v: "3.11.28", src: "weixinapp_cx" }
拼接结果: "cityId"="034"&"s"="h5"&"v"="3.11.28"&"src"="weixinapp_cx"qwihrnbtmj
MD5 结果: a1b2c3d4e5f6...  (32位十六进制)

响应解密机制

部分接口的响应数据经过 AES-256-ECB 加密。加密接口的响应格式为:

{
  "jsonr": {
    "status": "00",           // "00" 表示成功
    "data": {
      "encryptResult": "Base64编码的AES密文",
      ...其他元数据
    }
  }
}

解密参数:

  • 算法:AES-256-ECB
  • 密钥FF32AE65FBFD19414EAAFF6291A54B42(32字节 UTF-8 字符串)
  • 输入编码:Base64
  • 输出编码:UTF-8

旧版接口(2019年)的响应格式使用 YGKJ## 前缀和 **YGKJ 后缀包裹 JSON 数据,解析时需去除这些标记。

接口分类

根据响应处理方式,接口分为三类:

  • PLAIN 明文接口 — 返回普通 JSON,无需签名和解密(如城市列表)
  • SIGNED 签名接口 — 需要附加 cryptoSign 签名,返回普通 JSON(如逆地理编码)
  • ENCRYPTED 加密接口 — 需要签名,且响应数据需 AES 解密(大部分核心接口)

完整接口列表

#接口名称HTTP路径类型功能
1城市列表GET/wwd/ncitylistPLAIN获取所有支持的城市
2城市配置GET/api/bus/cityMaxInterval.actionENCRYPTED获取城市刷新间隔等配置
3关键词搜索GET/api/bus/query!nSearch.actionENCRYPTED搜索线路/站点/POI
4搜索更多GET/api/bus/query!searchMore.actionENCRYPTED分类分页获取更多结果
5附近站点GET/api/bus/stop!encryptedHomePage.actionENCRYPTED获取附近站点及到站信息
6站点详情GET/api/bus/stop!encryptedPhyStnDetail.actionENCRYPTED获取站点所有线路和地铁
7线路详情GET/api/bus/line!encryptedLineDetail.actionENCRYPTED线路完整站点列表和车辆
8线路轨迹GET/api/bus/line!lineRoute.actionENCRYPTED获取线路地图轨迹坐标
9线路实时GET/api/bus/line!encryptedBusDetail.actionENCRYPTED获取到站车辆实时信息
10车辆列表GET/api/bus/line!busList.actionENCRYPTED线路上所有车辆位置
11时刻表GET/api/bus/line!preStartTimetableNew.actionENCRYPTED首末班/发车间隔/时刻表
12批量刷新GET/api/bus/line!encryptedTsfRealInfos.actionENCRYPTED批量刷新多条线路到站信息
13逆地理编码GET/api/transfer/transit!getLocationByGps.actionSIGNEDGPS坐标转中文地址
14公交换乘规划GET/api/transfer/transit!integrate.actionENCRYPTED公交+地铁换乘方案规划
15IP定位GEThttp://ip-api.com/json/EXTERNAL基于IP的城市级定位(外部API)

城市相关接口

PLAIN GET /wwd/ncitylist 城市列表

获取车来了支持的所有城市(约 480+ 个),包含城市 ID、名称、拼音、是否支持地铁等。返回普通 JSON,无需签名。

请求参数

参数类型必填说明
sstring默认平台标识,值为 "h5"
vstring默认版本号,值为 "3.11.28"
srcstring默认来源标识,值为 "weixinapp_cx"
...其余 DEFAULT_PARAMS 默认参数
响应示例(截取):
{
  "cityList": [
    {
      "cityId": "034",
      "cityName": "上海",
      "pinyin": "ShangHai",
      "supportSubway": 1,
      "isHot": 1,
      "isSupport": 1,
      "cityVersion": 3
    },
    { "cityId": "027", "cityName": "北京", ... }
  ]
}
ENCRYPTED GET /api/bus/cityMaxInterval.action 城市配置

获取某城市的运行配置:最大轮询间隔、"即将到站"时间阈值等。主要用于决定刷新频率。

请求参数

参数类型必填说明
cityIdstring城市 ID,如 "034"(上海)
localCityIdstring同 cityId
...DEFAULT_PARAMS + cryptoSign
响应示例(解密后):
{
  "maxInterval": 30,                    // 最大刷新间隔(秒)
  "arrivingStationLimitSeconds": 180,   // "即将到站"阈值(秒)
  "busDisplayConfig": {
    "lineDetail": "time#order#distance",
    "other": "time#order"
  }
}

站点相关接口

ENCRYPTED GET /api/bus/stop!encryptedHomePage.action 附近站点

根据 GPS 坐标获取附近公交站点,包含经过线路和实时到站车辆信息。这是小程序首页核心接口。

请求参数

参数类型必填说明
cityIdstring城市 ID
localCityIdstring固定为 "undefined"
latstringWGS-84 纬度
lngstringWGS-84 经度
geo_latstring同 lat
geo_lngstring同 lng
typestring固定 "5"
permissionstring固定 "0"
响应示例(解密后):
{
  "nearSts": [
    {
      "sId": "021-15232",
      "sn": "西藏中路",
      "distance": 87,
      "isSubway": 0,
      "physicalStId": "...",
      "namesakeStId": "...",
      "firstLineId": "...",
      "lines": [
        {
          "line": {
            "lineId": "...", "name": "71", "direction": 0,
            "endSn": "申昆路枢纽站",
            "firstTime": "05:30", "lastTime": "23:30",
            "price": "2元"
          },
          "targetStation": { "sId": "...", "order": 2 },
          "stnStates": [
            {
              "busId": "沪D-B1234",
              "order": 2,
              "arrivalTime": 1779070466055,
              "travelTime": 25,
              "distanceToDest": 90,
              "capacity": 0
            }
          ]
        }
      ],
      "subwayV2Lines": [
        { "line": { "lineName": "地铁2号线", "shortName": "2号线", "bgColor": "140,194,32" },
          "sublines": [{ "destName": "...", "firstTime": "05:31", "lastTime": "23:24" }] }
      ]
    }
  ]
}

字段说明capacity 拥挤度:0=空,1=适中,2=拥挤;travelTime 剩余秒数;arrivalTime 到站时间戳(毫秒),-1 表示未知。

ENCRYPTED GET /api/bus/stop!encryptedPhyStnDetail.action 站点详情

获取站点完整详情:经过该站的所有线路(含首末班、票价)、实时车辆和附近地铁线路。

请求参数

参数类型必填说明
cityIdstring城市 ID
localCityIdstring同 cityId
physicalStIdstring物理站点 ID(从搜索/附近站点获取)
namesakeStIdstring同名站点 ID
firstLineIdstring高亮的线路 ID
stationIdstring固定空字符串 ""
lat / lngstring用户位置,用于计算距离
permissionstring固定 "0"

线路相关接口

ENCRYPTED GET /api/bus/line!encryptedLineDetail.action 线路详情

获取线路完整信息:站点列表、当前运营车辆、反方向线路、发车信息。

请求参数

参数类型必填说明
cityIdstring城市 ID
localCityIdstring同 cityId
lineIdstring线路 ID(从搜索/附近站点获取)
lat / lngstring用户位置
geo_lat / geo_lngstring同 lat / lng
响应示例(解密后):
{
  "line": {
    "lineId": "21283603183",
    "name": "71",
    "direction": 0,
    "startSn": "延安东路外滩",
    "endSn": "申昆路枢纽站",
    "firstTime": "05:30",
    "lastTime": "23:30",
    "price": "2元",
    "stationsNum": 20
  },
  "stations": [
    { "order": 0, "sId": "...", "sn": "延安东路外滩",
      "wgsLat": 31.231, "wgsLng": 121.474,
      "physicalStId": "...", "namesakeStId": "...",
      "metros": [] }
  ],
  "buses": [
    { "busId": "沪D-B1234", "order": 5, "lat": 31.232, "lng": 121.475,
      "speed": 30, "capacity": 0 }
  ],
  "otherlines": [
    { "lineId": "21283603182", "startSn": "申昆路枢纽站",
      "endSn": "延安东路外滩" }
  ],
  "depDesc": "下一班10:12发车",
  "preArrivalTime": "10:12"
}
地铁线路注意

地铁线路 ID(如 "1057")调用此接口会返回空数据壳。获取地铁首末班时间应通过附近站点接口的 subwayV2Lines 字段。

ENCRYPTED GET /api/bus/line!lineRoute.action 线路轨迹

获取线路在地图上的轨迹坐标点列表,用于绘制线路走向。

请求参数

参数类型必填说明
cityIdstring城市 ID
lineIdstring线路 ID
响应示例(解密后):
{
  "route": [
    { "lat": 31.231, "lng": 121.474, "stopOrder": 0 },
    { "lat": 31.232, "lng": 121.475 },
    // ... 通常400-500个坐标点
    { "lat": 31.245, "lng": 121.501, "stopOrder": 19 }
  ]
}
ENCRYPTED GET /api/bus/line!encryptedBusDetail.action 线路实时车辆

获取即将到达目标站点的车辆实时信息,包含车牌、位置、速度、ETA。

请求参数

参数类型必填说明
cityIdstring城市 ID
lineIdstring线路 ID
targetOrderstring目标站点在线路上的序号
stationIdstring目标站点 sId
lat / lngstringWGS-84 坐标(可使用站点坐标)
geo_lat / geo_lngstring同 lat / lng
响应示例(解密后):
{
  "buses": [
    {
      "busId": "沪D-B1234", "licence": "沪D-B1234",
      "order": 5, "lat": 31.232, "lng": 121.475,
      "speed": 30, "capacity": 0,
      "distanceToWaitStn": 500,
      "travels": [
        { "travelTime": 180, "arrivalTime": 1779070466055,
          "recommTip": "约3分钟" }
      ]
    }
  ],
  "line": { "lineId": "...", "name": "71", "endSn": "..." },
  "targetOrder": 2,
  "realData": true
}
ENCRYPTED GET /api/bus/line!busList.action 车辆列表

获取线路上所有运营车辆的位置和载客率。

请求参数

参数类型必填说明
cityIdstring城市 ID
lineIdstring线路 ID
targetOrderstring目标站点序号
stationNamestring目标站点显示名称
ENCRYPTED GET /api/bus/line!preStartTimetableNew.action 时刻表

获取线路时刻表信息:首末班时间、发车间隔或完整时刻表。

请求参数

参数类型必填说明
cityIdstring城市 ID
lineIdstring线路 ID
lineNostring线路短名(如 "71",非内部 lineNo)
directionenum方向:"0""1"
响应示例(解密后):
{
  "line": {
    "lineId": "...", "name": "71", "direction": 0,
    "startSn": "延安东路外滩", "endSn": "申昆路枢纽站"
  },
  "timeTableType": 1,   // 1=按时刻表, 2=按间隔, 3=特殊
  "timetable": [...],  // timeTableType=1时有详细时刻表
  "scheduleTags": [...]
}
ENCRYPTED GET /api/bus/line!encryptedTsfRealInfos.action 批量刷新

一次性批量刷新多个 (线路, 站点) 对的实时信息,用于多线路同时监控。

请求参数

参数类型必填说明
cityIdstring城市 ID
localCityIdstring同 cityId
lineStnstring格式见下方说明
lineStn 参数格式:
// 格式: lineId,stopId,nextId,targetOrder
// 多组用分号 ; 分隔,nextId 可为空
// 示例:
"21283603183,021-15232,,2;21283603184,021-15233,,3"

地理编码接口

SIGNED GET /api/transfer/transit!getLocationByGps.action 逆地理编码

将 WGS-84 经纬度转换为中文地址(省、市、区、乡镇)。需要签名但返回普通 JSON。

请求参数

参数类型必填说明
latstringWGS-84 纬度
lngstringWGS-84 经度
geo_lat / geo_lngstring同 lat / lng
gpsTypestring固定 "wgs"
gpstypestring固定 "wgs"
geo_typestring固定 "wgs"
响应示例:
{
  "regeocode": {
    "formatted_address": "上海市黄浦区...",
    "addressComponent": {
      "province": "上海市",
      "city": "上海市",
      "district": "黄浦区",
      "township": "南京东路街道",
      "citycode": "021",
      "adcode": "310101"
    }
  }
}

换乘规划接口

ENCRYPTED GET /api/transfer/transit!integrate.action 公交+地铁换乘规划

规划两点之间的公交+地铁换乘路线,返回多个方案,每个方案包含步行段和乘车段。

坐标系要求

此接口要求 GCJ-02(火星坐标),不是 WGS-84。建议从搜索接口的 pois 字段获取坐标(已为 GCJ-02)。

请求参数

参数类型必填说明
cityIdstring城市 ID
origin_namestring起点名称
origin_latstring起点纬度(GCJ-02)
origin_lngstring起点经度(GCJ-02)
dest_namestring终点名称
dest_latstring终点纬度(GCJ-02)
dest_lngstring终点经度(GCJ-02)
gpstypestring固定 "gcj"
geo_typestring固定 "gcj"
strategyenum"0"=推荐, "1"=少换乘, "2"=少步行, "3"=最短时间(仅公交)
isSelectTimestring固定 "0"
departure_timestring当前时间戳(毫秒)
响应示例(解密后):
{
  "route": {
    "origin": "121.490,31.233",
    "destination": "121.373,31.197",
    "distance": "17982",
    "transits": [
      {
        "duration": "3056",        // 总耗时(秒)
        "walking_distance": "1466",
        "distance": "19086",
        "cc": 1,                  // 换乘次数
        "tag": "直达",
        "segments": [
          {
            "walking": { "distance": "837", "duration": "717" }
          },
          {
            "bus": {
              "buslines": [{
                "name": "地铁2号线",
                "lineType": 1,   // 0=公交, 1=地铁
                "color": "...",
                "departure_stop": { "name": "人民广场" },
                "arrival_stop": { "name": "虹桥2号航站楼" },
                "via_num": "8",
                "station_start_time": "0537",
                "station_end_time": "2330",
                "duration": "1800",
                "distance": "17620"
              }]
            }
          }
        ]
      }
    ]
  }
}

Python 代码示例

以下 Python 代码演示了如何调用车来了 API,包含签名生成、AES 解密和请求发送的完整流程。

完整调用示例

import hashlib
import json
import requests
from Crypto.Cipher import AES
from urllib.parse import urlencode

# ===== 常量定义 =====
BASE_DOMAIN = "https://web.chelaile.net.cn"
BASE_URL = f"{BASE_DOMAIN}/api"
SIGN_SALT = "qwihrnbtmj"
AES_KEY = b"FF32AE65FBFD19414EAAFF6291A54B42"

DEFAULT_PARAMS = {
    "s": "h5",
    "wxs": "wx_app",
    "sign": "1",
    "h5RealData": "1",
    "v": "3.11.28",
    "src": "weixinapp_cx",
    "ctm_mp": "mp_wx",
    "vc": "2",
    "favoriteGray": "1",
    "gpstype": "wgs",
    "geo_type": "wgs",
    "scene": "1256",
}

REQUEST_HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 "
                  "MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI "
                  "MiniProgramEnv/Windows WindowsWechat/WMPF "
                  "WindowsWechat(0x63090a13) XWEB/18055",
    "Content-Type": "text",
    "Referer": "https://servicewechat.com/wx71d589ea01ce3321/814/page-frame.html",
    "Accept": "*/*",
    "Accept-Language": "zh-CN,zh;q=0.9",
}

# ===== 签名生成 =====
def crypto_sign(params: dict) -> str:
    """生成 cryptoSign 签名"""
    # 按 key 排列,格式为 "key"="value",用 & 连接
    str_to_sign = '&'.join(
        f'"{k}"="{v}"' for k, v in params.items()
    ) + SIGN_SALT
    return hashlib.md5(str_to_sign.encode()).hexdigest()

# ===== AES 解密 =====
def decrypt_result(ciphertext: str) -> dict:
    """解密 AES-256-ECB 加密的响应数据"""
    cipher = AES.new(AES_KEY, AES.MODE_ECB)
    decrypted = cipher.decrypt(
        __import__('base64').b64decode(ciphertext)
    )
    # 去除 PKCS7 padding
    pad_len = decrypted[-1]
    decrypted = decrypted[:-pad_len]
    return json.loads(decrypted.decode('utf-8'))

# ===== 响应解析 =====
def parse_encrypted_response(raw_text: str) -> dict:
    """解析加密响应"""
    # 找到 JSON 起始位置
    json_start = raw_text.index('{')
    # 匹配 JSON 大括号
    depth = 0
    json_end = json_start
    for i in range(json_start, len(raw_text)):
        if raw_text[i] == '{': depth += 1
        elif raw_text[i] == '}': depth -= 1
        if depth == 0:
            json_end = i + 1
            break
    envelope = json.loads(raw_text[json_start:json_end])
    data = envelope.get("jsonr", {}).get("data", {})
    if "encryptResult" in data:
        return decrypt_result(data["encryptResult"])
    return data

# ===== 发送加密请求 =====
def request_encrypted(url: str, params: dict) -> dict:
    """发送加密接口请求"""
    # 附加签名
    params_with_sign = {**params, "cryptoSign": crypto_sign(params)}
    full_url = f"{url}?{urlencode(params_with_sign)}"
    resp = requests.get(full_url, headers=REQUEST_HEADERS, timeout=15)
    return parse_encrypted_response(resp.text)

# ===== 发送签名请求(明文响应) =====
def request_signed(url: str, params: dict) -> dict:
    """发送签名接口请求(响应为普通 JSON)"""
    params_with_sign = {**params, "cryptoSign": crypto_sign(params)}
    full_url = f"{url}?{urlencode(params_with_sign)}"
    resp = requests.get(full_url, headers=REQUEST_HEADERS, timeout=15)
    return json.loads(resp.text)

# ===== 发送明文请求 =====
def request_plain(url: str, params: dict) -> dict:
    """发送明文接口请求"""
    full_url = f"{url}?{urlencode(params)}"
    resp = requests.get(full_url, headers=REQUEST_HEADERS, timeout=15)
    return json.loads(resp.text).get("data", {})

# ===== 使用示例 =====
if __name__ == "__main__":
    # 1. 获取城市列表
    cities = request_plain(
        f"{BASE_DOMAIN}/wwd/ncitylist",
        {**DEFAULT_PARAMS}
    )
    print(f"支持城市数: {len(cities.get('cityList', []))}")

    # 2. 搜索线路
    search_result = request_encrypted(
        f"{BASE_URL}/bus/query!nSearch.action",
        {**DEFAULT_PARAMS,
         "cityId": "034",        # 上海
         "localCityId": "034",
         "key": "71",
         "supportPhyStn": "true"}
    )
    lines = search_result.get("result", {}).get("lines", [])
    print(f"找到线路数: {len(lines)}")
    for line in lines:
        print(f"  {line.get('name')} -> {line.get('endSn')}")

    # 3. 获取附近站点
    nearby = request_encrypted(
        f"{BASE_URL}/bus/stop!encryptedHomePage.action",
        {**DEFAULT_PARAMS,
         "cityId": "034",
         "localCityId": "undefined",
         "lat": "31.230416",
         "lng": "121.473701",
         "geo_lat": "31.230416",
         "geo_lng": "121.473701",
         "type": "5",
         "permission": "0"}
    )
    stops = nearby.get("nearSts", [])
    print(f"附近站点数: {len(stops)}")
    for stop in stops[:3]:
        print(f"  {stop.get('sn')} ({stop.get('distance')}m)")
依赖安装

运行上述代码需要安装:pip install requests pycryptodome --break-system-packages