Skip to content

从零构建:途虎养车数据爬虫实战指南

约 3085 字大约 10 分钟

研究

2025-08-08

本文将以途虎养车平台为例,搭建一个高效、稳定的爬虫系统。内容涵盖从需求分析、技术选型到反爬策略应对的全流程,并提供可复用的代码示例与架构设计思路。

前言

最近,由于需要了解汽车滤芯更换套餐的市场行情,我决定通过爬虫技术从途虎养车平台采集相关数据。选择途虎养车作为目标平台主要有两个原因:

  1. ​行业地位​​:途虎养车是国内领先的汽车后市场服务平台,拥有丰富的商品和服务数据,具有较高的参考价值。
  2. ​技术可行性​​:与其他完全依赖APP的汽车服务平台不同,途虎仍然维护了网页端服务,使得数据抓取成为可能。

在调研过程中,我发现途虎的网页端仍然可用:途虎养车网页版,这为后续的爬虫开发提供了便利。

链接抓取

为了分析途虎养车的数据请求,我们可以使用浏览器的开发者工具进行抓包。首先在Chrome或Firefox中打开途虎养车网页端(途虎养车手机版),按下F12键调出开发者工具,切换到Network面板(网络)并确保选中All选项。

开发者模式
开发者模式

在正式抓取数据前,我们需要先完成几个关键操作步骤。首先登录途虎养车账号,在个人中心选择对应的车辆信息,然后进入"保养"服务页面。待页面完全加载后,系统会展示各类保养套餐信息。此时开发者工具已经记录了所有网络请求,我们需要特别关注那些返回JSON格式数据的API接口,这些往往包含我们需要的核心信息。

对于初学者来说,可以通过逐个查看接口的响应内容来定位目标数据。以当前页面为例,在Network面板中筛选请求后,可以清楚地看到返回的JSON数据中包含了我们所需的保养套餐详情,包括项目名称、价格、维护项目以及使用产品等关键字段。这个发现过程为后续编写爬虫代码提供了明确的数据接口定位。

目标链接
目标链接

我们可以通过以下方式获取目标链接:在开发者工具的Network面板中,找到包含所需数据的请求后,直接双击该请求行,浏览器会自动在新标签页中打开该请求的响应内容。此时地址栏显示的URL就是我们需要的目标API链接,这个链接将作为后续爬虫程序直接请求的数据接口。

链接分析

我们获得了以下链接:

https://maint-api.tuhu.cn/apinew/GetBaoYangAppPackages?channel=kH5&activityId=&city=上海市&province=上海市&lngBegin=119.80062593300364&latBegin=33.14440913557059&vehicle={"CarId":"ba12c20d-9256-4117-bade-47d47b68e822","PaiLiang":"电动","OnRoadTime":"","VehicleId":"VE-TSLY","tireSize":"","Properties":[],"Nian":"2025","Distance":0,"Tid":"160821"}&baoYangTypes=&isDefaultExpand=true&userId=0bdf60a3-ff01-459d-8621-de160851eed8&productIds=

我们需要深入解析这个API链接的结构,以确定哪些参数是必须的、哪些是可选的,从而构建出可批量爬取的请求链接。通过拆解这个URL,我们可以清晰地看到途虎养车API的参数设计逻辑:

核心参数分类说明

  1. ​基础定位参数​
    • city/province:省市信息(需URL编码)
    • lngBegin/latBegin:经纬度坐标(支持6位小数)
    • 示例:city=上海city=%E4%B8%8A%E6%B5%B7
  2. ​车辆身份参数​
    {
      "CarId": "ba12c20d-...",  // 车辆唯一标识
      "VehicleId": "VE-TSLY",    // 车型编码
      "PaiLiang": "电动",        // 动力类型
      "Nian": "2025"            // 年款
    }
  3. ​用户会话参数​
    • userId:用户唯一标识(32位UUID格式)
    • channel:渠道标识(kH5表示H5页面)
  4. ​业务筛选参数​
    • baoYangTypes:保养类型过滤
    • productIds:指定商品ID查询

首先观察基础URL部分:https://maint-api.tuhu.cn/apinew/GetBaoYangAppPackages,这是所有请求的入口端点。紧随其后的问号表示开始查询参数,这些参数使用标准的key=value格式,以&符号分隔。在这些参数中,有些是必填的核心参数,有些则是可选的辅助参数。

必填参数包括:

  • 地理位置参数:cityprovince需要填写具体的省市名称,建议使用URL编码格式
  • 车辆标识参数:vehicle是一个JSON字符串,必须包含有效的CarId等车辆信息
  • 用户标识:userId虽然是UUID格式,但在未登录状态下可以使用默认值

