127 lines
5.2 KiB
Markdown
127 lines
5.2 KiB
Markdown
# 淘宝/天猫选择器实测记录
|
||
|
||
> 实测日期:2026-08-11
|
||
> 页面:`detail.tmall.com/item.htm?id=960057430812`、`item.taobao.com/item.htm?id=1060253247160`
|
||
> 方法:反向扫描(dump 页面实际类名前缀,而非猜名字去查)
|
||
> 对应实现:`extension/src/profiles/taobao.ts`
|
||
|
||
---
|
||
|
||
## 1. 结论
|
||
|
||
**两站 DOM 完全一致**,同一套前端(`PageFramework--` / `tbpc-layout` / `keyInfo--` 骨架相同),一份 profile 覆盖淘宝与天猫。
|
||
|
||
类名形如 `mainTitle--HASH`,是 CSS Modules 产物:**语义前缀稳定,哈希后缀每次构建变**。因此选择器一律写 `[class*="前缀--"]`。
|
||
|
||
结尾的 `--` 不能省——它把父容器和子元素区分开:`generalParamsInfoItem--` 不会误命中 `generalParamsInfoItemTitle--`。
|
||
|
||
---
|
||
|
||
## 2. 确证的选择器
|
||
|
||
| 目标 | 选择器 | 实测证据 |
|
||
|---|---|---|
|
||
| 标题 | `[class*="mainTitle--"]` | n=2 imgs=0,纯文本节点。天猫「迷你特工队玩具X弗特…」淘宝「对插双刀流发光双刃剑…」 |
|
||
| 标题兜底 | `[class*="MainTitle--"]` `[class*="ItemTitle--"]` | 外层容器,天猫版带图标(imgs=2) |
|
||
| 价格 | `[class*="highlightPrice--"]` | 淘宝 `¥5.2`,天猫 `秒杀价¥27.72` |
|
||
| 价格兜底 | `[class*="priceWrap--"]` | 会带上「优惠前¥36.8」,故仅兜底 |
|
||
| 主图 | `[class*="picGallery--"] img` / `#picGalleryEle` | 天猫 imgs=6,淘宝 imgs=7 |
|
||
| 主图缩略 | `[class*="thumbnailPic--"]` | n=5/6 |
|
||
| SKU 容器 | `[class*="valueItem--"]` | **n=22 imgs=22**(天猫),每项恰含一张 img |
|
||
| SKU 规格名 | `[class*="valueItemText--"]` | 「特工x武器小【弗特】2种形态- 可变形」 |
|
||
| 参数项 | `[class*="generalParamsInfoItem--"]` | Title=「品牌」SubTitle=「劣狐狐(模玩)」 |
|
||
| 详情区 | `[class*="tabDetailWrap--"]` `[class*="detailInfo--"]` | 淘宝 imgs=7 |
|
||
|
||
---
|
||
|
||
## 3. 两个反直觉的点
|
||
|
||
### 3.1 淘宝 SKU 是真实 `<img>`,不是 CSS 背景图
|
||
|
||
**与 1688 相反。** 探测数据:
|
||
|
||
```
|
||
valueItem n=22 imgs=22 ← 容器,每项含 1 张 img
|
||
valueItemImgWrap n=22 imgs=22 ← 图片包裹层
|
||
valueItemImg n=22 imgs=0 ← img 元素本身(querySelectorAll('img') 查自己得 0)
|
||
valueItemText n=22 imgs=0 ← 规格名文本
|
||
```
|
||
|
||
`imgs=0` 恰恰证明 `valueItemImg--` 就是 `<img>`。所以淘宝 profile **不能**用 `srcProps: ['backgroundImage']`(1688 必须用)。
|
||
|
||
引擎为此补了一段兜底:选择器命中容器且未取到 URL 时,往下找一层 `querySelector('img')`(见 `collector/image.ts`)。
|
||
|
||
### 3.2 主图组不设 minWidth
|
||
|
||
`picGallery--` 里同时有大图和缩略图,缩略图 `naturalWidth` 只有 60 左右。按 `minWidth: 200` 过滤会把主图**全部误杀**。
|
||
|
||
不过滤是安全的,因为 `toOriginalUrl()` 会把两种尺寸都还原成同一个原图 URL,`dedupeKey` 相同即自动去重。
|
||
|
||
---
|
||
|
||
## 4. 两个失效的既有假设
|
||
|
||
### 4.1 页面上没有 `<h1>`
|
||
|
||
`document.querySelector('h1')` 返回 null。旧 profile 里的 `.tb-detail-hd h1`、`h1[data-spm]`、裸 `h1` 全部无效,`readySelectors` 也不能用 `h1` 探活。
|
||
|
||
### 4.2 没有 JSON-LD
|
||
|
||
两站 `script[type="application/ld+json"]` 都是**空数组**。
|
||
|
||
`plan-revision.md` R3 里"JSON-LD 比 DOM 选择器稳定一个数量级"的推测**对淘宝不成立**(对 Ozon 仍待验证)。淘宝只能走 DOM。
|
||
|
||
---
|
||
|
||
## 5. 待办
|
||
|
||
### 5.1 desc 暂不采集
|
||
|
||
`detailInfo--` 容器里混着用户评价、参数信息、图文详情三块,`extract: 'join'` 出来是无法使用的一坨。考虑到 1688/淘宝的中文文案对 Ozon 价值本就低(需重写),一期跳过。
|
||
|
||
若以后要采,需先定位「图文详情」那个 `tabDetailItem--` 的稳定标识。
|
||
|
||
### 5.2 详情图需要用户操作
|
||
|
||
图文详情是懒加载 + tab 切换。用户不点开「图文详情」tab 就采不到。`scan.ts` 已有 `stats.detail === 0` 的警告。
|
||
|
||
### 5.3 内嵌数据待评估
|
||
|
||
`window` 上有一批可能有用的键,但**当前架构读不到**——MV3 content script 默认跑在 isolated world,看不见页面 `window`。要读需 `world: 'MAIN'` 或注入 script 标签。
|
||
|
||
值得关注的:
|
||
|
||
```
|
||
__general_skupanel_cache_data ← 可能含完整 SKU 结构
|
||
__ICE_DATA_LOADER__ ← ICE 框架的数据层
|
||
__itempage_openapi
|
||
g_config
|
||
```
|
||
|
||
如果 `__general_skupanel_cache_data` 真含 SKU 数据,比 DOM 抓 22 个 `valueItem--` 可靠得多。二期评估。
|
||
|
||
---
|
||
|
||
## 6. 复测脚本
|
||
|
||
改版后重跑,对照本文档的证据列:
|
||
|
||
```js
|
||
(() => {
|
||
const KEY = /(title|name|main|pic|img|gallery|thumb|sku|value|desc|detail|param|price)/i;
|
||
const pfx = new Map();
|
||
document.querySelectorAll('*').forEach(el => {
|
||
if (typeof el.className !== 'string') return;
|
||
el.className.split(/\s+/).forEach(c => {
|
||
const m = c.match(/^([A-Za-z][A-Za-z0-9]*)--/);
|
||
if (!m || !KEY.test(m[1])) return;
|
||
const r = pfx.get(m[1]) ?? { prefix: m[1], n: 0, imgs: 0, sample: '' };
|
||
r.n++; r.imgs += el.querySelectorAll('img').length;
|
||
if (!r.sample) r.sample = (el.textContent || '').trim().slice(0, 40);
|
||
pfx.set(m[1], r);
|
||
});
|
||
});
|
||
console.table([...pfx.values()].sort((a, b) => b.n - a.n).slice(0, 40));
|
||
})();
|
||
```
|