Files
ozon-seller-kit/docs/extension/1688-taobao-implementation.md
T
2026-08-11 17:09:23 +08:00

12 KiB
Raw Blame History

1688/淘宝采集插件实施计划

优先级调整:Ozon 后置,先做 1688/淘宝 理由:1688 选择器有生产验证基础,可先跑通引擎;淘宝同属阿里系,复用度高 上游:总体架构 · 插件原方案 · 方案修正


1. 为什么先做 1688/淘宝

维度 1688/淘宝 Ozon
选择器来源 v1.1.8 生产 bundle 反编译,已验证 需实测,哈希类名随时失效
技术难度 中(同源复用多) 高(React SSR + 选择器未知)
调试价值 可作引擎基准——跑通后 Ozon 采不到就一定是选择器问题 选择器和引擎同时调,歧义大
业务价值 补素材(1688 图多) 跟卖(第一优先级但技术难)

策略:先用 1688 跑通引擎和 File System Access 写盘,再用它诊断 Ozon 的选择器问题。


2. 淘宝 Profile 设计

2.1 与 1688 的共同点

共享原因
CDN 规则 都是 xxx.jpg_400x400.jpg 后缀,getOriginalImageUrl 通用
懒加载 data-lazyload-src / data-src 优先级相同
SKU 背景图 都用 backgroundImage 取 SKU 缩略图
参数表结构 <dl> 嵌套 <dt> <dd>,解析逻辑相同

可以抽一个 profiles/alibaba-common.ts 存共享工具。

2.2 淘宝特有选择器

URL 匹配

urlPatterns: [
  /^https:\/\/item\.taobao\.com\/item\.htm\?id=\d+/,
  /^https:\/\/detail\.tmall\.com\/item\.htm\?id=\d+/  // 天猫
]

extractItemId: (url) => {
  const m = url.match(/[?&]id=(\d+)/);
  return m?.[1] ?? null;
}

就绪选择器(淘宝用 React 16,水合较快):

readySelectors: [
  '[class*="ItemHeader"]',     // 标题区
  '[class*="MainPic"]',        // 主图画廊
  '[class*="SkuSelector"]'     // SKU 选择器
]

文本规则

textRules: [
  {
    kind: 'title',
    selectors: [
      '[class*="ItemHeader--title"]',
      '.tb-detail-hd h1',
      'h1[data-spm="1000983"]'   // 旧版
    ],
    extract: 'first',
    required: true
  },
  {
    kind: 'price',
    selectors: [
      '[class*="Price--priceText"]',
      '.tb-rmb-num',
      '[class*="priceInt"]'
    ],
    extract: 'first'
  },
  {
    kind: 'params',
    selectors: [
      '[class*="Attributes"] dl',
      '#attributes .tm-clear',
      '.attributes-list dl'
    ],
    extract: 'table',
    tableKeySelector: 'dt',
    tableValueSelector: 'dd'
  }
]

图片规则

imageGroups: [
  {
    key: 'main',
    name: '主图',
    type: 'img',
    selectors: [
      '[class*="MainPic"] img',        // React 版
      '#J_ImgBooth img',               // 旧版画廊
      '.tb-booth img'
    ],
    minWidth: 200,
    minHeight: 200
  },
  {
    key: 'sku',
    name: 'SKU图片',
    type: 'img',
    selectors: [
      '[class*="SkuSelector"] li',     // React 版
      '.tb-img li',                    // 旧版
      '[class*="skuItem"]'
    ],
    srcProps: ['backgroundImage'],     // 与 1688 同
    nameSelectors: ['span', '.value'], // 规格名
    minWidth: 20,
    minHeight: 20
  },
  {
    key: 'detail',
    name: '详情图',
    type: 'img',
    selectors: [
      '#description img',
      '[class*="Description"] img',
      '.detail-content img'
    ],
    minWidth: 300,
    minHeight: 100
  }
]

2.3 淘宝特殊处理

动态详情图:淘宝详情常用懒加载模块,需滚动触发。侧边栏提示同 Ozon:

⚠️ 详情图为 0,请滚动到页面底部后重新采集

