dongchedi-cli 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dongchedi_cli-0.2.0 → dongchedi_cli-0.3.0}/.gitignore +1 -0
- {dongchedi_cli-0.2.0 → dongchedi_cli-0.3.0}/PKG-INFO +9 -3
- {dongchedi_cli-0.2.0 → dongchedi_cli-0.3.0}/README.md +8 -2
- {dongchedi_cli-0.2.0 → dongchedi_cli-0.3.0}/pyproject.toml +1 -1
- {dongchedi_cli-0.2.0 → dongchedi_cli-0.3.0}/src/dongchedi_cli/__init__.py +1 -1
- {dongchedi_cli-0.2.0 → dongchedi_cli-0.3.0}/src/dongchedi_cli/cli.py +201 -23
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: dongchedi-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: 懂车帝(dongchedi.com)数据查询 CLI:销量榜 / 品牌 / 车系 / 车型库筛选 / 车型 / 参数配置 / 车主评论 / 搜索
|
|
5
5
|
Author: weisanju
|
|
6
6
|
Keywords: automobile,car,cli,dongchedi,vehicle-data
|
|
@@ -46,8 +46,12 @@ dcd filter --price 10,20 --fuel 纯电 --body 轿车 # 车型库筛选:10-20
|
|
|
46
46
|
dcd filter --body 小型SUV # 全部小型SUV(级别细分,如 紧凑型SUV/中大型车/中型MPV)
|
|
47
47
|
dcd filter --price 10, --fuel 新能源 --sort 销量高 -n 50 # 10万以上新能源,按销量排序
|
|
48
48
|
dcd cars 20154 # 星愿的车型年款与价格(含停售/未上市)
|
|
49
|
+
dcd cars 26051 -k 700 --fuel 纯电 # 车型筛选:名称含 700 的纯电车型(大小写不敏感)
|
|
50
|
+
dcd cars 26051 --price 15,17 # 按指导价筛选(万,一侧留空表示不限)
|
|
49
51
|
dcd specs 20154 259371 # 车型参数配置表
|
|
50
52
|
dcd specs 20154 259371 259369 -s 电池 # 多车对比,只看"电池"章节
|
|
53
|
+
dcd specs 26051 --all # 全系可查配置车型横向对比
|
|
54
|
+
dcd specs 26051 --all --price 15,17 --fuel 纯电 -s 电池 # 全系筛选后对比
|
|
51
55
|
dcd reviews 20154 -p 1 -n 20 # 车主评论(含成交价/评分)
|
|
52
56
|
dcd search 秦plus # 搜车系
|
|
53
57
|
dcd dealer 259371 # 某车型的经销商报价与优惠(默认杭州)
|
|
@@ -88,8 +92,8 @@ Cookie 文件解析优先级:`--cookie-file` 参数 → `DCD_COOKIE_FILE` 环
|
|
|
88
92
|
| brands | `GET /motor/brand/v4/brand/` | 品牌在 `data[].info`(其中 4 个热门品牌在 `info.brand`) |
|
|
89
93
|
| series | `POST /motor/pc/car/brand/select_series_v2` | 表单参数是 `brand`(不是 `brand_id`,传错会静默返回全站热门车系) |
|
|
90
94
|
| filter | `POST /motor/pc/car/brand/select_series_v2` | 与 series 同接口,隐藏筛选参数:`price="10,20"`(万元,区间重叠即命中,`-1` 表示不限)、`fuel_form`(1汽油/2柴油/3油电混合/4纯电/5增程/6插混/13轻混)、`series_type`(0轿车/1SUV/2MPV…)、`outter_detail_type`(细分级别:10小型SUV/11紧凑型SUV…);`page` 分页有效,总数在 `data.series_count`;枚举出自车型库页前端 chunk(2026-09 逐项实测) |
|
|
91
|
-
| cars | `GET /motor/pc/car/series/car_list` | `data.tab_list[]`,type 1115=车型、1137=年款分组,1068
|
|
92
|
-
| specs | `GET /motor/pc/car/get_full_config_info` | 需 `series_id` + `car_ids`;实测忽略 car_ids,总是返回全车系,CLI
|
|
95
|
+
| cars | `GET /motor/pc/car/series/car_list` | `data.tab_list[]`,type 1115=车型、1137=年款分组,1068 为占位需跳过;`-k/--price/--fuel` 为本地筛选,能源取自配置接口 `car_info[].info.fuel_form.value`(纯电动/增程式/插电式混合动力/汽油/油电混合/48V轻混系统,子串匹配),筛选时按 car_id 去重(同一车型会同时出现在「在售」与年款 tab) |
|
|
96
|
+
| specs | `GET /motor/pc/car/get_full_config_info` | 需 `series_id` + `car_ids`;实测忽略 car_ids,总是返回全车系,CLI 已按请求过滤(返回车型与 car_list 不完全一致,约只含可查配置/在售车型);`--all` 一次取全系在本地筛选 |
|
|
93
97
|
| reviews | `GET /motor/pc/car/series/get_review_list` | 需 `Referer: /auto/series/score/{series_id}` |
|
|
94
98
|
| search | `GET /motor/searchapi/search_content` | `cur_tab=2`,车系卡片为 `cell_type=5002`(不一定出现,无则回退内容标题) |
|
|
95
99
|
| dealer | `GET /motor/pc/car/series/car_dealer_price` | 匿名可用;依赖 `city_name`(全国 → 暂无报价),未覆盖城市会回落邻近城市 |
|
|
@@ -106,6 +110,8 @@ Cookie 文件解析优先级:`--cookie-file` 参数 → `DCD_COOKIE_FILE` 环
|
|
|
106
110
|
- 登录态 Cookie 含中文值,urllib 请求头默认 latin-1 编码,发送前已转 UTF-8 裸字节(与浏览器线上行为一致)。
|
|
107
111
|
- 参数配置表字段:`data.properties[]` 为行定义(type 0/2 为章节标题,1 普通行,3 带子项,6 跳过),值在 `data.car_info[].info[<key>].value`;type 3 的值挂在 `sub_list[].key` 上。注意 `type` 字段时而为 int 时而为字符串,代码里已统一归一化。
|
|
108
112
|
- 车型库筛选中 `series_type` 与 `outter_detail_type` **互斥**:同传时服务端会忽略后者(按大类返回);CLI 的 `--body` 已自动只传其一(轿车/SUV 传 series_type,小型SUV 等细分传 outter_detail_type)。
|
|
113
|
+
- 年款分组标题的「/ 后缀」不一定是能源:奥迪A6L 是世代代号(/ C9、/ C8),部分车系无后缀(钛7 PHEV)。`cars --fuel` 不解析标题,统一以配置接口的「能源类型」为准;因此只覆盖可查配置的车型,停售车型若无配置数据会被筛掉。
|
|
114
|
+
- `cars` / `specs --all` 的筛选均在本地完成;`--json` 始终输出接口原始数据、不受筛选影响。筛选无匹配时:`cars` 输出空表(退出码 0,提示在 stderr),`specs --all` 报错(退出码 1)。
|
|
109
115
|
- 接口可能随版本变化(版本参数当前为 version_code=476),失效时优先用 `--json` 排查原始返回。
|
|
110
116
|
|
|
111
117
|
## 开发与发布
|
|
@@ -26,8 +26,12 @@ dcd filter --price 10,20 --fuel 纯电 --body 轿车 # 车型库筛选:10-20
|
|
|
26
26
|
dcd filter --body 小型SUV # 全部小型SUV(级别细分,如 紧凑型SUV/中大型车/中型MPV)
|
|
27
27
|
dcd filter --price 10, --fuel 新能源 --sort 销量高 -n 50 # 10万以上新能源,按销量排序
|
|
28
28
|
dcd cars 20154 # 星愿的车型年款与价格(含停售/未上市)
|
|
29
|
+
dcd cars 26051 -k 700 --fuel 纯电 # 车型筛选:名称含 700 的纯电车型(大小写不敏感)
|
|
30
|
+
dcd cars 26051 --price 15,17 # 按指导价筛选(万,一侧留空表示不限)
|
|
29
31
|
dcd specs 20154 259371 # 车型参数配置表
|
|
30
32
|
dcd specs 20154 259371 259369 -s 电池 # 多车对比,只看"电池"章节
|
|
33
|
+
dcd specs 26051 --all # 全系可查配置车型横向对比
|
|
34
|
+
dcd specs 26051 --all --price 15,17 --fuel 纯电 -s 电池 # 全系筛选后对比
|
|
31
35
|
dcd reviews 20154 -p 1 -n 20 # 车主评论(含成交价/评分)
|
|
32
36
|
dcd search 秦plus # 搜车系
|
|
33
37
|
dcd dealer 259371 # 某车型的经销商报价与优惠(默认杭州)
|
|
@@ -68,8 +72,8 @@ Cookie 文件解析优先级:`--cookie-file` 参数 → `DCD_COOKIE_FILE` 环
|
|
|
68
72
|
| brands | `GET /motor/brand/v4/brand/` | 品牌在 `data[].info`(其中 4 个热门品牌在 `info.brand`) |
|
|
69
73
|
| series | `POST /motor/pc/car/brand/select_series_v2` | 表单参数是 `brand`(不是 `brand_id`,传错会静默返回全站热门车系) |
|
|
70
74
|
| filter | `POST /motor/pc/car/brand/select_series_v2` | 与 series 同接口,隐藏筛选参数:`price="10,20"`(万元,区间重叠即命中,`-1` 表示不限)、`fuel_form`(1汽油/2柴油/3油电混合/4纯电/5增程/6插混/13轻混)、`series_type`(0轿车/1SUV/2MPV…)、`outter_detail_type`(细分级别:10小型SUV/11紧凑型SUV…);`page` 分页有效,总数在 `data.series_count`;枚举出自车型库页前端 chunk(2026-09 逐项实测) |
|
|
71
|
-
| cars | `GET /motor/pc/car/series/car_list` | `data.tab_list[]`,type 1115=车型、1137=年款分组,1068
|
|
72
|
-
| specs | `GET /motor/pc/car/get_full_config_info` | 需 `series_id` + `car_ids`;实测忽略 car_ids,总是返回全车系,CLI
|
|
75
|
+
| cars | `GET /motor/pc/car/series/car_list` | `data.tab_list[]`,type 1115=车型、1137=年款分组,1068 为占位需跳过;`-k/--price/--fuel` 为本地筛选,能源取自配置接口 `car_info[].info.fuel_form.value`(纯电动/增程式/插电式混合动力/汽油/油电混合/48V轻混系统,子串匹配),筛选时按 car_id 去重(同一车型会同时出现在「在售」与年款 tab) |
|
|
76
|
+
| specs | `GET /motor/pc/car/get_full_config_info` | 需 `series_id` + `car_ids`;实测忽略 car_ids,总是返回全车系,CLI 已按请求过滤(返回车型与 car_list 不完全一致,约只含可查配置/在售车型);`--all` 一次取全系在本地筛选 |
|
|
73
77
|
| reviews | `GET /motor/pc/car/series/get_review_list` | 需 `Referer: /auto/series/score/{series_id}` |
|
|
74
78
|
| search | `GET /motor/searchapi/search_content` | `cur_tab=2`,车系卡片为 `cell_type=5002`(不一定出现,无则回退内容标题) |
|
|
75
79
|
| dealer | `GET /motor/pc/car/series/car_dealer_price` | 匿名可用;依赖 `city_name`(全国 → 暂无报价),未覆盖城市会回落邻近城市 |
|
|
@@ -86,6 +90,8 @@ Cookie 文件解析优先级:`--cookie-file` 参数 → `DCD_COOKIE_FILE` 环
|
|
|
86
90
|
- 登录态 Cookie 含中文值,urllib 请求头默认 latin-1 编码,发送前已转 UTF-8 裸字节(与浏览器线上行为一致)。
|
|
87
91
|
- 参数配置表字段:`data.properties[]` 为行定义(type 0/2 为章节标题,1 普通行,3 带子项,6 跳过),值在 `data.car_info[].info[<key>].value`;type 3 的值挂在 `sub_list[].key` 上。注意 `type` 字段时而为 int 时而为字符串,代码里已统一归一化。
|
|
88
92
|
- 车型库筛选中 `series_type` 与 `outter_detail_type` **互斥**:同传时服务端会忽略后者(按大类返回);CLI 的 `--body` 已自动只传其一(轿车/SUV 传 series_type,小型SUV 等细分传 outter_detail_type)。
|
|
93
|
+
- 年款分组标题的「/ 后缀」不一定是能源:奥迪A6L 是世代代号(/ C9、/ C8),部分车系无后缀(钛7 PHEV)。`cars --fuel` 不解析标题,统一以配置接口的「能源类型」为准;因此只覆盖可查配置的车型,停售车型若无配置数据会被筛掉。
|
|
94
|
+
- `cars` / `specs --all` 的筛选均在本地完成;`--json` 始终输出接口原始数据、不受筛选影响。筛选无匹配时:`cars` 输出空表(退出码 0,提示在 stderr),`specs --all` 报错(退出码 1)。
|
|
89
95
|
- 接口可能随版本变化(版本参数当前为 version_code=476),失效时优先用 `--json` 排查原始返回。
|
|
90
96
|
|
|
91
97
|
## 开发与发布
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
免登录子命令:rank / brands / series / filter / cars / specs / reviews / search / dealer
|
|
4
4
|
需登录态子命令:series-info(HTML SSR)/ me(账号信息)
|
|
5
5
|
|
|
6
|
+
cars 可按 关键字/价格/能源 筛选;specs 支持 --all 对比全系可查配置车型并叠加同一套筛选
|
|
7
|
+
|
|
6
8
|
登录态来源(auth-import 写入,或 --cookie-file / DCD_COOKIE_FILE 指定):
|
|
7
9
|
dcd auth-import <playwright-state.json> # playwright-cli state-save 的产物
|
|
8
10
|
|
|
@@ -420,13 +422,19 @@ SORTS = {
|
|
|
420
422
|
}
|
|
421
423
|
|
|
422
424
|
|
|
423
|
-
def
|
|
424
|
-
"""'10,20' -> '10,20';一侧留空表示不限:'10,' -> '10
|
|
425
|
+
def parse_price_range(text: str) -> tuple[str | None, str | None]:
|
|
426
|
+
"""'10,20' -> ('10','20');一侧留空表示不限:'10,' -> ('10',None),',20' -> (None,'20')。"""
|
|
425
427
|
normalized = text.replace("万", "").replace(",", ",").replace("~", "~")
|
|
426
428
|
match = re.fullmatch(r"\s*(\d+(?:\.\d+)?)?\s*[,\-~]\s*(\d+(?:\.\d+)?)?\s*", normalized)
|
|
427
429
|
if not match or (not match.group(1) and not match.group(2)):
|
|
428
430
|
raise DcdError('价格格式应为 "最低,最高"(单位万),如 "10,20";一侧留空表示不限,如 "10,"')
|
|
429
|
-
return
|
|
431
|
+
return match.group(1), match.group(2)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def parse_price(text: str) -> str:
|
|
435
|
+
"""'10,20' -> '10,20';一侧留空表示不限:'10,' -> '10,-1',',20' -> '0,20'。"""
|
|
436
|
+
low, high = parse_price_range(text)
|
|
437
|
+
return f"{low or '0'},{high or '-1'}"
|
|
430
438
|
|
|
431
439
|
|
|
432
440
|
def resolve_fuel(text: str) -> str:
|
|
@@ -502,6 +510,99 @@ def cmd_filter(args) -> None:
|
|
|
502
510
|
print_series_table(series)
|
|
503
511
|
|
|
504
512
|
|
|
513
|
+
# ---------- 车系内筛选(cars / specs --all 共用) ----------
|
|
514
|
+
|
|
515
|
+
# 配置接口的能源类型实际取值:纯电动 / 增程式 / 插电式混合动力 / 汽油 / 油电混合 / 48V轻混系统
|
|
516
|
+
# 按子串匹配,别名与 filter 子命令保持一致
|
|
517
|
+
FUEL_TEXTS = {
|
|
518
|
+
"新能源": ("纯电", "增程", "插电"),
|
|
519
|
+
"纯电": ("纯电",),
|
|
520
|
+
"纯电动": ("纯电",),
|
|
521
|
+
"增程": ("增程",),
|
|
522
|
+
"增程式": ("增程",),
|
|
523
|
+
"插混": ("插电",),
|
|
524
|
+
"插电混动": ("插电",),
|
|
525
|
+
"插电式混合动力": ("插电",),
|
|
526
|
+
"汽油": ("汽油",),
|
|
527
|
+
"柴油": ("柴油",),
|
|
528
|
+
"油电混动": ("油电",),
|
|
529
|
+
"油电混合": ("油电",),
|
|
530
|
+
"轻混": ("轻混", "48V"),
|
|
531
|
+
"48V轻混": ("轻混", "48V"),
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def resolve_fuel_text(text: str) -> tuple[str, ...]:
|
|
536
|
+
key = text.strip()
|
|
537
|
+
if key in FUEL_TEXTS:
|
|
538
|
+
return FUEL_TEXTS[key]
|
|
539
|
+
raise DcdError(
|
|
540
|
+
f"未知能源 {text!r};可用: 新能源/纯电/增程/插混/汽油/柴油/油电混动/轻混"
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def parse_wan(value) -> float | None:
|
|
545
|
+
"""车系接口指导价是浮点,配置接口是 '13.99万' 字符串,统一为数字(万)。"""
|
|
546
|
+
if isinstance(value, (int, float)):
|
|
547
|
+
return float(value)
|
|
548
|
+
match = re.fullmatch(r"\s*(\d+(?:\.\d+)?)\s*万?\s*", str(value or ""))
|
|
549
|
+
return float(match.group(1)) if match else None
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def car_energy(car: dict) -> str:
|
|
553
|
+
return (((car.get("info") or {}).get("fuel_form") or {}).get("value") or "").strip()
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def matches_keyword(name: str, keyword: str) -> bool:
|
|
557
|
+
return keyword.lower() in name.lower()
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def matches_price(price_wan: float | None, bounds: tuple[float | None, float | None]) -> bool:
|
|
561
|
+
if price_wan is None:
|
|
562
|
+
return False
|
|
563
|
+
low, high = bounds
|
|
564
|
+
if low is not None and price_wan < low:
|
|
565
|
+
return False
|
|
566
|
+
return high is None or price_wan <= high
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def matches_fuel(energy: str, texts: tuple[str, ...]) -> bool:
|
|
570
|
+
return bool(energy) and any(t in energy for t in texts)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def parse_price_bounds(text: str) -> tuple[float | None, float | None]:
|
|
574
|
+
low, high = parse_price_range(text)
|
|
575
|
+
return (float(low) if low else None, float(high) if high else None)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def fetch_car_fuels(series_id) -> dict[str, str]:
|
|
579
|
+
"""车系内全部可查配置车型的能源类型:car_id -> fuel_form。"""
|
|
580
|
+
payload = api_get("/motor/pc/car/get_full_config_info", {"series_id": series_id})
|
|
581
|
+
fuels = {}
|
|
582
|
+
for car in (payload.get("data") or {}).get("car_info") or []:
|
|
583
|
+
cid = car.get("car_id")
|
|
584
|
+
if cid is not None:
|
|
585
|
+
fuels[str(cid)] = car_energy(car)
|
|
586
|
+
return fuels
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def filter_desc(keyword, price, fuel) -> str:
|
|
590
|
+
parts = []
|
|
591
|
+
if keyword:
|
|
592
|
+
parts.append(f"关键字 {keyword}")
|
|
593
|
+
if price:
|
|
594
|
+
low, high = parse_price_range(price)
|
|
595
|
+
if low and high:
|
|
596
|
+
parts.append(f"价格 {low}-{high}万")
|
|
597
|
+
elif low:
|
|
598
|
+
parts.append(f"价格 {low}万以上")
|
|
599
|
+
else:
|
|
600
|
+
parts.append(f"价格 {high}万以下")
|
|
601
|
+
if fuel:
|
|
602
|
+
parts.append(fuel)
|
|
603
|
+
return " · ".join(parts)
|
|
604
|
+
|
|
605
|
+
|
|
505
606
|
def cmd_cars(args) -> None:
|
|
506
607
|
payload = api_get(
|
|
507
608
|
"/motor/pc/car/series/car_list",
|
|
@@ -510,27 +611,66 @@ def cmd_cars(args) -> None:
|
|
|
510
611
|
if args.json:
|
|
511
612
|
dump_json(payload)
|
|
512
613
|
return
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
614
|
+
filtering = bool(args.keyword or args.price or args.fuel)
|
|
615
|
+
bounds = parse_price_bounds(args.price) if args.price else None
|
|
616
|
+
texts = resolve_fuel_text(args.fuel) if args.fuel else None
|
|
617
|
+
fuels = fetch_car_fuels(args.series_id) if texts else {}
|
|
618
|
+
seen: set[str] = set()
|
|
619
|
+
rows: list[list[str]] = []
|
|
620
|
+
header: list[str] | None = None
|
|
621
|
+
section: list[list[str]] = []
|
|
622
|
+
|
|
623
|
+
def flush() -> None:
|
|
624
|
+
# 筛选模式下整段无匹配的章节连标题一起隐藏
|
|
625
|
+
if not filtering or section:
|
|
626
|
+
if header:
|
|
627
|
+
rows.append(header)
|
|
628
|
+
rows.extend(section)
|
|
629
|
+
|
|
630
|
+
for tab in (payload.get("data") or {}).get("tab_list") or []:
|
|
516
631
|
status = tab.get("tab_text") or ""
|
|
517
632
|
for entry in tab.get("data") or []:
|
|
518
633
|
info = entry.get("info") or {}
|
|
519
|
-
|
|
634
|
+
etype = entry.get("type")
|
|
635
|
+
if etype == "1068":
|
|
520
636
|
continue
|
|
521
|
-
if
|
|
522
|
-
|
|
637
|
+
if etype == "1137":
|
|
638
|
+
flush()
|
|
639
|
+
header = [f"[{status}] {info.get('name') or ''}", "", "", "", "", ""]
|
|
640
|
+
section = []
|
|
523
641
|
continue
|
|
524
|
-
|
|
642
|
+
name = info.get("car_name") or info.get("name") or ""
|
|
643
|
+
car_id = str(info.get("car_id") or info.get("id") or "")
|
|
644
|
+
if filtering:
|
|
645
|
+
if args.keyword and not matches_keyword(name, args.keyword):
|
|
646
|
+
continue
|
|
647
|
+
if bounds and not matches_price(parse_wan(info.get("official_price")), bounds):
|
|
648
|
+
continue
|
|
649
|
+
if texts and not matches_fuel(fuels.get(car_id, ""), texts):
|
|
650
|
+
continue
|
|
651
|
+
# 同一车型会同时出现在「在售」与年款 tab,筛选时按 car_id 去重
|
|
652
|
+
if car_id and car_id in seen:
|
|
653
|
+
continue
|
|
654
|
+
if car_id:
|
|
655
|
+
seen.add(car_id)
|
|
656
|
+
section.append(
|
|
525
657
|
[
|
|
526
|
-
|
|
658
|
+
name,
|
|
527
659
|
str(info.get("year") or ""),
|
|
528
660
|
info.get("official_price_str") or "",
|
|
529
661
|
info.get("dealer_price") or "",
|
|
530
662
|
info.get("owner_price") or "",
|
|
531
|
-
|
|
663
|
+
car_id,
|
|
532
664
|
]
|
|
533
665
|
)
|
|
666
|
+
flush()
|
|
667
|
+
if filtering:
|
|
668
|
+
matched = sum(1 for r in rows if r[5])
|
|
669
|
+
desc = filter_desc(args.keyword, args.price, args.fuel)
|
|
670
|
+
if matched:
|
|
671
|
+
print(f"筛选后 {matched} 款({desc})", file=sys.stderr)
|
|
672
|
+
else:
|
|
673
|
+
print(f"没有匹配的车型({desc})", file=sys.stderr)
|
|
534
674
|
print(render_table(["车型", "年款", "官方指导价", "经销商价", "车主价", "车型ID"], rows))
|
|
535
675
|
|
|
536
676
|
|
|
@@ -560,6 +700,16 @@ def _spec_value(car: dict, prop: dict) -> str:
|
|
|
560
700
|
|
|
561
701
|
|
|
562
702
|
def cmd_specs(args) -> None:
|
|
703
|
+
if args.all and args.car_id:
|
|
704
|
+
raise DcdError("--all 与车型ID 不能同时使用")
|
|
705
|
+
if not args.all and (args.keyword or args.price or args.fuel):
|
|
706
|
+
raise DcdError(
|
|
707
|
+
"--keyword/--price/--fuel 需配合 --all 使用,如: dcd specs 26051 --all --fuel 纯电"
|
|
708
|
+
)
|
|
709
|
+
if not args.all and not args.car_id:
|
|
710
|
+
raise DcdError(
|
|
711
|
+
"需要车型ID(可多个,逗号分隔),或用 --all 对比全系可查配置车型;车型ID 用 dcd cars 查"
|
|
712
|
+
)
|
|
563
713
|
car_ids = [cid for raw in args.car_id for cid in raw.split(",") if cid]
|
|
564
714
|
payload = api_get(
|
|
565
715
|
"/motor/pc/car/get_full_config_info",
|
|
@@ -570,14 +720,29 @@ def cmd_specs(args) -> None:
|
|
|
570
720
|
return
|
|
571
721
|
data = payload["data"]
|
|
572
722
|
cars = data.get("car_info") or []
|
|
573
|
-
if
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
723
|
+
if args.all:
|
|
724
|
+
bounds = parse_price_bounds(args.price) if args.price else None
|
|
725
|
+
texts = resolve_fuel_text(args.fuel) if args.fuel else None
|
|
726
|
+
cars = [
|
|
727
|
+
car
|
|
728
|
+
for car in cars
|
|
729
|
+
if (not args.keyword or matches_keyword(car.get("car_name") or "", args.keyword))
|
|
730
|
+
and (not bounds or matches_price(parse_wan(car.get("official_price")), bounds))
|
|
731
|
+
and (not texts or matches_fuel(car_energy(car), texts))
|
|
732
|
+
]
|
|
733
|
+
desc = filter_desc(args.keyword, args.price, args.fuel)
|
|
734
|
+
if not cars:
|
|
735
|
+
raise DcdError(f"全系没有匹配的车型({desc})")
|
|
736
|
+
print(f"全系 {len(cars)} 款" + (f"({desc})" if desc else ""), file=sys.stderr)
|
|
737
|
+
else:
|
|
738
|
+
if not cars:
|
|
739
|
+
raise DcdError(f"未找到车型配置: {', '.join(car_ids)}")
|
|
740
|
+
# 实测该接口忽略 car_ids,总是返回整个车系,这里按请求过滤
|
|
741
|
+
by_id = {str(c.get("car_id")): c for c in cars}
|
|
742
|
+
missing = [cid for cid in car_ids if cid not in by_id]
|
|
743
|
+
if missing:
|
|
744
|
+
raise DcdError(f"车系 {args.series_id} 下不存在车型: {', '.join(missing)}")
|
|
745
|
+
cars = [by_id[cid] for cid in car_ids]
|
|
581
746
|
headers = ["参数"] + [c.get("car_name") or str(c.get("car_id")) for c in cars]
|
|
582
747
|
rows: list[list[str]] = []
|
|
583
748
|
section = ""
|
|
@@ -912,14 +1077,27 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
912
1077
|
p.add_argument("-j", "--json", action="store_true", help="输出原始 JSON")
|
|
913
1078
|
p.set_defaults(func=cmd_filter)
|
|
914
1079
|
|
|
915
|
-
p = sub.add_parser("cars", help="
|
|
1080
|
+
p = sub.add_parser("cars", help="车系下的车型(年款/价格),可按条件筛选")
|
|
916
1081
|
p.add_argument("series_id", help="车系ID")
|
|
1082
|
+
p.add_argument("-k", "--keyword", help="按车型名筛选(不区分大小写),如 700、max")
|
|
1083
|
+
p.add_argument("--price", help='按指导价筛选(万),如 "15,17";一侧留空表示不限')
|
|
1084
|
+
p.add_argument(
|
|
1085
|
+
"-f", "--fuel", help="按能源筛选:纯电/增程/插混/汽油/柴油/油电混动/新能源/轻混"
|
|
1086
|
+
)
|
|
917
1087
|
p.add_argument("-j", "--json", action="store_true", help="输出原始 JSON")
|
|
918
1088
|
p.set_defaults(func=cmd_cars)
|
|
919
1089
|
|
|
920
|
-
p = sub.add_parser("specs", help="
|
|
1090
|
+
p = sub.add_parser("specs", help="车型参数配置表(指定车型对比,或 --all 对比全系)")
|
|
921
1091
|
p.add_argument("series_id", help="车系ID")
|
|
922
|
-
p.add_argument("car_id", nargs="
|
|
1092
|
+
p.add_argument("car_id", nargs="*", help="车型ID,可多个(用 dcd cars 查);或用 --all 对比全系")
|
|
1093
|
+
p.add_argument(
|
|
1094
|
+
"--all", action="store_true", help="对比全系可查配置车型,可叠加 --keyword/--price/--fuel"
|
|
1095
|
+
)
|
|
1096
|
+
p.add_argument("-k", "--keyword", help="按车型名筛选(需 --all),如 Max、四驱")
|
|
1097
|
+
p.add_argument("--price", help='按指导价筛选(需 --all,万),如 "15,17"')
|
|
1098
|
+
p.add_argument(
|
|
1099
|
+
"-f", "--fuel", help="按能源筛选(需 --all):纯电/增程/插混/汽油/油电混动/新能源/轻混"
|
|
1100
|
+
)
|
|
923
1101
|
p.add_argument("-s", "--section", help="只看包含该文字的章节,如 电池")
|
|
924
1102
|
p.add_argument("-j", "--json", action="store_true", help="输出原始 JSON")
|
|
925
1103
|
p.set_defaults(func=cmd_specs)
|