可选参数包括:

  • 坐标参数:lngBeginlatBegin可以留空或使用默认值
  • 筛选参数:baoYangTypesproductIds可以留空获取全部结果
  • 活动参数:activityId通常可以留空

特别需要注意的是channel=kH5这个参数,它标识请求来源,保持这个值可以避免一些反爬检测。在实际批量爬取时,我们主要需要动态替换的参数是cityprovincevehicle中的车辆信息,其他参数可以保持固定值。通过这种参数分析,我们就可以设计出可批量请求的URL模板,只需替换关键参数即可获取不同地区、不同车型的保养套餐数据。

必要参数获取

这一步将使用Python去获得目标参数,并解决途虎平台"最多只能添加5辆车"的限制问题。我们需要做以下几步:

  1. 获取途虎养车的品牌、车型、排量等基础数据
  2. 绕过5辆车限制实现大批量数据爬取
  3. 构建有效的API请求
  4. 处理和存储爬取结果 爬取流程如下:

环境准备

安装必要库

pip install requests

核心代码解析

配置参数

# 基本配置
USER_ID = "0bdf60a3-ff01-459d-8621-de160851eed8"  # 用户ID
CITY = "上海市"  # 默认城市
PROVINCE = "上海市"  # 默认省份

# 请求控制
RETRY_COUNT = 3  # 请求重试次数
DELAY_BETWEEN_REQUESTS = 3  # 请求间隔时间(秒)

请求头设置

headers = {
    "User-Agent": "Mozilla/5.0...",
    "Content-Type": "application/json",
    "Authorization": "Bearer 204576661ec744519fcd3c2714a850ad",
    # 其他必要headers...
}

请求头在爬虫中的作用至关重要,它就像是网络请求的身份证和通行证,决定了服务器是否会接受并响应你的请求。在途虎养车这样的平台爬取数据时,请求头不仅需要包含基本的身份标识信息,还需要模拟真实用户的行为特征,以避免被识别为自动化程序而遭到拦截。一个典型的请求头会包含User-Agent来伪装成普通浏览器,携带Authorization和Cookie来维持登录状态,设置Content-Type来声明数据格式,还可能包含一些平台特定的验证字段如blackbox或TongDun-TokenId等反爬机制相关的信息。这些头部信息共同构成了一个完整的请求身份,缺少任何一个关键字段都可能导致请求失败。

车辆管理功能

获取当前车辆列表

def get_car_list() -> List[Dict]:
    """获取当前账户下的车辆列表"""
    url = "https://cl-gateway.tuhu.cn/cl-user-info-site/myCar/getCarListByUserId"
    payload = {"userId": USER_ID}
    
    response = requests.post(url, headers=headers, json=payload)
    if response.json().get("code") == 10000:
        return response.json().get("data", [])
    return []
  • 这里的url也需要通过上面的F12开发者模式的网络请求中获取,即调用哪个api才能获取汽车列表。我们在汽车列表页面对网络请求进行抓取就会获得以上的api,通过发送我们的userID即可获得到所有的汽车品牌。
    image.png

删除指定车辆

因为途虎只允许未认证的情况下添加5辆汽车,限制了我们爬取全部汽车的信息,这个时候我们就需要在获得一辆汽车的信息后删除这辆车,反复执行添加删除车辆,直到获得所有车的信息。

提示

这里的url是途虎的车辆删除端口,通过F12抓取,发送carIDuserID就可以删除你的汽车。

def delete_car(car_id: str) -> bool:
    """删除指定车辆"""
    url = "https://cl-gateway.tuhu.cn/cl-user-info-site/myCar/removeCar"
    payload = {"carId": car_id, "userId": USER_ID}
    
    response = requests.post(url, headers=headers, json=payload)
    return response.json().get("code") == 10000

添加新车

通过添加新车,我们可以获得汽车的carId,我们需要给url发送payload中的信息就可以获取:

def add_vehicle(model_code: str, year: int, tid: int) -> Optional[str]:
    """添加新车并返回carId"""
    url = "https://cl-gateway.tuhu.cn/cl-user-info-site/myCar/addCar"
    payload = {
        "modelCode": model_code,
        "productionYear": year,
        "tid": tid,
        "source": "tuhu_wap"
    }
    
    response = requests.post(url, headers=headers, json=payload)
    if response.json().get("code") == 10000:
        return response.json().get("data", {}).get("carId")
    return None

