feat: 开发采集、采集箱和商品编辑功能
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 1688 采集配置
|
||||
* 从 docs/extension/plan.md §6.2 移植(选择器来自 v1.1.8 生产 bundle)
|
||||
*/
|
||||
import type { SiteProfile } from './types';
|
||||
|
||||
export const profile1688: SiteProfile = {
|
||||
id: '1688',
|
||||
name: '1688',
|
||||
|
||||
urlPatterns: [/^https:\/\/detail\.1688\.com\/offer\/\d+\.html/],
|
||||
|
||||
extractItemId: (url) => url.match(/\/offer\/(\d+)\.html/)?.[1] ?? null,
|
||||
|
||||
readySelectors: ['.title-content', '#dt-tab', '#screen', '#content'],
|
||||
readyTimeoutMs: 10_000,
|
||||
|
||||
// 懒加载真实地址在 data-* 上(顺序不能动)
|
||||
defaultSrcProps: ['data-lazyload-src', 'data-src', 'currentSrc', 'src'],
|
||||
|
||||
refererOrigin: 'https://www.1688.com',
|
||||
|
||||
textRules: [
|
||||
{
|
||||
kind: 'title',
|
||||
// 标题被拆成多个 .title-text span,必须 join
|
||||
selectors: ['.title-content .title-text', '.title-content h1', '.od-pc-offer-title', 'h1'],
|
||||
extract: 'join',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
kind: 'price',
|
||||
selectors: ['.price-original', '.od-pc-offer-price-priceRange', '.price .value'],
|
||||
extract: 'first'
|
||||
},
|
||||
{
|
||||
kind: 'params',
|
||||
selectors: [
|
||||
'.offer-attr-list .offer-attr-item',
|
||||
'.od-pc-attribute-table tr',
|
||||
'.obj-content .table-tr'
|
||||
],
|
||||
extract: 'table',
|
||||
tableKeySelector: '.offer-attr-item-name, td:first-child, .table-th',
|
||||
tableValueSelector: '.offer-attr-item-value, td:last-child, .table-td'
|
||||
},
|
||||
{
|
||||
kind: 'desc',
|
||||
selectors: ['.de-description-detail', '#detailContentContainer', '.html-description'],
|
||||
extract: 'join'
|
||||
}
|
||||
],
|
||||
|
||||
imageGroups: [
|
||||
{
|
||||
key: 'main',
|
||||
name: '主图',
|
||||
type: 'img',
|
||||
// 四套画廊变体(说明 1688 至少有四个线上版本)
|
||||
selectors: [
|
||||
'#recyclerview .detail-gallery-turn-wrapper .detail-gallery-img',
|
||||
'#screen .od-gallery-turn-item-wrapper .od-gallery-img',
|
||||
'#content .od-scroller-item .v-image-cover',
|
||||
'#content .od-picture-gallery-list .v-image-cover',
|
||||
'#dt-tab img',
|
||||
'.detail-gallery-turn img.detail-gallery-img',
|
||||
'.img-list-wrapper img.od-gallery-img'
|
||||
],
|
||||
activeSelectors: [
|
||||
'.detail-gallery-turn-wrapper.prepic-active .detail-gallery-img',
|
||||
'.od-gallery-turn-item-wrapper.prepic-active .od-gallery-img',
|
||||
'.v-image-cover.image-item-active'
|
||||
],
|
||||
minWidth: 200,
|
||||
minHeight: 200
|
||||
},
|
||||
{
|
||||
key: 'sku',
|
||||
name: 'SKU图片',
|
||||
type: 'img',
|
||||
selectors: [
|
||||
'.pc-sku-wrapper .prop-item-inner-wrapper',
|
||||
'.sku-item-wrapper',
|
||||
'.specification-cell',
|
||||
'.sku-filter-button',
|
||||
'.expand-view-item',
|
||||
'.feature-item img'
|
||||
],
|
||||
// SKU 缩略图是 CSS 背景图
|
||||
srcProps: ['backgroundImage'],
|
||||
// 规格名(五种 DOM 结构)
|
||||
nameSelectors: ['.prop-name', '.sku-item-name', '.item-label', '.label-name', '.normal-text'],
|
||||
minWidth: 20,
|
||||
minHeight: 20
|
||||
},
|
||||
{
|
||||
key: 'detail',
|
||||
name: '详情图',
|
||||
type: 'img',
|
||||
selectors: [
|
||||
'.de-description-detail img',
|
||||
'#detailContentContainer img',
|
||||
'.html-description img'
|
||||
],
|
||||
minWidth: 300,
|
||||
minHeight: 100
|
||||
},
|
||||
{
|
||||
key: 'video',
|
||||
name: '视频',
|
||||
type: 'video',
|
||||
selectors: ['.lib-video video', 'video']
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Profile 路由 - 根据 URL 匹配平台
|
||||
*/
|
||||
import type { SiteProfile } from './types';
|
||||
import { profile1688 } from './1688';
|
||||
import { profileTaobao } from './taobao';
|
||||
|
||||
const PROFILES: SiteProfile[] = [
|
||||
profile1688,
|
||||
profileTaobao
|
||||
];
|
||||
|
||||
export function matchProfile(url: string): SiteProfile | null {
|
||||
for (const p of PROFILES) {
|
||||
if (p.urlPatterns.some(re => re.test(url))) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export { profile1688, profileTaobao };
|
||||
export type { SiteProfile };
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 淘宝 / 天猫采集配置
|
||||
*
|
||||
* 选择器全部来自真实页面实测(2026-08-11,两个商品页各跑一轮反向探测):
|
||||
* 天猫 detail.tmall.com/item.htm?id=960057430812
|
||||
* 淘宝 item.taobao.com/item.htm?id=1060253247160
|
||||
* 两站 DOM 完全一致(同一套前端),一份 profile 覆盖。
|
||||
*
|
||||
* 类名是 CSS Modules 的 `语义前缀--哈希` 形式,哈希每次构建都变,
|
||||
* 所以一律用 `[class*="前缀--"]` 前缀匹配。
|
||||
*
|
||||
* 结尾那个 `--` 不能省——它把父容器和子元素区分开:
|
||||
* `generalParamsInfoItem--` 不会误命中 `generalParamsInfoItemTitle--`。
|
||||
*
|
||||
* 实测证据见 docs/extension/selectors-taobao.md
|
||||
*/
|
||||
import type { SiteProfile } from './types';
|
||||
|
||||
export const profileTaobao: SiteProfile = {
|
||||
id: 'taobao',
|
||||
name: '淘宝/天猫',
|
||||
|
||||
urlPatterns: [
|
||||
/^https:\/\/item\.taobao\.com\/item\.htm/,
|
||||
/^https:\/\/detail\.tmall\.com\/item\.htm/,
|
||||
],
|
||||
|
||||
extractItemId: (url) => url.match(/[?&]id=(\d+)/)?.[1] ?? null,
|
||||
|
||||
// 页面上没有 <h1>,别再拿它探活
|
||||
readySelectors: [
|
||||
'[class*="mainTitle--"]',
|
||||
'[class*="picGallery--"]',
|
||||
'#picGalleryEle',
|
||||
],
|
||||
readyTimeoutMs: 10_000,
|
||||
|
||||
// 阿里系 CDN 规则与 1688 相同
|
||||
defaultSrcProps: ['data-lazyload-src', 'data-src', 'currentSrc', 'src'],
|
||||
|
||||
refererOrigin: 'https://www.taobao.com',
|
||||
|
||||
textRules: [
|
||||
{
|
||||
kind: 'title',
|
||||
// mainTitle-- 是纯文本节点(探测里 imgs=0),最干净
|
||||
// ItemTitle-- / MainTitle-- 是外层容器,带图标,作兜底
|
||||
// 注意:属性选择器区分大小写,三个都得写
|
||||
selectors: [
|
||||
'[class*="mainTitle--"]',
|
||||
'[class*="MainTitle--"]',
|
||||
'[class*="ItemTitle--"]',
|
||||
],
|
||||
extract: 'first',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
kind: 'price',
|
||||
// highlightPrice-- 是当前实际售价,两站一致
|
||||
// priceWrap-- 是外层,会把"优惠前¥36.8"一起带进来,只作兜底
|
||||
selectors: [
|
||||
'[class*="highlightPrice--"]',
|
||||
'[class*="priceWrap--"]',
|
||||
],
|
||||
extract: 'first',
|
||||
},
|
||||
{
|
||||
kind: 'params',
|
||||
// generalParamsInfoItem-- 每项含 Title(键) + SubTitle(值)
|
||||
selectors: ['[class*="generalParamsInfoItem--"]'],
|
||||
extract: 'table',
|
||||
tableKeySelector: '[class*="ParamsInfoItemTitle--"]',
|
||||
tableValueSelector: '[class*="ParamsInfoItemSubTitle--"]',
|
||||
},
|
||||
// desc 故意不采:详情容器 detailInfo-- 里混着用户评价、参数、图文详情,
|
||||
// join 出来是一坨无法使用的字符串。1688/淘宝的中文文案对 Ozon 价值也低
|
||||
// (见 docs/extension/1688-taobao-implementation.md 采集优先级)。
|
||||
],
|
||||
|
||||
imageGroups: [
|
||||
{
|
||||
key: 'main',
|
||||
name: '主图',
|
||||
type: 'img',
|
||||
// picGallery-- 内含大图 + 缩略图,同一张图的两种尺寸
|
||||
// toOriginalUrl() 剥掉尺寸后缀后 dedupeKey 相同,会自动去重
|
||||
selectors: [
|
||||
'[class*="picGallery--"] img',
|
||||
'#picGalleryEle img',
|
||||
'[class*="thumbnailPic--"]',
|
||||
],
|
||||
// 不设 minWidth:缩略图 naturalWidth 只有 60 左右,
|
||||
// 按 200 过滤会把主图全误杀(原图靠 toOriginalUrl 还原)
|
||||
},
|
||||
{
|
||||
key: 'sku',
|
||||
name: 'SKU图片',
|
||||
type: 'img',
|
||||
// ★ 与 1688 不同:淘宝 SKU 是真实 <img>,不是 CSS 背景图
|
||||
// 探测证据:valueItem-- n=22 imgs=22(每项恰含一张 img)
|
||||
// 所以这里不能用 srcProps: ['backgroundImage']
|
||||
selectors: [
|
||||
'[class*="valueItem--"]',
|
||||
'[class*="valueItemImgWrap--"]',
|
||||
],
|
||||
nameSelectors: ['[class*="valueItemText--"]'],
|
||||
minWidth: 20,
|
||||
minHeight: 20,
|
||||
},
|
||||
{
|
||||
key: 'detail',
|
||||
name: '详情图',
|
||||
type: 'img',
|
||||
// 图文详情是懒加载的,需用户点开「图文详情」tab 或滚到底
|
||||
selectors: [
|
||||
'[class*="tabDetailWrap--"] img',
|
||||
'[class*="detailInfo--"] img',
|
||||
],
|
||||
// detailInfo-- 同时包着「用户评价」区,买家晒单图能有 400-800px,
|
||||
// 光靠 minWidth 滤不掉。这些图带水印、质量差,不能采
|
||||
excludeWithin: [
|
||||
'[class*="Comment--"]',
|
||||
'[class*="comments--"]',
|
||||
'[class*="userInfo--"]',
|
||||
'[class*="rate"]',
|
||||
],
|
||||
minWidth: 300,
|
||||
minHeight: 100,
|
||||
},
|
||||
{
|
||||
key: 'video',
|
||||
name: '视频',
|
||||
type: 'video',
|
||||
selectors: ['[class*="picGallery--"] video', 'video'],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Site Profile - 平台采集配置(声明式)
|
||||
* 从 docs/extension/plan.md §6.1 移植
|
||||
*/
|
||||
|
||||
export type TextKind = 'title' | 'price' | 'params' | 'desc';
|
||||
export type ImageGroupKey = 'main' | 'sku' | 'detail' | 'video';
|
||||
export type SrcProp = 'data-lazyload-src' | 'data-src' | 'currentSrc' | 'src' | 'backgroundImage';
|
||||
|
||||
export interface TextRule {
|
||||
kind: TextKind;
|
||||
selectors: string[]; // 多套选择器,逐个尝试
|
||||
extract: 'join' | 'first' | 'table';
|
||||
tableKeySelector?: string; // table 模式的 key 选择器
|
||||
tableValueSelector?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export interface ImageGroupRule {
|
||||
key: ImageGroupKey;
|
||||
name: string;
|
||||
type: 'img' | 'video';
|
||||
selectors: string[];
|
||||
srcProps?: SrcProp[]; // 覆盖 defaultSrcProps
|
||||
nameSelectors?: string[]; // SKU 规格名来源
|
||||
activeSelectors?: string[]; // 画廊"当前高亮"元素(排除)
|
||||
/** 位于这些容器内的图片一律跳过(用 el.closest 判断)。
|
||||
* 典型用途:详情区里混着的用户评价晒单图 */
|
||||
excludeWithin?: string[];
|
||||
minWidth?: number;
|
||||
minHeight?: number;
|
||||
}
|
||||
|
||||
export interface SiteProfile {
|
||||
id: string;
|
||||
name: string;
|
||||
urlPatterns: RegExp[];
|
||||
extractItemId: (url: string) => string | null;
|
||||
readySelectors: string[]; // 等待这些元素出现
|
||||
readyTimeoutMs?: number;
|
||||
defaultSrcProps: SrcProp[];
|
||||
textRules: TextRule[];
|
||||
imageGroups: ImageGroupRule[];
|
||||
originalUrlRules?: Array<{ match: RegExp; replace: string }>;
|
||||
refererOrigin?: string; // 图片防盗链需要的 Referer
|
||||
}
|
||||
Reference in New Issue
Block a user