车辆基本数据抓取

This commit is contained in:
2026-03-10 11:54:58 +08:00
parent a246ac8bb3
commit cb692a8f90
+388
View File
@@ -10,6 +10,9 @@ use support\exception\BusinessException;
use support\Log;
use support\Request;
use support\Redis;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use QL\QueryList;
class UsedCarDetailService
{
@@ -175,6 +178,21 @@ class UsedCarDetailService
}
}
// 如果提供了 crawl_car_id,自动抓取懂车帝车辆参数
if (!empty($data['crawl_car_id'])) {
try {
$params = $this->crawlParams($data['crawl_car_id']);
// 合并抓取的数据,手动提交的字段优先级更高
$data = array_merge($params, $data);
} catch (\Throwable $e) {
// 抓取失败不阻断创建,记录日志
Log::warning('抓取车辆参数失败', [
'crawl_car_id' => $data['crawl_car_id'],
'error' => $e->getMessage(),
]);
}
}
return $data;
}
@@ -448,4 +466,374 @@ class UsedCarDetailService
'total' => $query->count(),
];
}
/**
* 抓取懂车帝车辆参数
*
* @param string $crawlCarId 车型ID
* @return array 格式化后的参数数据
* @throws BusinessException
*/
public function crawlParams(string $crawlCarId): array
{
$url = 'https://www.dongchedi.com/auto/params-carIds-' . $crawlCarId;
// 请求头配置
$headers = [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept-Language' => 'zh-CN,zh;q=0.9',
'Referer' => 'https://www.dongchedi.com/',
];
try {
$client = new Client();
$response = $client->request('GET', $url, [
'headers' => $headers,
'timeout' => 10,
'verify' => false, // 禁用SSL验证
]);
$html = (string)$response->getBody();
if (empty($html)) {
throw new BusinessException('页面内容为空');
}
// 解析HTML获取JSON数据
$ql = QueryList::html($html);
$jsonText = $ql->find('#__NEXT_DATA__')->text();
if (empty($jsonText)) {
throw new BusinessException('未找到 __NEXT_DATA__ JSON');
}
$raw = json_decode($jsonText, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new BusinessException('JSON解析失败: ' . json_last_error_msg());
}
// 提取数据
$properties = $raw['props']['pageProps']['rawData']['properties'] ?? [];
$carInfo = $raw['props']['pageProps']['rawData']['car_info'][0] ?? [];
if (empty($properties) || empty($carInfo)) {
throw new BusinessException('数据结构异常,无法提取参数');
}
// 构建值映射表和分组
$valuesMap = $this->buildValuesMap($carInfo);
$groups = $this->buildCarSpecGroups($properties, $valuesMap);
// 映射到数据库字段
return $this->mapParamsToDbFields($groups, $carInfo);
} catch (GuzzleException $e) {
Log::error('抓取懂车帝参数失败', [
'crawl_car_id' => $crawlCarId,
'error' => $e->getMessage(),
]);
throw new BusinessException('抓取参数失败: ' . $e->getMessage());
} catch (\Throwable $e) {
Log::error('解析懂车帝参数失败', [
'crawl_car_id' => $crawlCarId,
'error' => $e->getMessage(),
]);
throw new BusinessException('解析参数失败: ' . $e->getMessage());
}
}
/**
* 构建值映射表
*/
private function buildValuesMap(array $carInfo): array
{
$valuesMap = [];
// 顶层字段
foreach ($carInfo as $k => $v) {
if ($k === 'info' || is_array($v)) {
continue;
}
$valuesMap[$k] = ['value' => $v];
}
// info中的配置项
if (!empty($carInfo['info']) && is_array($carInfo['info'])) {
foreach ($carInfo['info'] as $k => $v) {
if (is_array($v) && array_key_exists('value', $v)) {
$valuesMap[$k] = $v;
}
}
}
return $valuesMap;
}
/**
* 构建车辆规格分组
*/
private function buildCarSpecGroups(array $properties, array $valuesMap): array
{
$groups = [];
$currentKey = null;
foreach ($properties as $item) {
$type = (int)($item['type'] ?? 1);
$key = $item['key'] ?? '';
$text = $item['text'] ?? '';
$subList = $item['sub_list'] ?? null;
// 分类字段:type = 0 / 2
if (in_array($type, [0, 2], true)) {
$currentKey = $key ?: ($text ?: ('group_' . count($groups)));
if (!isset($groups[$currentKey])) {
$groups[$currentKey] = [
'group_key' => $currentKey,
'group_text' => $text,
'group_type' => $type,
'items' => [],
];
}
continue;
}
// 普通字段:挂到最近的分类下
if ($currentKey === null) {
$currentKey = '_default';
$groups[$currentKey] = [
'group_key' => '_default',
'group_text' => '其他',
'group_type' => -1,
'items' => [],
];
}
$value = '';
if ($key && isset($valuesMap[$key])) {
$value = trim((string)($valuesMap[$key]['value'] ?? ''));
}
$node = [
'key' => $key,
'text' => $text,
'value' => $value,
];
if (is_array($subList) && !empty($subList)) {
$children = [];
foreach ($subList as $sub) {
$subKey = $sub['key'] ?? '';
$subText = $sub['text'] ?? '';
$subValue = '';
if ($subKey && isset($valuesMap[$subKey])) {
$subValue = trim((string)($valuesMap[$subKey]['value'] ?? ''));
}
$children[] = [
'key' => $subKey,
'text' => $subText,
'value' => $subValue,
];
}
$node['children'] = $children;
}
$groups[$currentKey]['items'][] = $node;
}
return array_values($groups);
}
/**
* 从分组数据中获取字段值
*/
private function getFieldValue($groups, string $field): ?string
{
if (is_string($groups)) {
$groups = json_decode($groups, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return null;
}
}
if (!is_array($groups)) {
return null;
}
foreach ($groups as $group) {
if (isset($group['items']) && is_array($group['items'])) {
foreach ($group['items'] as $item) {
if (($item['key'] ?? null) === $field) {
return $item['value'] ?? null;
}
if (!empty($item['children']) && is_array($item['children'])) {
foreach ($item['children'] as $child) {
if (($child['key'] ?? null) === $field) {
return $child['value'] ?? null;
}
}
}
}
}
}
return null;
}
/**
* 解析尺寸字符串 (如 "4890x1890x1680" 转为长宽高)
*/
private function parseDimensions(string $dimensionStr): array
{
$result = ['body_length' => '', 'body_width' => '', 'body_height' => ''];
if (empty($dimensionStr)) {
return $result;
}
$parts = explode('x', strtolower(str_replace(['×', 'X', 'X'], 'x', $dimensionStr)));
if (count($parts) >= 3) {
$result['body_length'] = trim($parts[0]);
$result['body_width'] = trim($parts[1]);
$result['body_height'] = trim($parts[2]);
}
return $result;
}
/**
* 转换安全配置值 (●→标配, ○→选装, -→空)
*/
private function convertSafetyValue(?string $value): string
{
if ($value === '●') {
return '标配';
} elseif ($value === '○') {
return '选装';
} elseif ($value === '-') {
return '';
}
return $value ?? '';
}
/**
* 映射参数到数据库字段
* DongChedi 字段 → 数据库字段
*/
private function mapParamsToDbFields(array $groups, array $carInfo): array
{
$result = [];
// 从 car_info 直接获取的字段
$result['official_guide_price'] = $carInfo['official_price'] ?? '';
// 1. 基本信息
$result['manufacturer'] = $this->getFieldValue($groups, 'sub_brand_name') ?? '';
$result['level'] = $this->getFieldValue($groups, 'jb') ?? '';
$result['energy_type'] = $this->getFieldValue($groups, 'fuel_form') ?? '';
$result['listing_time'] = $this->getFieldValue($groups, 'market_time') ?? '';
$result['engine'] = $this->getFieldValue($groups, 'engine_description') ?? '';
$result['electric_motor'] = $this->getFieldValue($groups, 'electric_description') ?? '';
$result['nedc_range'] = $this->parseNumeric($this->getFieldValue($groups, 'nedc_range'));
$result['cltc_range'] = $this->parseNumeric($this->getFieldValue($groups, 'cltc_range'));
$result['fast_charge_platform'] = $this->getFieldValue($groups, 'fast_charge_platform') ?? '';
$result['charging_time'] = $this->parseDecimal($this->getFieldValue($groups, 'charging_time'));
$result['fast_charge_percent'] = $this->parseNumeric($this->getFieldValue($groups, 'fast_charge_percent'));
$result['max_power_kw'] = $this->parseNumeric($this->getFieldValue($groups, 'energy_elect_max_power'));
$result['max_torque_nm'] = $this->parseNumeric($this->getFieldValue($groups, 'energy_elect_max_torque'));
$result['transmission_desc'] = $this->getFieldValue($groups, 'gearbox_description') ?? '';
$result['dimensions'] = $this->getFieldValue($groups, 'length_width_height') ?? '';
$result['body_structure'] = $this->getFieldValue($groups, 'car_body_struct') ?? '';
$result['max_speed'] = $this->parseNumeric($this->getFieldValue($groups, 'max_speed'));
$result['acceleration_100'] = $this->parseDecimal($this->getFieldValue($groups, 'acceleration_time'));
$result['nedc_fuel_consumption'] = $this->parseDecimal($this->getFieldValue($groups, 'fuel_comprehensive'));
$result['power_consumption_100'] = $this->parseDecimal($this->getFieldValue($groups, 'power_consumption_100'));
$result['equivalent_fuel_consumption'] = $this->parseDecimal($this->getFieldValue($groups, 'equivalent_fuel_consumption'));
$result['maintenance_cost_60k'] = $this->parseDecimal($this->getFieldValue($groups, 'maintain_cost'));
// 2. 车身信息 (解析尺寸)
$dimensionStr = $this->getFieldValue($groups, 'length_width_height');
$dimensions = $this->parseDimensions($dimensionStr ?? '');
$result['body_length'] = $dimensions['body_length'];
$result['body_width'] = $dimensions['body_width'];
$result['body_height'] = $dimensions['body_height'];
$result['wheelbase'] = $this->parseNumeric($this->getFieldValue($groups, 'wheelbase'));
$result['door_count'] = $this->parseNumeric($this->getFieldValue($groups, 'door_nums'));
$result['door_open_type'] = $this->getFieldValue($groups, 'door_open_way') ?? '';
$result['seat_count'] = $this->parseNumeric($this->getFieldValue($groups, 'seat_count'));
// 3. 变速箱
$result['gear_count'] = $this->parseNumeric($this->getFieldValue($groups, 'stalls'));
$result['transmission_type'] = $this->getFieldValue($groups, 'gearbox_type') ?? '';
// 4. 底盘/转向
$result['drive_mode'] = $this->getFieldValue($groups, 'driver_form') ?? '';
$result['front_suspension'] = $this->getFieldValue($groups, 'front_suspension_form') ?? '';
$result['rear_suspension'] = $this->getFieldValue($groups, 'rear_suspension_form') ?? '';
$result['steering_type'] = $this->getFieldValue($groups, 'power_steering_type') ?? '';
// 5. 主动安全配置
$result['has_abs'] = $this->convertSafetyValue($this->getFieldValue($groups, 'abs_anti_lock'));
$result['has_ebd'] = $this->convertSafetyValue($this->getFieldValue($groups, 'brake_force'));
$result['has_bra'] = $this->convertSafetyValue($this->getFieldValue($groups, 'brake_assist'));
$result['has_tcs'] = $this->convertSafetyValue($this->getFieldValue($groups, 'traction_control'));
$result['has_esp'] = $this->convertSafetyValue($this->getFieldValue($groups, 'body_stability_system'));
$result['has_active_brake'] = $this->convertSafetyValue($this->getFieldValue($groups, 'active_brake'));
$result['has_lane_assist'] = $this->convertSafetyValue($this->getFieldValue($groups, 'line_support'));
$result['has_lka'] = $this->convertSafetyValue($this->getFieldValue($groups, 'lane_keeping_assist'));
$result['has_lane_centering'] = $this->convertSafetyValue($this->getFieldValue($groups, 'lane_center'));
$result['has_fatigue_remind'] = $this->convertSafetyValue($this->getFieldValue($groups, 'fatigue_driving_warning'));
// 6. 车轮/制动
$result['front_brake_type'] = $this->getFieldValue($groups, 'front_brake_type') ?? '';
$result['rear_brake_type'] = $this->getFieldValue($groups, 'rear_brake_type') ?? '';
$result['parking_brake_type'] = $this->getFieldValue($groups, 'park_brake_type') ?? '';
$result['front_tire_spec'] = $this->getFieldValue($groups, 'front_tire_size') ?? '';
$result['rear_tire_spec'] = $this->getFieldValue($groups, 'rear_tire_size') ?? '';
// 确保空字符串不被转为null
foreach ($result as $key => $value) {
if ($value === null) {
$result[$key] = '';
}
}
return $result;
}
/**
* 解析数值字段
*/
private function parseNumeric(?string $value): string
{
if ($value === null || $value === '') {
return '';
}
// 提取数字部分
if (preg_match('/\d+/', $value, $matches)) {
return $matches[0];
}
return '';
}
/**
* 解析小数
*/
private function parseDecimal(?string $value): string
{
if ($value === null || $value === '') {
return '';
}
// 提取数字和小数点
if (preg_match('/[\d.]+/', $value, $matches)) {
return $matches[0];
}
return '';
}
}