天猫 vs 淘宝 C 店:URL 模式不同但 DOM 结构相似,用同一份 profile 即可。主要差异在类名前缀(tm- vs tb-),多写几套选择器兜底。


3. 更新后的里程碑

# 内容 工作量 产出
M1 WXT 项目初始化 0.5h extension/ 目录就位,能 dev
M2 核心类型与工具 1h profiles/types + collector/url + product.json TS 类型
M3 1688 profile + 采集引擎 3h 能在 1688 页面 console 里跑 scanCurrentPage()
M4 淘宝 profile 1h 同上,淘宝页面可用
M5 Side Panel UI(基础) 2h 分组展示采集结果,勾选图片
M6 File System Access 写盘 2h 生成完整商品文件夹到本地
M7 sources.json 去重 1h 二次采集追加不重复
M8 淘宝实测与修正 1h 在真实页面上跑,修选择器

总计 11.5 小时。M3 结束时引擎已可用,M6 结束时完整流程跑通。


4. 目录结构(实际代码)

extension/
├── wxt.config.ts
├── package.json
├── entrypoints/
│   ├── background.ts            # 图片代理 fetch(绕 CORS
│   ├── sidepanel/
│   │   ├── index.html
│   │   └── App.tsx              # 采集控制 UI
│   └── content/
│       └── index.ts             # 注入页面,触发采集
│
├── src/
│   ├── profiles/
│   │   ├── types.ts             # SiteProfile / TextRule / ImageGroupRule
│   │   ├── alibaba-common.ts    # 阿里系共享工具(URL / CDN
│   │   ├── 1688.ts              # ← 从 plan.md §6.2 移植
│   │   ├── taobao.ts            # ← 上面 §2.2 设计
│   │   └── index.ts             # matchProfile(url) 路由
│   │
│   ├── collector/
│   │   ├── scan.ts              # scanCurrentPage() 入口
│   │   ├── text.ts              # 文本提取
│   │   ├── image.ts             # 图片提取 + 分组
│   │   ├── url.ts               # ← 从 plan.md §6.3 移植
│   │   ├── dom.ts               # waitForAny / onUrlChange
│   │   └── dedupe.ts            # dedupeKey()
│   │
│   ├── export/
│   │   ├── filesystem.ts        # File System Access API 封装
│   │   ├── builder.ts           # 构建 product.json / sources.json
│   │   └── images.ts            # 图片写盘(调 background 代理)
│   │
│   ├── storage/
│   │   ├── keys.ts              # SH_ROOT_DIR / SH_CURRENT_FOLDER
│   │   └── settings.ts          # 配置读写
│   │
│   └── schema/
│       └── product.ts           # ProductJson / SourcesJson TS 类型
│
└── components/                  # Side Panel UI 组件
    ├── ScanResult.tsx           # 采集结果展示
    ├── ImagePicker.tsx          # 分组图片勾选
    └── FolderSelector.tsx       # 文件夹选择/新建

5. 关键技术点

5.1 File System Access API 核心代码

// src/export/filesystem.ts

let rootDirHandle: FileSystemDirectoryHandle | null = null;

export async function selectRootDir(): Promise<void> {
  rootDirHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
  // 持久化到 IndexedDBWXT 有 storage.defineItem 封装)
  await storage.setItem('local:SH_ROOT_DIR', rootDirHandle);
}

export async function ensureRootDir(): Promise<FileSystemDirectoryHandle> {
  if (!rootDirHandle) {
    rootDirHandle = await storage.getItem('local:SH_ROOT_DIR');
  }
  if (!rootDirHandle) {
    throw new Error('请先选择保存目录');
  }
  // 验证权限
  if (await rootDirHandle.queryPermission({ mode: 'readwrite' }) !== 'granted') {
    await rootDirHandle.requestPermission({ mode: 'readwrite' });
  }
  return rootDirHandle;
}