数据爬取功能

构建养护查询链接

def build_maintenance_link(vehicle_json: str) -> str:
    """构建养护查询完整链接"""
    base_url = "https://maint-api.tuhu.cn/apinew/GetAppFirstPageExternalData"
    params = {
        "channel": "kH5",
        "city": CITY,
        "province": PROVINCE,
        "vehicle": requests.utils.quote(vehicle_json)
    }
    return f"{base_url}?{urllib.parse.urlencode(params)}"

执行数据爬取

def crawl_maintenance(link: str) -> Optional[Dict]:
    """爬取养护数据"""
    try:
        response = requests.get(link, headers=headers, timeout=15)
        return response.json().get("Categories", [])
    except Exception as e:
        print(f"爬取失败: {str(e)}")
        return None

5辆车限制解决方案

解决方案设计

核心实现代码

def process_vehicle(model_info: Dict) -> Optional[Dict]:
    """处理单个车型的爬取流程"""
    # 检查并管理车辆
    while True:
        current_vehicles = get_car_list()
        if len(current_vehicles) < 5:
            break
        # 删除最早添加的车辆
        oldest = sorted(current_vehicles, key=lambda x: x["addTime"])[0]
        if not delete_car(oldest["carId"]):
            return None
        time.sleep(2)
    
    # 添加新车
    car_id = add_vehicle(
        model_info["model_code"],
        model_info["year"],
        model_info["tid"]
    )
    if not car_id:
        return None
    
    # 构建并爬取链接
    vehicle_json = build_vehicle_json(car_id, model_info)
    link = build_maintenance_link(vehicle_json)
    data = crawl_maintenance(link)
    
    # 立即删除车辆
    delete_car(car_id)
    
    return {
        "model_info": model_info,
        "data": data,
        "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    }

完整工作流程

主函数实现

def main():
    # 1. 获取所有品牌
    brands = get_brands()
    
    # 2. 遍历品牌获取车型
    for brand in brands[:2]:  # 测试时限制品牌数量
        models = get_models(brand["name"])
        
        # 3. 处理每个车型
        for model in models[:3]:  # 测试时限制车型数量
            displacements = get_displacements(model["code"])
            
            for disp in displacements[:2]:  # 测试时限制排量数量
                years = get_product_years(model["code"], disp)
                
                for year in years[:1]:  # 测试时限制年份数量
                    # 4. 获取tid并处理车辆
                    vehicle_info = get_vehicle_tid(model["code"], disp, year)
                    if vehicle_info:
                        result = process_vehicle({
                            "brand": brand["name"],
                            "model_name": model["displayName"],
                            "model_code": model["code"],
                            "displacement": disp,
                            "year": year,
                            "tid": vehicle_info["tid"]
                        })
                        
                        # 5. 保存结果
                        if result:
                            save_result(result)
                            time.sleep(DELAY_BETWEEN_REQUESTS)

运行示例

python main.py

输出示例:

开始处理品牌: 大众
获取到车型: 高尔夫 (code: VW-GOLF)
处理排量: 1.4T
处理年份: 2022
成功添加车辆: VW123
爬取数据完成
删除车辆: VW123
保存结果成功
...

最终信息爬取

在数据功能爬取阶段,我们成功爬取了想要的参数,参数结果示例如下所示:

"奥迪": [  
  {  
    "model_name": "奥迪 A3",  
    "model_code": "VE-AADA3AD",  
    "factory": "一汽大众奥迪",  
    "displacement": "1.4T(35TFSI)",  
    "production_year": 2025,  
    "tid": "157761",  
    "car_id": "3B1BD673-BEE9-45CC-8B57-036275D53EFA",  
    "vehicle_json": "{\"CarId\": \"3B1BD673-BEE9-45CC-8B57-036275D53EFA\", \"PaiLiang\": \"1.4T(35TFSI)\", \"OnRoadTime\": \"\", \"VehicleId\": \"VE-AADA3AD\", \"tireSize\": \"\", \"Properties\": [], \"Nian\": \"2025\", \"Distance\": 0, \"Tid\": \"157761\"}",   
    "crawl_time": "2025-08-05 22:41:51",  
    "status": "完整",  
    "car_details": null  
  }
  ]

其实已经结束了,我们只需要把CarId,model_code,PaiLiang必须信息填入之前的链接就可以获得套餐了,读取json文件填入就像,批量获取链接里面的内容,这里不多赘述,但是要控制爬的时间,一般随机1-3秒即可,实践证明太快会导致途虎封ip。 image.png