车辆基本数据抓取
This commit is contained in:
@@ -10,6 +10,9 @@ use support\exception\BusinessException;
|
|||||||
use support\Log;
|
use support\Log;
|
||||||
use support\Request;
|
use support\Request;
|
||||||
use support\Redis;
|
use support\Redis;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
|
use QL\QueryList;
|
||||||
|
|
||||||
class UsedCarDetailService
|
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;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,4 +466,374 @@ class UsedCarDetailService
|
|||||||
'total' => $query->count(),
|
'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 '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user