export async function writeProductFolder(
  folderName: string,
  data: {
    product: ProductJson;
    sources: SourcesJson;
    images: Array<{ file: string; blob: Blob }>;
  }
): Promise<void> {
  const root = await ensureRootDir();
  const productDir = await root.getDirectoryHandle(folderName, { create: true });

  // 写 product.json
  const productFile = await productDir.getFileHandle('product.json', { create: true });
  const w1 = await productFile.createWritable();
  await w1.write(JSON.stringify(data.product, null, 2));
  await w1.close();

  // 写 sources.json
  const sourcesFile = await productDir.getFileHandle('sources.json', { create: true });
  const w2 = await sourcesFile.createWritable();
  await w2.write(JSON.stringify(data.sources, null, 2));
  await w2.close();

  // 写图片(分组到子目录)
  const imagesDir = await productDir.getDirectoryHandle('images', { create: true });
  for (const img of data.images) {
    const [group] = img.file.split('/');  // "main/main-001.jpg" → "main"
    const groupDir = await imagesDir.getDirectoryHandle(group, { create: true });
    const filename = img.file.split('/')[1];
    const fh = await groupDir.getFileHandle(filename, { create: true });
    const w = await fh.createWritable();
    await w.write(img.blob);
    await w.close();
  }
}

5.2 跨页去重(读 sources.json

export async function readExistingSources(
  folderName: string
): Promise<Set<string>> {
  try {
    const root = await ensureRootDir();
    const productDir = await root.getDirectoryHandle(folderName);
    const sourcesFile = await productDir.getFileHandle('sources.json');
    const file = await sourcesFile.getFile();
    const text = await file.text();
    const sources: SourcesJson = JSON.parse(text);
    return new Set(sources.dedupeKeys || []);
  } catch {
    return new Set();  // 文件夹不存在或首次采集
  }
}

6. Side Panel UI 交互(简化版)

┌─ 1688/淘宝 采集助手 ────────────┐
│                               │
│ 保存到: ~/Ozon商品库/         │
│        [选择目录]              │
│                               │
│ 当前文件夹: 儿童保温杯_316    │
│            [新建]              │
│                               │
├─────────────────────────────── │
│ 本页识别到:                    │
│                               │
│ 标题: 儿童316不锈钢保温杯…    │
│                               │
│ ☑ 主图 (6)          [全选]    │
│   [缩略图缩略图...]            │
│                               │
│ ☑ SKU图 (4)         [全选]    │
│   蓝色 粉色 绿色 白色          │
│                               │
│ ☐ 详情图 (9)        [全选]    │
│   ⚠️ 0张,请滚到底部后重新采集  │
│                               │
│ ☑ 视频 (1)                    │
│                               │
├─────────────────────────────── │
│ 已选 11 项                     │
│                               │
│ [开始采集]  [追加到文件夹]     │
└───────────────────────────────┘

交互要点

  • 首次使用提示选择根目录(只需一次)
  • 文件夹名默认取商品标题(可改)
  • 详情图为 0 时明确提示原因
  • "追加到文件夹"按钮读 sources.json,标灰重复项

7. 开工前检查清单

环境

  • Node.js 18+ / pnpm 已安装
  • Chrome 114+File System Access 与 Side Panel 最低版本)

技术决策确认

  • Side Panel UI 用 React(已定)还是原生 JS? → React
  • 要不要一期就做淘宝,还是先只做 1688? → 都做,复用度高
  • product.json 的 TS 类型现在就手写,还是等 Pydantic 先写? → 手写,用契约文档

文件准备

  • docs/extension/drafts/*.ts 要不要直接搬到 extension/src/等项目初始化后再搬
  • reference/1688-extension/ 的 bundle 要不要进 git已在 .gitignore,不进

8. 下一步

我可以:

A. 立刻初始化项目(会生成约 20 个文件)

cd /Users/joey-xd/sites/seller-store/ozon-seller-kit
mkdir extension && cd extension
pnpm create wxt@latest .
# 选 React + TypeScript

B. 先写 M2 的核心类型和工具,验证设计

  • src/schema/product.ts(按契约文档)
  • src/collector/url.ts(从 plan.md 移植)
  • src/profiles/types.ts(从 plan.md 移植)

C. 分步实现,每个里程碑验收后再进下一个

你倾向哪个?还是有其他想法?