电商平台图片采集技术完全指南:从爬虫原理到浏览器方案的完整技术演进与选型深度解析

引言

很多做电商的朋友在问:"推荐个下载淘宝店铺和天猫店铺商品图片的软件"

做电商运营的朋友每天都要存大量的商品图片。主图要存、详情图要存、SKU颜色图要存、模特展示图要存......一个商品几十张图,手动右键保存效率太低。

市面上的工具不少,但真正好用、稳定、能下载高清原图的却不多。为什么有的工具用着用着就坏了?为什么有的工具能下载原图有的只能下缩略图?为什么淘宝一改版某些工具就不能用了?

这些问题背后的答案,都指向同一个核心------技术选型。

本文将从技术原理、实现方案、实测数据、选型建议等多个维度,全方位解析电商平台图片采集的技术路线,帮助你理解"为什么有的工具稳定,有的工具用不了多久就废了"。

目录

  1. 电商图片采集的核心需求分析

  2. 电商平台商品页面的技术结构

  3. 电商平台图片的URL格式与多尺寸版本

  4. SKU图的识别与分类难点

  5. 电商平台反爬机制的完整演进

  6. 技术路线一:爬虫方案的深度剖析

  7. 技术路线二:浏览器插件方案的深度剖析

  8. 技术路线三:浏览器方案的深度剖析

  9. 三条路线的多维度实测对比

  10. 浏览器方案的技术实现细节

  11. 图片URL的完整处理链路

  12. SKU图智能分类的完整算法

  13. 懒加载的完整处理方案

  14. 视频下载的完整技术实现

  15. 批量下载与任务队列设计

  16. 剪贴板监听与自动化流程

  17. 错误处理与重试机制

  18. 性能优化策略

  19. 各平台差异适配

  20. 火蚁一键存图的完整技术架构

  21. 各平台用户常见问题解答

  22. 最终总结

一、电商图片采集的核心需求分析

1.1 电商运营需要下载哪些图片?

电商运营日常需要下载的图片类型包括:

图片类型 典型数量 用途 重要性
主图 5-8张 商品轮播展示 极高
SKU图(颜色/尺码图) 5-20张 规格细节展示
详情图 5-30张 商品描述
模特展示图 2-10张 上身效果展示
主图视频 0-1个 动态展示

1.2 电商运营的核心痛点

痛点 具体表现 时间成本
操作繁琐 每张图需要右键-另存为-选位置-确认 每个商品5-10分钟
图片模糊 下载的是缩略图,放大就糊了 无法使用,需重新下载
分类困难 主图和颜色图混在一起 每个商品额外3-5分钟
视频难存 主图视频需要录屏 每个视频2-3分钟
工具不稳定 平台改版后工具失效 工作停摆1-7天
平台覆盖少 不支持抖音、亚马逊等 需要多套工具

1.3 好用的工具应该具备什么特征?

特征 说明 重要性
高清原图 下载的是原图而非缩略图 极高
自动分类 主图/SKU图/详情图自动分文件夹 极高
稳定可靠 不受平台改版影响 极高
操作简单 复制链接即可下载
视频下载 直接下载原画质视频
平台覆盖广 支持淘宝、京东、拼多多、抖音、亚马逊等
安全可靠 不收集用户数据

1.4 时间成本量化分析

手动保存一个商品的完整流程:

  1. 打开商品页面(10秒)

  2. 右键保存主图5张(10秒)

  3. 往下滚动找到颜色图,右键保存(20秒)

  4. 往下滚动找到详情图,右键保存(30秒)

  5. 创建文件夹,命名(10秒)

  6. 将所有图片拖入文件夹(10秒)

  7. 筛选分类主图/颜色图/详情图(60秒)

  8. 重命名所有图片(60秒)

总计:约3.5-5分钟/商品

一天处理30个商品:1.75-2.5小时

使用专业工具后:

  • 复制链接(2秒)

  • 粘贴到软件(2秒)

  • 勾选下载(3秒)

  • 自动分类(0秒)

  • 自动命名(0秒)

总计:约7秒/商品

一天处理30个商品:约3.5分钟

效率提升:30-40倍

二、电商平台商品页面的技术结构

2.1 淘宝商品页面的DOM结构

淘宝商品页面的DOM结构经历了多次演进:

主图区域的典型DOM结构:

html

复制代码
<div class="tb-main-pic">
    <div class="J_UlThumb">
        <ul class="tb-thumb">
            <li class="tb-thumb-item">
                <img src="//img.alicdn.com/xxx_50x50.jpg" 
                     data-src="//img.alicdn.com/xxx_50x50.jpg" 
                     alt="商品主图1">
            </li>
            <!-- 通常共5张 -->
        </ul>
        <div class="tb-main-img">
            <img class="J_zoomPic" src="//img.alicdn.com/xxx.jpg">
        </div>
    </div>
</div>

SKU图区域的典型DOM结构:

html

复制代码
<div class="tb-sku" data-property="颜色">
    <div class="tb-sku-title">颜色分类</div>
    <div class="tb-sku-list">
        <div class="sku-item J_skuItem" data-value="红色">
            <img src="//img.alicdn.com/red_50x50.jpg" alt="红色">
            <span class="sku-name">红色</span>
        </div>
        <!-- 更多颜色 -->
    </div>
</div>

详情图区域的典型DOM结构:

html

复制代码
<div id="description" class="tb-detail">
    <div class="desc">
        <img src="//img.alicdn.com/detail_1.jpg" 
             data-src="//img.alicdn.com/detail_1.jpg" 
             alt="商品详情1">
        <!-- 更多详情图 -->
    </div>
</div>

2.2 京东商品页面的DOM结构

html

复制代码
<!-- 京东主图结构 -->
<div class="spec-img">
    <img src="//img13.360buyimg.com/n1/xxx.jpg">
</div>

<!-- 京东SKU结构 -->
<div class="sku-img-list">
    <div class="sku-img-item" title="红色">
        <img src="//img13.360buyimg.com/red_thumb.jpg">
    </div>
</div>

<!-- 京东详情结构 -->
<div id="detail">
    <div class="detail-content">
        <img src="//img13.360buyimg.com/detail_1.jpg">
    </div>
</div>

2.3 拼多多商品页面的DOM结构

html

复制代码
<!-- 拼多多主图结构 -->
<div class="main-image">
    <img src="//img.pddpic.com/xxx.jpg">
</div>

<!-- 拼多多SKU结构 -->
<div class="sku-list">
    <div class="sku-item" data-value="红色">
        <img src="//img.pddpic.com/red_thumb.jpg">
        <span class="sku-name">红色</span>
    </div>
</div>

<!-- 拼多多详情结构 -->
<div class="detail-content">
    <img src="//img.pddpic.com/detail_1.jpg">
</div>

2.4 不同平台的结构差异对比

平台 主图容器 SKU容器 详情容器 图片格式
淘宝 .J_UlThumb .tb-sku #description jpg/png
天猫 .tb-thumb .J_sku .desc jpg/png
京东 .spec-img .sku-img-list #detail jpg
拼多多 .main-image .sku-list .detail-content webp/jpg

三、电商平台图片的URL格式与多尺寸版本

3.1 淘宝图片的多个尺寸版本

淘宝在CDN上存储了多个尺寸版本:

URL格式 分辨率 文件大小 使用场景
xxx_50x50.jpg 50x50 5-15KB 最小缩略图
xxx_100x100.jpg 100x100 15-30KB 列表页
xxx_200x200.jpg 200x200 30-60KB 搜索结果
xxx_400x400.jpg 400x400 60-120KB 详情页缩略
xxx_800x800.jpg 800x800 200-400KB 大图展示
xxx.jpg 最大分辨率 300KB-2MB 原图

3.2 京东图片的尺寸版本

URL格式 含义 分辨率
xxx_n1.jpg 缩略图1 较小
xxx_n2.jpg 缩略图2 中等
xxx_n0.jpg 原图 最大

3.3 拼多多图片的尺寸版本

URL格式 含义 分辨率
xxx_100x100.jpg 缩略图 100x100
xxx_200x200.jpg 中等图 200x200
xxx.jpg 原图 最大

3.4 原图URL的转换规则

淘宝原图转换:

javascript

复制代码
function getTaobaoOriginalUrl(url) {
    if (!url) return null;
    
    // 去除URL参数
    url = url.split('?')[0];
    
    // 去除尺寸后缀:_50x50.jpg -> .jpg
    url = url.replace(/_\d+x\d+\./g, '.');
    
    // 去除sum后缀
    url = url.replace(/\.sum\./g, '.');
    
    return url;
}

转换示例:

输入URL 输出URL
https://img.alicdn.com/xxx_100x100.jpg https://img.alicdn.com/xxx.jpg
https://img.alicdn.com/xxx_400x400.jpg https://img.alicdn.com/xxx.jpg
https://img.alicdn.com/xxx.sum.jpg https://img.alicdn.com/xxx.jpg

京东原图转换:

javascript

复制代码
function getJdOriginalUrl(url) {
    if (!url) return null;
    url = url.split('?')[0];
    url = url.replace(/\/n\d\//, '/n0/');
    return url;
}

拼多多原图转换:

javascript

复制代码
function getPddOriginalUrl(url) {
    if (!url) return null;
    url = url.split('?')[0];
    url = url.replace(/_\d+x\d+\./g, '.');
    url = url.replace(/\.webp$/i, '.jpg');
    return url;
}

3.5 为什么很多工具只能下载缩略图?

爬虫方案直接解析HTML,获取到的往往是缩略图地址(因为缩略图出现在HTML中的概率更高)。浏览器方案等页面完全加载后从DOM中获取的则是原图地址。

方案 获取的图片类型 原因
爬虫方案 缩略图 直接解析HTML,拿到的就是缩略图地址
浏览器方案 原图 页面完全加载后,从完整DOM中获取原图地址

四、SKU图的识别与分类难点

4.1 SKU图的特点

SKU图是电商商品中最难处理的图片类型:

特点 说明
数量多 一个商品可能5-20张SKU图
名称关联 每张图关联一个规格名称(红色、蓝色、S码等)
位置固定 位于特定容器内
格式统一 通常是缩略图,需要转换原图

4.2 SKU图分类的技术难点

javascript

复制代码
function extractSkuName(item) {
    // 第一优先级:专用名称元素
    const nameEl = item.querySelector('.sku-name, .J_skuName');
    if (nameEl) return nameEl.textContent.trim();
    
    // 第二优先级:data属性
    const dataValue = item.getAttribute('data-value');
    if (dataValue) return dataValue;
    
    // 第三优先级:title属性
    const title = item.getAttribute('title');
    if (title) return title;
    
    // 第四优先级:内部文本
    const text = item.textContent.trim();
    if (text) return text;
    
    return '规格';
}

4.3 分类后的存储结构

text

复制代码
商品标题/
├── 主图/
│   ├── 主图_1.jpg
│   ├── 主图_2.jpg
│   └── 主图_3.jpg
├── SKU图/
│   ├── 红色.jpg
│   ├── 蓝色.jpg
│   ├── S码.jpg
│   ├── M码.jpg
│   └── L码.jpg
└── 详情图/
    ├── 详情图_1.jpg
    └── 详情图_2.jpg

4.4 SKU图分类的业务价值

对于服装、鞋包等类目的电商运营来说,SKU图自动分类是刚需功能。一个商品通常有多个颜色和尺码,每个规格对应独立的细节图。手动分类需要逐个对照商品页面,耗时且容易出错。

通过自动分类,SKU图按以下结构归档,用户无需再手动筛选和命名。

五、电商平台反爬机制的完整演进

5.1 反爬机制的时间线

时期 反爬手段 对工具的影响
2010-2015 User-Agent检测 换UA就能绕过
2015-2018 签名参数(_tb_token等) 需要逆向JS
2018-2020 动态令牌+行为验证 模拟请求难以通过
2020-2023 浏览器指纹检测 需要真实浏览器环境
2023-2026 指纹+行为轨迹分析 几乎无法用纯HTTP请求模拟

5.2 浏览器指纹检测

电商平台会在页面中执行以下检测:

javascript

复制代码
function detectBrowserFingerprint() {
    const checks = {};
    
    // Canvas指纹
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    ctx.textBaseline = 'top';
    ctx.font = '14px Arial';
    ctx.fillStyle = '#f60';
    ctx.fillRect(125, 1, 62, 20);
    ctx.fillStyle = '#069';
    ctx.fillText('指纹', 2, 15);
    checks.canvasFingerprint = canvas.toDataURL();
    
    // WebGL指纹
    const gl = document.createElement('canvas').getContext('webgl');
    if (gl) {
        checks.webglRenderer = gl.getParameter(gl.RENDERER);
        checks.webglVendor = gl.getParameter(gl.VENDOR);
    }
    
    // 字体指纹
    const fontList = ['Arial', 'Verdana', 'Times New Roman', 'Helvetica', 'Courier New'];
    const availableFonts = fontList.filter(font => {
        return document.createElement('span').style.fontFamily === font;
    });
    checks.availableFonts = availableFonts;
    
    // 检测是否为自动化环境
    checks.isWebDriver = navigator.webdriver === true;
    checks.pluginsCount = navigator.plugins.length;
    checks.languages = navigator.languages;
    
    return checks;
}

5.3 TLS指纹检测

不同客户端的TLS指纹特征:

客户端 TLS库 JA3指纹特征 检测结果
Chrome BoringSSL 真实Chrome指纹 ✅ 正常
Python requests OpenSSL 爬虫指纹 ❌ 识别
Java HttpClient OpenSSL 爬虫指纹 ❌ 识别

5.4 IP频率限制

电商平台会对IP的请求频率进行监控:

python

复制代码
class IPRateLimiter:
    def __init__(self):
        self.request_records = {}
    
    def check(self, ip):
        now = time.time()
        records = self.request_records.get(ip, [])
        records = [t for t in records if now - t < 60]
        if len(records) > 30:
            return True, "IP被限流"
        records.append(now)
        self.request_records[ip] = records
        return False, "正常"

5.5 反爬机制对工具的影响

工具类型 受影响程度 说明
爬虫方案 严重 TLS指纹+浏览器指纹双重检测,基本无法正常工作
浏览器插件 中等 运行在真实Chrome中,指纹检测通过,但依赖Chrome版本
浏览器方案 运行在独立Chromium中,完全真实浏览器环境

六、技术路线一:爬虫方案的深度剖析

6.1 工作原理

爬虫方案的核心思路是绕过浏览器,直接向电商平台服务器发送HTTP请求:

python

复制代码
import requests
from bs4 import BeautifulSoup

def fetch_taobao_product(url):
    headers = {'User-Agent': 'Mozilla/5.0...'}
    resp = requests.get(url, headers=headers)
    soup = BeautifulSoup(resp.text, 'html.parser')
    # 依赖淘宝的CSS选择器(脆弱!)
    img_urls = soup.select('.J_UlThumb img')
    return [img.get('src') for img in img_urls]

6.2 爬虫方案的五大问题

问题一:TLS指纹检测

Python的requests库使用OpenSSL,TLS指纹特征明显,电商平台能轻松识别。

问题二:无法执行JavaScript

电商平台商品页的很多图片URL是动态生成的,爬虫拿不到。

问题三:强依赖DOM结构

电商平台改版后CSS类名变化,爬虫立刻失效。

问题四:IP频率限制

短时间内大量请求会被封IP,需要维护代理池。

问题五:验证码

异常请求会触发验证码,无法自动处理。

6.3 爬虫方案的维护成本

成本项 说明 月均成本
服务器 运行爬虫程序 $50-200
IP代理池 应对IP封禁 $50-150
人力维护 应对平台改版 $100-500
月均成本 $200-850
成功率 70-80%

6.4 爬虫方案的典型问题案例

案例:淘宝2024年改版事件

2024年,淘宝对商品详情页进行了大规模改版。改版前后的变化:

版本 主图容器类名 SKU容器类名
改版前 .J_UlThumb .tb-sku
改版后 .tb-thumb .J_sku

这次改版导致市面上一大批爬虫工具失效了3-7天,开发者紧急修复,用户只能等待。

七、技术路线二:浏览器插件方案的深度剖析

7.1 工作原理

浏览器插件方案寄生在Chrome浏览器中,利用Chrome的渲染能力获取页面内容。

javascript

复制代码
// Chrome Extension 内容脚本
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    if (request.type === 'EXTRACT_IMAGES') {
        const images = [];
        document.querySelectorAll('img').forEach(img => {
            const url = img.src || img.getAttribute('data-src');
            if (url) images.push(url);
        });
        sendResponse({ images: images });
    }
});

7.2 浏览器插件方案的优势

  • 运行在真实Chrome环境,反爬检测通过

  • 完整的JS执行能力

  • 用户安装方便

7.3 浏览器插件方案的四大问题

问题一:依赖Chrome版本

Chrome每几周更新一次,Extension API可能变化,插件可能失效。

Chrome版本 Extension API变化 影响
120→121 chrome.tabs.executeScript 参数变化 部分功能失效
121→122 Manifest V3强制要求 需重写插件
122→123 Service Worker生命周期变化 后台脚本失效

问题二:权限过大

需要申请读取所有网页数据的权限,用户信任度低。

问题三:性能受限

运行在Chrome渲染进程里,下载大量图片时会卡顿。

问题四:Manifest V3限制

从2024年开始,Google强制推行Manifest V3,Extension的能力被大幅限制。

八、技术路线三:浏览器方案的深度剖析

8.1 什么是浏览器方案?

浏览器方案基于Chromium开源项目,封装成独立的桌面应用。

Chromium是Google开源的浏览器内核项目,Chrome、Edge、Opera等浏览器都基于它开发。CEF(Chromium Embedded Framework)是将Chromium嵌入桌面应用的成熟框架。

8.2 浏览器方案的架构

text

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                          桌面客户端                                         │
├─────────────────────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                    Chromium Embedded Framework                       │    │
│  │                        (CEF / Chromium内核)                          │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                      │                                       │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                        业务层                                        │    │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐       │    │
│  │  │URL加载  │ │DOM提取  │ │智能分类 │ │图片处理 │ │视频处理 │       │    │
│  │  └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘       │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                      │                                       │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                        功能层                                        │    │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐       │    │
│  │  │剪贴板   │ │自动分类 │ │原图转换 │ │批量下载 │ │历史记录 │       │    │
│  │  │监听     │ │        │ │        │ │        │ │        │       │    │
│  │  └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘       │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

8.3 浏览器方案的优势

对比维度 爬虫方案 浏览器插件 浏览器方案
浏览器指纹 ❌ 没有 ✅ 有(依赖Chrome) ✅ 有(独立)
页面渲染 ❌ 不渲染 ✅ Chrome渲染 ✅ 自己渲染
JS执行 ❌ 不执行 ✅ Chrome执行 ✅ 自己执行
平台改版影响 ❌ 强依赖 ⚠️ 可能受影响 ✅ 完全不受影响
独立运行 ❌ 必须开Chrome

8.4 浏览器方案的技术实现

CEF初始化:

cpp

复制代码
#include "include/cef_app.h"

class SimpleApp : public CefApp {
public:
    void OnBeforeCommandLineProcessing(
        const CefString& process_type,
        CefRefPtr<CefCommandLine> command_line) override {
        
        command_line->AppendSwitch("disable-gpu");
        command_line->AppendSwitch("disable-plugins");
        command_line->AppendSwitch("remote-debugging-port=0");
        command_line->AppendSwitch("disable-blink-features=AutomationControlled");
        command_line->AppendSwitchWithValue(
            "user-agent",
            "Mozilla/5.0 Chrome/120.0.0.0 Safari/537.36"
        );
    }
};

页面等待策略:

javascript

复制代码
async function waitForPageReady() {
    // 第一重:等待DOM就绪
    while (document.readyState !== 'complete') {
        await sleep(200);
    }
    
    // 第二重:等待网络空闲
    let idleCount = 0;
    while (idleCount < 2) {
        const activeRequests = performance.getEntriesByType('resource')
            .filter(r => r.duration === 0).length;
        if (activeRequests === 0) idleCount++;
        else idleCount = 0;
        await sleep(500);
    }
    
    // 第三重:等待jQuery
    while (typeof jQuery === 'undefined') {
        await sleep(100);
    }
    
    // 第四重:触发懒加载
    await triggerLazyLoad();
    
    // 第五重:额外等待
    await sleep(1000);
}

SKU图提取:

javascript

复制代码
function extractSkuImages() {
    const container = document.querySelector('.tb-sku, .J_sku');
    if (!container) return [];
    
    const items = container.querySelectorAll('.sku-item, .J_skuItem');
    const results = [];
    
    for (const item of items) {
        const nameEl = item.querySelector('.sku-name, .J_skuName');
        const name = nameEl ? nameEl.textContent.trim() : '规格';
        const img = item.querySelector('img');
        if (img) {
            let url = img.src || img.getAttribute('data-src');
            url = url.split('?')[0];
            url = url.replace(/_\d+x\d+\./g, '.');
            results.push({ url, name });
        }
    }
    return results;
}

九、三条路线的多维度实测对比

9.1 平台改版影响

维度 爬虫方案 浏览器插件 浏览器方案
依赖解析规则
平台改版影响 失效1-7天 可能失效 无影响
恢复时间 1-7天 1-3天 0天

9.2 采集成功率

测试条件:连续采集500个淘宝商品

方案 采集成功率 失败原因
爬虫方案 70-80% TLS指纹识别、验证码、IP封禁
浏览器插件 85-90% Chrome版本兼容、权限限制
浏览器方案 99%+ 极少数因网络问题失败

9.3 各维度综合对比

维度 爬虫方案 浏览器插件 浏览器方案
技术路线 模拟HTTP请求 Chrome扩展 定制浏览器
反爬风险
改版影响 严重 中等
平台覆盖 广
视频下载 困难 部分支持 ✅ 完整支持
SKU自动分类 部分
维护成本
稳定性 ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐

9.4 成本对比

成本项 爬虫方案 浏览器插件 浏览器方案
开发成本
维护成本
服务器成本
代理IP成本
总成本(年)

十、浏览器方案技术实现详解

10.1 CEF框架完整初始化

cpp

复制代码
// main.cpp - 完整CEF初始化
#include "include/cef_app.h"
#include "include/cef_client.h"
#include "include/cef_browser.h"
#include "include/wrapper/cef_helpers.h"
#include "include/wrapper/cef_closure_task.h"
#include <iostream>
#include <string>
#include <chrono>
#include <thread>

// 日志输出
#define LOG(msg) std::cout << "[LOG] " << msg << std::endl

class SimpleApp : public CefApp {
public:
    void OnBeforeCommandLineProcessing(
        const CefString& process_type,
        CefRefPtr<CefCommandLine> command_line) override {
        
        // 禁用GPU加速(降低资源占用约20%)
        command_line->AppendSwitch("disable-gpu");
        
        // 禁用插件
        command_line->AppendSwitch("disable-plugins");
        
        // 禁用远程调试(避免WebDriver检测)
        command_line->AppendSwitch("remote-debugging-port=0");
        
        // 禁用自动化控制特征(避免被识别为自动化工具)
        command_line->AppendSwitch("disable-blink-features=AutomationControlled");
        
        // 设置缓存目录
        command_line->AppendSwitchWithValue("disk-cache-dir", "./cache");
        
        // 设置User-Agent为真实Chrome
        command_line->AppendSwitchWithValue(
            "user-agent",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
            "Chrome/120.0.0.0 Safari/537.36"
        );
        
        // 禁用Web安全策略(允许跨域提取素材)
        command_line->AppendSwitch("disable-web-security");
        
        // 设置语言
        command_line->AppendSwitchWithValue("lang", "zh-CN");
        
        LOG("CEF命令行配置完成");
    }
    
    IMPLEMENT_REFCOUNTING(SimpleApp);
};

class BrowserClient : public CefClient,
                      public CefLifeSpanHandler,
                      public CefLoadHandler,
                      public CefDisplayHandler {
public:
    BrowserClient() : loading_complete_(false), load_success_(false) {}
    
    // 生命周期处理器
    CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
    CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }
    CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; }
    
    // OnAfterCreated - 浏览器创建完成
    void OnAfterCreated(CefRefPtr<CefBrowser> browser) override {
        CEF_REQUIRE_UI_THREAD();
        browser_ = browser;
        loading_complete_ = false;
        load_success_ = false;
        LOG("浏览器创建完成");
    }
    
    // OnLoadingStateChange - 加载状态变化
    void OnLoadingStateChange(CefRefPtr<CefBrowser> browser,
                              bool isLoading,
                              bool canGoBack,
                              bool canGoForward) override {
        CEF_REQUIRE_UI_THREAD();
        if (!isLoading) {
            loading_complete_ = true;
            LOG("页面加载完成");
            // 触发JS提取
            ExtractPageContent(browser);
        }
    }
    
    // OnLoadEnd - 加载结束
    void OnLoadEnd(CefRefPtr<CefBrowser> browser,
                   CefRefPtr<CefFrame> frame,
                   int httpStatusCode) override {
        CEF_REQUIRE_UI_THREAD();
        if (frame->IsMain() && httpStatusCode == 200) {
            load_success_ = true;
            LOG("主页面加载成功,状态码: 200");
        } else if (frame->IsMain()) {
            LOG("主页面加载失败,状态码: " + std::to_string(httpStatusCode));
        }
    }
    
    // 页面加载失败
    void OnLoadError(CefRefPtr<CefBrowser> browser,
                     CefRefPtr<CefFrame> frame,
                     ErrorCode errorCode,
                     const CefString& errorText,
                     const CefString& failedUrl) override {
        CEF_REQUIRE_UI_THREAD();
        LOG("页面加载错误: " + errorText.ToString());
        load_success_ = false;
    }
    
    // OnConsoleMessage - 控制台消息(用于调试)
    bool OnConsoleMessage(CefRefPtr<CefBrowser> browser,
                          cef_log_severity_t level,
                          const CefString& message,
                          const CefString& source,
                          int line) override {
        // 只输出错误信息
        if (level == LOGSEVERITY_ERROR) {
            LOG("JS错误: " + message.ToString() + " (行 " + std::to_string(line) + ")");
        }
        return false;
    }
    
    bool WaitForLoad(int timeout_seconds = 15) {
        auto start = std::chrono::steady_clock::now();
        while (!loading_complete_) {
            auto elapsed = std::chrono::steady_clock::now() - start;
            if (elapsed > std::chrono::seconds(timeout_seconds)) {
                LOG("页面加载超时");
                return false;
            }
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }
        return load_success_;
    }
    
    // 执行JS提取页面内容
    void ExtractPageContent(CefRefPtr<CefBrowser> browser) {
        CefRefPtr<CefFrame> frame = browser->GetMainFrame();
        if (!frame) {
            LOG("获取主框架失败");
            return;
        }
        
        std::string script = GetExtractScript();
        frame->ExecuteJavaScript(script, frame->GetURL(), 0);
        LOG("JS提取脚本已执行");
    }
    
    // 获取提取脚本
    std::string GetExtractScript() {
        return R"(
            (function() {
                // 获取原图URL
                function getOriginalUrl(url) {
                    if (!url) return null;
                    if (url.startsWith('data:')) return null;
                    if (url.includes('1x1') || url.includes('blank.gif')) return null;
                    url = url.split('?')[0];
                    url = url.replace(/_\d+x\d+\./g, '.');
                    url = url.replace(/\.sum\./g, '.');
                    url = url.replace(/\.webp$/i, '.jpg');
                    return url;
                }
                
                // 提取主图
                function extractMainImages() {
                    const images = [];
                    const seen = new Set();
                    const selectors = ['.J_UlThumb', '.tb-thumb', '.tb-main-pic'];
                    
                    for (const selector of selectors) {
                        const container = document.querySelector(selector);
                        if (container) {
                            const imgs = container.querySelectorAll('img');
                            imgs.forEach(img => {
                                let url = img.src || img.getAttribute('data-src');
                                if (url) {
                                    url = getOriginalUrl(url);
                                    if (url && !seen.has(url)) {
                                        seen.add(url);
                                        images.push(url);
                                    }
                                }
                            });
                            break;
                        }
                    }
                    return images;
                }
                
                // 提取SKU图
                function extractSkuImages() {
                    const skuImages = [];
                    const container = document.querySelector('.tb-sku, .J_sku');
                    if (container) {
                        const items = container.querySelectorAll('.sku-item, .J_skuItem');
                        items.forEach(item => {
                            const nameEl = item.querySelector('.sku-name, .J_skuName');
                            const name = nameEl ? nameEl.textContent.trim() : '规格';
                            const img = item.querySelector('img');
                            if (img) {
                                let url = img.src || img.getAttribute('data-src');
                                if (url) {
                                    url = getOriginalUrl(url);
                                    skuImages.push({ url: url, name: name });
                                }
                            }
                        });
                    }
                    return skuImages;
                }
                
                // 提取详情图
                function extractDetailImages() {
                    const images = [];
                    const seen = new Set();
                    const container = document.querySelector('#description, .desc');
                    if (container) {
                        const imgs = container.querySelectorAll('img');
                        imgs.forEach(img => {
                            let url = img.src || img.getAttribute('data-src');
                            if (url) {
                                url = getOriginalUrl(url);
                                if (url && !seen.has(url)) {
                                    seen.add(url);
                                    images.push(url);
                                }
                            }
                        });
                    }
                    return images;
                }
                
                // 提取视频
                function extractVideo() {
                    const video = document.querySelector('#J_ItemVideo video, .tb-video video');
                    if (video && video.src) {
                        return { url: video.src, type: video.src.endsWith('.mp4') ? 'mp4' : 'm3u8' };
                    }
                    return null;
                }
                
                // 提取标题
                function extractTitle() {
                    const el = document.querySelector('.tb-main-title, .J_mainTitle, h1');
                    if (el && el.textContent) {
                        const title = el.textContent.trim();
                        if (title.length > 3) return title;
                    }
                    return document.title || '未命名商品';
                }
                
                // 返回结果
                const result = {
                    title: extractTitle(),
                    mainImages: extractMainImages(),
                    skuImages: extractSkuImages(),
                    detailImages: extractDetailImages(),
                    video: extractVideo()
                };
                
                // 通过window对象传递结果
                window.__extractResult = result;
                
                // 控制台输出结果(用于调试)
                console.log('提取完成:', result);
                
                return result;
            })();
        )";
    }
    
    // 获取提取结果
    CefRefPtr<CefV8Value> GetExtractResult() {
        CefRefPtr<CefFrame> frame = browser_->GetMainFrame();
        if (!frame) return nullptr;
        
        std::string script = "window.__extractResult";
        return frame->ExecuteJavaScript(script, "", 0);
    }
    
private:
    CefRefPtr<CefBrowser> browser_;
    bool loading_complete_;
    bool load_success_;
    
    IMPLEMENT_REFCOUNTING(BrowserClient);
};

int main(int argc, char* argv[]) {
    CefMainArgs main_args(argc, argv);
    CefRefPtr<SimpleApp> app(new SimpleApp());
    
    CefSettings settings;
    settings.no_sandbox = true;
    settings.windowless_rendering_enabled = true;
    settings.multi_threaded_message_loop = true;
    settings.log_severity = LOGSEVERITY_ERROR;
    
    if (!CefInitialize(main_args, settings, app, nullptr)) {
        LOG("CEF初始化失败");
        return -1;
    }
    
    CefWindowInfo window_info;
    window_info.SetAsWindowless(0);
    window_info.width = 1024;
    window_info.height = 768;
    
    CefBrowserSettings browser_settings;
    browser_settings.javascript = STATE_ENABLED;
    browser_settings.image_loading = STATE_ENABLED;
    browser_settings.webgl = STATE_DISABLED;
    browser_settings.plugins = STATE_DISABLED;
    
    CefRefPtr<BrowserClient> client(new BrowserClient());
    CefBrowserHost::CreateBrowserSync(
        window_info, client, 
        "https://item.taobao.com/xxx.html", 
        browser_settings, nullptr, nullptr);
    
    // 等待加载完成
    if (client->WaitForLoad(15)) {
        LOG("页面加载成功,开始提取");
        // 提取结果已通过JS自动完成
    } else {
        LOG("页面加载失败");
    }
    
    CefRunMessageLoop();
    CefShutdown();
    
    return 0;
}

10.2 剪贴板监听实现

cpp

复制代码
// clipboard_monitor.h
#ifndef CLIPBOARD_MONITOR_H
#define CLIPBOARD_MONITOR_H

#include <windows.h>
#include <string>
#include <functional>
#include <thread>
#include <atomic>
#include <chrono>

class ClipboardMonitor {
public:
    ClipboardMonitor() : running_(false), hwnd_(nullptr), next_viewer_(nullptr) {}
    
    bool Initialize(HWND hwnd) {
        hwnd_ = hwnd;
        next_viewer_ = SetClipboardViewer(hwnd);
        if (next_viewer_) {
            running_ = true;
            LOG("剪贴板监听已启动");
            return true;
        }
        LOG("剪贴板监听启动失败");
        return false;
    }
    
    void Shutdown() {
        running_ = false;
        if (hwnd_) {
            ChangeClipboardChain(hwnd_, next_viewer_);
            hwnd_ = nullptr;
        }
        LOG("剪贴板监听已关闭");
    }
    
    void SetCallback(std::function<void(const std::string&)> callback) {
        callback_ = callback;
    }
    
    void StartPolling(int interval_ms = 500) {
        if (polling_thread_.joinable()) return;
        
        polling_thread_ = std::thread([this, interval_ms]() {
            std::string last_text;
            while (running_) {
                std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
                
                std::string current_text = GetClipboardText();
                if (current_text.empty() || current_text == last_text) continue;
                
                last_text = current_text;
                std::string url = DetectUrl(current_text);
                if (!url.empty() && callback_) {
                    callback_(url);
                }
            }
        });
    }
    
    void StopPolling() {
        running_ = false;
        if (polling_thread_.joinable()) {
            polling_thread_.join();
        }
    }
    
private:
    std::string GetClipboardText() {
        if (!OpenClipboard(hwnd_)) return "";
        
        HANDLE hData = GetClipboardData(CF_TEXT);
        if (!hData) {
            CloseClipboard();
            return "";
        }
        
        char* pszText = static_cast<char*>(GlobalLock(hData));
        if (!pszText) {
            CloseClipboard();
            return "";
        }
        
        std::string text(pszText);
        GlobalUnlock(hData);
        CloseClipboard();
        
        return text;
    }
    
    std::string DetectUrl(const std::string& text) {
        if (text.empty()) return "";
        
        const std::vector<std::string> patterns = {
            "taobao.com/item.htm",
            "tmall.com/item.htm",
            "jd.com/",
            ".jd.com/",
            "yangkeduo.com/goods.html",
            "douyin.com/product",
            "1688.com/offer",
            "detail.1688.com",
            "amazon.com/dp/",
            "amazon.com/product/"
        };
        
        for (const auto& pattern : patterns) {
            if (text.find(pattern) != std::string::npos) {
                // 提取完整的URL
                size_t start = text.find("http");
                if (start == std::string::npos) {
                    start = text.find("https");
                }
                if (start != std::string::npos) {
                    size_t end = text.find_first_of(" \t\n\r", start);
                    if (end == std::string::npos) {
                        return text.substr(start);
                    }
                    return text.substr(start, end - start);
                }
                return text;
            }
        }
        return "";
    }
    
    std::atomic<bool> running_;
    HWND hwnd_;
    HWND next_viewer_;
    std::function<void(const std::string&)> callback_;
    std::thread polling_thread_;
    
    void LOG(const std::string& msg) {}
};

#endif

十一、图片URL的完整处理链路

11.1 URL处理流程

javascript

复制代码
class ImageUrlProcessor {
    constructor() {
        this.urlCache = new Map();
        this.processedCount = 0;
        this.failedCount = 0;
        this.maxCacheSize = 1000;
    }
    
    process(url) {
        // 1. 验证URL
        if (!this.isValidUrl(url)) {
            this.failedCount++;
            return null;
        }
        
        // 2. 检查缓存
        const cacheKey = this.getCacheKey(url);
        if (this.urlCache.has(cacheKey)) {
            return this.urlCache.get(cacheKey);
        }
        
        // 3. 转换为原图
        let processed = this.convertToOriginal(url);
        
        // 4. 缓存结果
        if (this.urlCache.size >= this.maxCacheSize) {
            const firstKey = this.urlCache.keys().next().value;
            this.urlCache.delete(firstKey);
        }
        this.urlCache.set(cacheKey, processed);
        this.processedCount++;
        
        return processed;
    }
    
    isValidUrl(url) {
        if (!url) return false;
        if (url.startsWith('data:')) return false;
        if (url.includes('1x1') || url.includes('blank.gif')) return false;
        if (url.includes('placeholder') || url.includes('loading')) return false;
        if (!url.startsWith('http')) return false;
        
        // 检查是否为图片格式
        const imgExts = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'];
        const lowerUrl = url.toLowerCase();
        for (const ext of imgExts) {
            if (lowerUrl.includes(ext)) return true;
        }
        return false;
    }
    
    getCacheKey(url) {
        // 使用URL的hash作为缓存键
        let hash = 0;
        for (let i = 0; i < url.length; i++) {
            hash = ((hash << 5) - hash) + url.charCodeAt(i);
            hash = hash & hash;
        }
        return 'img_' + Math.abs(hash);
    }
    
    convertToOriginal(url) {
        let result = url.split('?')[0];
        
        // 淘宝/天猫:去除尺寸后缀
        result = result.replace(/_\d+x\d+\./g, '.');
        result = result.replace(/\.sum\./g, '.');
        
        // 京东:n1/n2 -> n0
        result = result.replace(/\/n\d\//, '/n0/');
        result = result.replace(/\/popWaterMark\//, '/');
        
        // 亚马逊:去除尺寸参数
        result = result.replace(/\._[A-Z]+_\d+_\./g, '.');
        result = result.replace(/\._SR\d+_\d+_\./g, '.');
        
        // 拼多多:webp转jpg
        result = result.replace(/\.webp$/i, '.jpg');
        
        // 通用:去除缩略图标识
        result = result.replace(/_thumb\./g, '.');
        result = result.replace(/\.small\./g, '.');
        result = result.replace(/\.medium\./g, '.');
        
        return result;
    }
    
    getStats() {
        return {
            processed: this.processedCount,
            failed: this.failedCount,
            cacheSize: this.urlCache.size,
            hitRate: this.urlCache.size > 0 ? 
                (this.processedCount - this.failedCount) / this.processedCount : 0
        };
    }
}

十二、SKU图智能分类的完整算法

12.1 SKU分类器完整实现

javascript

复制代码
class SkuClassifier {
    constructor() {
        this.containerSelectors = [
            '.tb-sku', '.J_sku',           // 淘宝/天猫
            '.sku-img-list', '.J_skuImgList', // 京东
            '.sku-list', '.J_skuList',       // 拼多多
            '.sku-list', '.attribute-list'   // 1688
        ];
        this.nameSelectors = [
            '.sku-name', '.J_skuName',
            '.tb-sku-name', '.attr-name',
            '.property-name', '.spec-name'
        ];
        this.itemSelectors = [
            '.sku-item', '.J_skuItem',
            '.sku-img-item', '.attribute-item',
            '.spec-item', '[data-value]'
        ];
        this.platformConfig = {
            'taobao': { container: '.tb-sku, .J_sku', item: '.sku-item, .J_skuItem', name: '.sku-name, .J_skuName' },
            'jd': { container: '.sku-img-list, .J_skuImgList', item: '.sku-img-item', name: 'title' },
            'pdd': { container: '.sku-list, .J_skuList', item: '.sku-item', name: '.sku-name' },
            '1688': { container: '.sku-list, .attribute-list', item: '.sku-item, .attribute-item', name: '.sku-name, .attr-name' }
        };
    }
    
    detectPlatform() {
        const host = location.hostname || '';
        if (host.includes('taobao.com') || host.includes('tmall.com')) return 'taobao';
        if (host.includes('jd.com')) return 'jd';
        if (host.includes('yangkeduo.com') || host.includes('pinduoduo.com')) return 'pdd';
        if (host.includes('1688.com')) return '1688';
        if (host.includes('amazon.com')) return 'amazon';
        return 'unknown';
    }
    
    extract(platform) {
        const config = this.platformConfig[platform] || {};
        const containerSelectors = config.container ? [config.container] : this.containerSelectors;
        
        const container = this.findContainer(containerSelectors);
        if (!container) return [];
        
        const itemSelectors = config.item ? [config.item] : this.itemSelectors;
        const items = this.findItems(container, itemSelectors);
        const results = [];
        const seenNames = new Set();
        const seenUrls = new Set();
        
        for (const item of items) {
            const name = this.extractName(item, config);
            const url = this.extractImage(item);
            
            if (!url || seenUrls.has(url)) continue;
            seenUrls.add(url);
            
            const normalizedName = this.normalizeName(name);
            if (seenNames.has(normalizedName)) continue;
            seenNames.add(normalizedName);
            
            results.push({
                name: normalizedName,
                url: this.convertToOriginal(url),
                rawName: name
            });
        }
        
        return results;
    }
    
    findContainer(selectors) {
        for (const selector of selectors) {
            try {
                const container = document.querySelector(selector);
                if (container && container.querySelectorAll('img').length > 0) {
                    return container;
                }
            } catch (e) {
                continue;
            }
        }
        return null;
    }
    
    findItems(container, selectors) {
        for (const selector of selectors) {
            try {
                const items = container.querySelectorAll(selector);
                if (items.length > 0) return items;
            } catch (e) {
                continue;
            }
        }
        return [];
    }
    
    extractName(item, config) {
        // 如果配置指定了使用title属性
        if (config && config.name === 'title') {
            const title = item.getAttribute('title');
            if (title) return title;
        }
        
        // 从名称元素提取
        const nameSelectors = config && config.name !== 'title' ? [config.name] : this.nameSelectors;
        for (const selector of nameSelectors) {
            try {
                const el = item.querySelector(selector);
                if (el) {
                    const name = el.textContent?.trim();
                    if (name && name.length > 0 && name.length < 30) {
                        return name;
                    }
                }
            } catch (e) {
                continue;
            }
        }
        
        // 从data属性提取
        const dataAttrs = ['data-value', 'data-title', 'data-name', 'data-label', 'data-sku-name'];
        for (const attr of dataAttrs) {
            const value = item.getAttribute(attr);
            if (value && value.length < 30) {
                return value;
            }
        }
        
        // 从title属性提取
        const title = item.getAttribute('title');
        if (title && title.length < 30) {
            return title;
        }
        
        // 从内部文本提取
        const text = item.textContent?.trim();
        if (text && text.length > 0 && text.length < 20) {
            return text;
        }
        
        return '规格';
    }
    
    normalizeName(name) {
        if (!name) return '规格';
        name = name.trim();
        name = name.replace(/\s+/g, ' ');
        if (name.length > 30) {
            name = name.substring(0, 30);
        }
        name = name.replace(/[\\/*?:"<>|]/g, '_');
        return name;
    }
    
    extractImage(item) {
        const img = item.querySelector('img');
        if (!img) return null;
        return img.src || img.getAttribute('data-src') || img.getAttribute('data-original');
    }
    
    convertToOriginal(url) {
        if (!url) return null;
        url = url.split('?')[0];
        url = url.replace(/_\d+x\d+\./g, '.');
        url = url.replace(/\.sum\./g, '.');
        url = url.replace(/\.webp$/i, '.jpg');
        return url;
    }
}

十三、懒加载的完整处理方案

13.1 懒加载检测与触发

javascript

复制代码
class LazyLoadHandler {
    constructor() {
        this.lazyAttributes = ['data-src', 'data-original', 'data-lazy-src', 'data-lazy-img'];
        this.loadedImages = new Set();
        this.pendingImages = new Set();
        this.maxWaitSeconds = 15;
    }
    
    detect() {
        const lazyImages = [];
        const allImages = document.querySelectorAll('img');
        
        for (const img of allImages) {
            let isLazy = false;
            let realUrl = null;
            let attrName = '';
            
            for (const attr of this.lazyAttributes) {
                const url = img.getAttribute(attr);
                if (url) {
                    isLazy = true;
                    realUrl = url;
                    attrName = attr;
                    break;
                }
            }
            
            if (isLazy && realUrl) {
                lazyImages.push({
                    element: img,
                    url: realUrl,
                    currentSrc: img.src || '',
                    attrName: attrName
                });
                this.pendingImages.add(realUrl);
            }
        }
        
        return lazyImages;
    }
    
    async triggerAll() {
        const lazyImages = this.detect();
        console.log(`发现 ${lazyImages.length} 个懒加载图片`);
        
        if (lazyImages.length === 0) {
            console.log('未发现懒加载图片');
            return;
        }
        
        // 方法1:滚动触发
        await this.scrollTrigger();
        
        // 方法2:直接设置src(兜底)
        for (const item of lazyImages) {
            if (!this.isLoaded(item.element)) {
                try {
                    item.element.src = item.url;
                    item.element.removeAttribute(item.attrName);
                    this.pendingImages.delete(item.url);
                    this.loadedImages.add(item.url);
                } catch (e) {
                    // 忽略错误
                }
            }
        }
        
        // 方法3:等待加载完成
        await this.waitForComplete();
        
        console.log(`懒加载完成,已加载: ${this.loadedImages.size}`);
    }
    
    async scrollTrigger() {
        // 滚动到底部
        window.scrollTo(0, document.body.scrollHeight);
        await this.sleep(500);
        
        // 分段滚动
        const steps = 10;
        for (let i = 1; i <= steps; i++) {
            const scrollTo = (document.body.scrollHeight / steps) * i;
            window.scrollTo(0, scrollTo);
            await this.sleep(250);
        }
        
        // 滚动回顶部
        window.scrollTo(0, 0);
        await this.sleep(300);
        
        // 再次滚动到底部(确保所有图片都触发)
        window.scrollTo(0, document.body.scrollHeight);
        await this.sleep(500);
        window.scrollTo(0, 0);
        await this.sleep(300);
    }
    
    isLoaded(img) {
        try {
            return img.complete && img.naturalWidth > 0 && img.naturalHeight > 0;
        } catch (e) {
            return false;
        }
    }
    
    async waitForComplete() {
        let maxWait = this.maxWaitSeconds * 2;
        let lastCount = 0;
        let stableCount = 0;
        
        while (maxWait-- > 0 && stableCount < 3) {
            const currentCount = this.pendingImages.size;
            if (currentCount === lastCount) {
                stableCount++;
            } else {
                stableCount = 0;
                lastCount = currentCount;
            }
            await this.sleep(500);
        }
        
        console.log(`懒加载等待完成,剩余待加载: ${this.pendingImages.size}`);
    }
    
    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}

十四、视频下载的完整技术实现

14.1 m3u8解析器

javascript

复制代码
class M3U8Parser {
    parse(content, baseUrl) {
        const lines = content.split('\n');
        const segments = [];
        let currentDuration = 0;
        let isDiscontinuity = false;
        
        for (let i = 0; i < lines.length; i++) {
            const line = lines[i].trim();
            
            if (!line) continue;
            
            if (line.startsWith('#')) {
                if (line.startsWith('#EXTINF:')) {
                    currentDuration = parseFloat(line.substring(8)) || 5.0;
                } else if (line.startsWith('#EXT-X-DISCONTINUITY')) {
                    isDiscontinuity = true;
                }
                continue;
            }
            
            // 片段URL
            let segmentUrl = line;
            if (!segmentUrl.startsWith('http')) {
                segmentUrl = this.resolveUrl(baseUrl, segmentUrl);
            }
            
            segments.push({
                url: segmentUrl,
                duration: currentDuration,
                discontinuity: isDiscontinuity
            });
            
            currentDuration = 0;
            isDiscontinuity = false;
        }
        
        return segments;
    }
    
    resolveUrl(base, relative) {
        if (relative.startsWith('http')) return relative;
        if (relative.startsWith('/')) {
            const urlObj = new URL(base);
            return `${urlObj.protocol}//${urlObj.host}${relative}`;
        }
        const basePath = base.substring(0, base.lastIndexOf('/') + 1);
        return basePath + relative;
    }
}

14.2 ts下载器

javascript

复制代码
class TsDownloader {
    constructor(maxConcurrent = 10) {
        this.maxConcurrent = maxConcurrent;
        this.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            'Referer': 'https://item.taobao.com/'
        };
        this.timeout = 30000;
        this.retryCount = 3;
    }
    
    async downloadAll(segments, onProgress) {
        const results = new Array(segments.length);
        const total = segments.length;
        let completed = 0;
        
        if (total === 0) {
            return results;
        }
        
        const queue = [...segments];
        const workers = [];
        const workerCount = Math.min(this.maxConcurrent, total);
        
        for (let i = 0; i < workerCount; i++) {
            workers.push(this.worker(queue, results, onProgress, total, completed));
        }
        
        await Promise.all(workers);
        return results;
    }
    
    async worker(queue, results, onProgress, total, completedRef) {
        while (queue.length > 0) {
            const index = total - queue.length;
            const segment = queue.shift();
            
            let success = false;
            let data = null;
            let error = null;
            
            for (let attempt = 0; attempt < this.retryCount; attempt++) {
                try {
                    data = await this.downloadSingle(segment.url);
                    success = true;
                    break;
                } catch (e) {
                    error = e;
                    if (attempt < this.retryCount - 1) {
                        await this.sleep(1000 * Math.pow(2, attempt));
                    }
                }
            }
            
            results[index] = {
                success: success,
                data: data,
                error: error ? error.message : null,
                index: index
            };
            
            completedRef++;
            if (onProgress) {
                onProgress(completedRef, total);
            }
        }
    }
    
    async downloadSingle(url) {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), this.timeout);
        
        try {
            const response = await fetch(url, {
                headers: this.headers,
                signal: controller.signal
            });
            clearTimeout(timeoutId);
            
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }
            
            return await response.arrayBuffer();
        } catch (e) {
            clearTimeout(timeoutId);
            throw e;
        }
    }
    
    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}

14.3 视频合并器

javascript

复制代码
class VideoMerger {
    async merge(segmentsData, outputPath) {
        const validSegments = segmentsData.filter(s => s.success && s.data);
        
        if (validSegments.length === 0) {
            throw new Error('没有可用的ts片段,合并失败');
        }
        
        const totalSize = validSegments.reduce((sum, s) => sum + s.data.byteLength, 0);
        console.log(`合并 ${validSegments.length} 个片段,总大小: ${(totalSize / 1024 / 1024).toFixed(2)} MB`);
        
        const blob = new Blob(validSegments.map(s => s.data), { type: 'video/mp4' });
        const url = URL.createObjectURL(blob);
        
        // 触发下载
        const a = document.createElement('a');
        a.href = url;
        a.download = outputPath;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        
        // 释放内存
        URL.revokeObjectURL(url);
        
        return {
            size: blob.size,
            segmentCount: validSegments.length,
            totalSegments: segmentsData.length
        };
    }
}

十五、批量下载与任务队列设计

15.1 任务队列完整实现

javascript

复制代码
class TaskQueue {
    constructor(concurrency = 5) {
        this.concurrency = concurrency;
        this.queue = [];
        this.running = 0;
        this.results = [];
        this.completed = 0;
        this.failed = [];
        this.total = 0;
        this.onProgress = null;
        this.onComplete = null;
        this.onError = null;
        this.paused = false;
        this.maxRetries = 3;
    }
    
    add(task) {
        this.queue.push({
            ...task,
            retries: 0,
            maxRetries: this.maxRetries,
            addedAt: Date.now()
        });
        this.total++;
        this.process();
    }
    
    addAll(tasks) {
        for (const task of tasks) {
            this.queue.push({
                ...task,
                retries: 0,
                maxRetries: this.maxRetries,
                addedAt: Date.now()
            });
            this.total++;
        }
        this.process();
    }
    
    pause() {
        this.paused = true;
        console.log('任务队列已暂停');
    }
    
    resume() {
        this.paused = false;
        this.process();
        console.log('任务队列已恢复');
    }
    
    async process() {
        if (this.paused) return;
        if (this.running >= this.concurrency || this.queue.length === 0) {
            if (this.queue.length === 0 && this.running === 0) {
                this.onComplete && this.onComplete({
                    completed: this.completed,
                    failed: this.failed,
                    total: this.total,
                    results: this.results
                });
            }
            return;
        }
        
        this.running++;
        const task = this.queue.shift();
        
        try {
            const result = await this.executeTask(task);
            this.results.push({ success: true, ...result, taskId: task.id || Date.now() });
            this.completed++;
        } catch (error) {
            this.results.push({ success: false, task, error: error.message });
            this.failed.push(task);
            this.onError && this.onError(error, task);
        }
        
        this.running--;
        this.onProgress && this.onProgress(this.completed, this.total);
        
        // 继续处理下一个任务
        this.process();
    }
    
    async executeTask(task) {
        let lastError;
        for (let attempt = 0; attempt < task.maxRetries; attempt++) {
            try {
                return await this.download(task.url, task.path);
            } catch (error) {
                lastError = error;
                console.warn(`重试 ${attempt + 1}/${task.maxRetries}: ${task.url.substring(0, 50)}...`);
                if (attempt < task.maxRetries - 1) {
                    await this.sleep(1000 * Math.pow(2, attempt));
                }
            }
        }
        throw lastError;
    }
    
    async download(url, path) {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), 30000);
        
        try {
            const response = await fetch(url, { signal: controller.signal });
            clearTimeout(timeoutId);
            
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }
            
            const blob = await response.blob();
            return { url, path, size: blob.size };
        } catch (e) {
            clearTimeout(timeoutId);
            throw e;
        }
    }
    
    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    
    getStats() {
        return {
            total: this.total,
            completed: this.completed,
            failed: this.failed.length,
            pending: this.queue.length,
            running: this.running,
            concurrency: this.concurrency
        };
    }
}

十六、剪贴板监听与自动化流程

16.1 完整监听实现

javascript

复制代码
class ClipboardManager {
    constructor() {
        this.lastText = '';
        this.isProcessing = false;
        this.onUrlDetected = null;
        this.onError = null;
        this.intervalId = null;
        this.isRunning = false;
        this.debounceMs = 1000;
        this.supportedDomains = [
            'taobao.com', 'tmall.com', 'jd.com', 
            'yangkeduo.com', 'douyin.com', 
            '1688.com', 'amazon.com'
        ];
    }
    
    start() {
        if (this.isRunning) return;
        this.isRunning = true;
        this.intervalId = setInterval(() => this.check(), 500);
        console.log('剪贴板监听已启动');
    }
    
    stop() {
        if (this.intervalId) {
            clearInterval(this.intervalId);
            this.intervalId = null;
        }
        this.isRunning = false;
        console.log('剪贴板监听已停止');
    }
    
    async check() {
        if (this.isProcessing) return;
        
        try {
            const currentText = await this.getClipboardText();
            if (currentText === this.lastText) return;
            
            this.lastText = currentText;
            const url = this.detectUrl(currentText);
            
            if (url && this.onUrlDetected) {
                this.isProcessing = true;
                this.onUrlDetected(url);
                setTimeout(() => { this.isProcessing = false; }, this.debounceMs);
            }
        } catch (error) {
            this.onError && this.onError(error);
        }
    }
    
    async getClipboardText() {
        // 使用navigator.clipboard API (需要用户交互)
        if (navigator.clipboard && navigator.clipboard.readText) {
            try {
                return await navigator.clipboard.readText();
            } catch (e) {
                // 降级到传统方法
            }
        }
        
        // 传统方法
        return new Promise((resolve) => {
            const textarea = document.createElement('textarea');
            textarea.style.position = 'fixed';
            textarea.style.opacity = '0';
            textarea.style.left = '-9999px';
            textarea.style.top = '-9999px';
            document.body.appendChild(textarea);
            textarea.focus();
            
            try {
                document.execCommand('paste');
                const text = textarea.value;
                document.body.removeChild(textarea);
                resolve(text);
            } catch (e) {
                document.body.removeChild(textarea);
                resolve('');
            }
        });
    }
    
    detectUrl(text) {
        if (!text || typeof text !== 'string') return null;
        
        // 提取URL
        const urlMatch = text.match(/https?:\/\/[^\s<>"']+/g);
        if (!urlMatch) return null;
        
        for (const url of urlMatch) {
            const lowerUrl = url.toLowerCase();
            for (const domain of this.supportedDomains) {
                if (lowerUrl.includes(domain)) {
                    return url;
                }
            }
        }
        
        return null;
    }
}

十七、错误处理与重试机制

17.1 错误分类器

javascript

复制代码
class ErrorClassifier {
    static classify(error) {
        const message = error.message || '';
        const code = error.code || '';
        
        // 网络错误
        if (message.includes('timeout') || message.includes('Timeout')) return 'TIMEOUT';
        if (message.includes('network') || message.includes('Network')) return 'NETWORK';
        if (message.includes('ECONNRESET') || message.includes('ECONNREFUSED')) return 'CONNECTION';
        if (message.includes('fetch') && message.includes('failed')) return 'FETCH';
        
        // HTTP错误
        if (message.includes('403')) return 'FORBIDDEN';
        if (message.includes('404')) return 'NOT_FOUND';
        if (message.includes('429')) return 'RATE_LIMIT';
        if (message.includes('500') || message.includes('502') || message.includes('503')) return 'SERVER_ERROR';
        
        // 解析错误
        if (message.includes('parse') || message.includes('json')) return 'PARSE_ERROR';
        if (message.includes('DOM')) return 'DOM_ERROR';
        
        // 其他
        if (message.includes('abort') || message.includes('Abort')) return 'ABORTED';
        
        return 'UNKNOWN';
    }
    
    static isRetryable(error) {
        const type = this.classify(error);
        return ['TIMEOUT', 'NETWORK', 'CONNECTION', 'FETCH', 'SERVER_ERROR', 'RATE_LIMIT'].includes(type);
    }
    
    static getRetryDelay(error, attempt) {
        const type = this.classify(error);
        const baseDelay = 1000;
        const maxDelay = 30000;
        
        const multipliers = {
            'TIMEOUT': 2,
            'NETWORK': 1.5,
            'CONNECTION': 1.5,
            'FETCH': 1,
            'SERVER_ERROR': 3,
            'RATE_LIMIT': 5,
            'UNKNOWN': 2
        };
        
        let delay = baseDelay * Math.pow(multipliers[type] || 2, attempt);
        delay = Math.min(delay, maxDelay);
        
        // 添加抖动
        delay = delay * (0.8 + Math.random() * 0.4);
        
        return delay;
    }
    
    static getFriendlyMessage(error) {
        const type = this.classify(error);
        const messages = {
            'TIMEOUT': '请求超时,请检查网络连接后重试',
            'NETWORK': '网络连接异常,请检查网络后重试',
            'CONNECTION': '连接失败,请检查网络后重试',
            'FETCH': '资源获取失败,请稍后重试',
            'FORBIDDEN': '访问被拒绝,请检查权限设置',
            'NOT_FOUND': '资源不存在,请检查链接是否正确',
            'RATE_LIMIT': '请求频率过高,请稍后重试',
            'SERVER_ERROR': '服务器错误,请稍后重试',
            'PARSE_ERROR': '数据解析失败,请检查页面结构',
            'DOM_ERROR': '页面结构异常,请检查页面是否完整加载',
            'ABORTED': '请求被取消',
            'UNKNOWN': '发生未知错误,请重试'
        };
        return messages[type] || error.message;
    }
}

17.2 重试管理器

javascript

复制代码
class RetryManager {
    constructor(options = {}) {
        this.maxRetries = options.maxRetries || 5;
        this.initialDelay = options.initialDelay || 1000;
        this.maxDelay = options.maxDelay || 30000;
        this.factor = options.factor || 2;
        this.jitter = options.jitter !== false;
        this.onRetry = options.onRetry || null;
        this.onFailure = options.onFailure || null;
    }
    
    async execute(fn, context = '') {
        let lastError;
        let attempt = 0;
        let delay = this.initialDelay;
        
        while (attempt < this.maxRetries) {
            try {
                return await fn();
            } catch (error) {
                lastError = error;
                attempt++;
                
                const isRetryable = ErrorClassifier.isRetryable(error);
                const friendlyMessage = ErrorClassifier.getFriendlyMessage(error);
                
                console.warn(`[${context}] 尝试 ${attempt}/${this.maxRetries} 失败: ${friendlyMessage}`);
                
                if (this.onRetry) {
                    this.onRetry({
                        attempt,
                        maxRetries: this.maxRetries,
                        error: error,
                        friendlyMessage: friendlyMessage,
                        context: context
                    });
                }
                
                if (!isRetryable || attempt >= this.maxRetries) {
                    break;
                }
                
                const waitTime = this.getWaitTime(error, attempt, delay);
                console.log(`[${context}] 等待 ${(waitTime / 1000).toFixed(1)} 秒后重试...`);
                await this.sleep(waitTime);
                delay = Math.min(delay * this.factor, this.maxDelay);
            }
        }
        
        if (this.onFailure) {
            this.onFailure({
                error: lastError,
                context: context,
                attempts: attempt
            });
        }
        
        throw lastError;
    }
    
    getWaitTime(error, attempt, delay) {
        let waitTime = ErrorClassifier.getRetryDelay(error, attempt);
        if (this.jitter) {
            waitTime = waitTime * (0.5 + Math.random() * 0.5);
        }
        return Math.min(waitTime, this.maxDelay);
    }
    
    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}

十八、性能优化策略

18.1 内存管理

javascript

复制代码
class MemoryManager {
    constructor() {
        this.maxMemoryMB = 500;
        this.warningThreshold = 0.8;
        this.criticalThreshold = 0.95;
        this.cache = new Map();
        this.cacheLimit = 200;
        this.imageCache = new Map();
        this.imageCacheLimit = 50;
        this.memoryCheckInterval = null;
        this.isRunning = false;
    }
    
    startMonitoring(intervalMs = 5000) {
        if (this.isRunning) return;
        this.isRunning = true;
        this.memoryCheckInterval = setInterval(() => this.check(), intervalMs);
        console.log('内存监控已启动');
    }
    
    stopMonitoring() {
        if (this.memoryCheckInterval) {
            clearInterval(this.memoryCheckInterval);
            this.memoryCheckInterval = null;
        }
        this.isRunning = false;
        console.log('内存监控已停止');
    }
    
    check() {
        const usage = this.getMemoryUsage();
        if (usage === null) return;
        
        const ratio = usage / this.maxMemoryMB;
        
        if (ratio >= this.criticalThreshold) {
            console.warn(`内存使用率过高: ${usage.toFixed(0)}MB / ${this.maxMemoryMB}MB (${(ratio * 100).toFixed(0)}%)`);
            this.emergencyRelease();
        } else if (ratio >= this.warningThreshold) {
            console.log(`内存使用率较高: ${usage.toFixed(0)}MB / ${this.maxMemoryMB}MB (${(ratio * 100).toFixed(0)}%)`);
            this.releaseMemory();
        }
    }
    
    getMemoryUsage() {
        if (window.performance && window.performance.memory) {
            return window.performance.memory.usedJSHeapSize / (1024 * 1024);
        }
        return null;
    }
    
    releaseMemory() {
        // 清理缓存
        this.cache.clear();
        this.imageCache.clear();
        
        // 清理DOM引用
        if (window.gc) {
            window.gc();
        }
        
        console.log('内存已释放');
    }
    
    emergencyRelease() {
        this.cache.clear();
        this.imageCache.clear();
        this.cacheLimit = Math.floor(this.cacheLimit * 0.5);
        this.imageCacheLimit = Math.floor(this.imageCacheLimit * 0.5);
        
        if (window.gc) {
            window.gc();
        }
        
        console.log('紧急内存释放完成');
    }
    
    addToCache(key, value, cacheType = 'default') {
        const targetCache = cacheType === 'image' ? this.imageCache : this.cache;
        const limit = cacheType === 'image' ? this.imageCacheLimit : this.cacheLimit;
        
        if (targetCache.size >= limit) {
            const firstKey = targetCache.keys().next().value;
            targetCache.delete(firstKey);
        }
        targetCache.set(key, value);
    }
    
    getFromCache(key, cacheType = 'default') {
        const targetCache = cacheType === 'image' ? this.imageCache : this.cache;
        const value = targetCache.get(key);
        if (value) {
            // LRU更新
            targetCache.delete(key);
            targetCache.set(key, value);
        }
        return value;
    }
}

18.2 并发控制优化

并发数 适用场景 优点 缺点
1 网络不稳定 最稳定 速度慢
3-5 日常采集 平衡速度和稳定性 可能触发限流
10+ 快速采集 速度快 可能被封IP

十九、各平台差异适配

19.1 平台适配器

javascript

复制代码
class PlatformAdapter {
    constructor() {
        this.platforms = {
            taobao: {
                name: '淘宝',
                mainSelector: '.J_UlThumb, .tb-thumb',
                skuSelector: '.tb-sku, .J_sku',
                detailSelector: '#description, .desc',
                videoSelector: '#J_ItemVideo video, .tb-video video',
                urlConverter: this.convertTaobaoUrl,
                nameExtractor: '.sku-name, .J_skuName'
            },
            jd: {
                name: '京东',
                mainSelector: '.spec-img',
                skuSelector: '.sku-img-list, .J_skuImgList',
                detailSelector: '#detail, .detail-content',
                videoSelector: '.JDV-video video, .video-box video',
                urlConverter: this.convertJdUrl,
                nameExtractor: 'title'
            },
            pdd: {
                name: '拼多多',
                mainSelector: '.main-image',
                skuSelector: '.sku-list, .J_skuList',
                detailSelector: '.detail-content, .J_detail',
                videoSelector: '.video-container video',
                urlConverter: this.convertPddUrl,
                nameExtractor: '.sku-name'
            }
        };
    }
    
    detect() {
        const host = location.hostname || '';
        if (host.includes('taobao.com') || host.includes('tmall.com')) return this.platforms.taobao;
        if (host.includes('jd.com')) return this.platforms.jd;
        if (host.includes('yangkeduo.com') || host.includes('pinduoduo.com')) return this.platforms.pdd;
        return null;
    }
    
    convertTaobaoUrl(url) {
        if (!url) return null;
        url = url.split('?')[0];
        url = url.replace(/_\d+x\d+\./g, '.');
        url = url.replace(/\.sum\./g, '.');
        return url;
    }
    
    convertJdUrl(url) {
        if (!url) return null;
        url = url.split('?')[0];
        url = url.replace(/\/n\d\//, '/n0/');
        url = url.replace(/\/popWaterMark\//, '/');
        return url;
    }
    
    convertPddUrl(url) {
        if (!url) return null;
        url = url.split('?')[0];
        url = url.replace(/_\d+x\d+\./g, '.');
        url = url.replace(/\.webp$/i, '.jpg');
        return url;
    }
}

19.2 各平台差异总结

平台 主图容器 SKU容器 详情容器 视频格式 特殊处理
淘宝 .J_UlThumb .tb-sku #description mp4/m3u8 尺寸后缀去除
京东 .spec-img .sku-img-list #detail mp4/m3u8 n1→n0转换
拼多多 .main-image .sku-list .detail-content mp4 webp转jpg
1688 .main-image .sku-list .detail-content 不支持 需登录

二十、火蚁一键存图的完整技术架构

火蚁一键存图正是基于浏览器方案开发的。它基于Chromium内核,不是爬虫,不会因为平台改版而失效。

20.1 核心流程

text

复制代码
用户复制链接
    ↓
剪贴板监听触发
    ↓
Chromium内核加载页面
    ↓
等待DOM就绪 + 网络空闲 + jQuery加载
    ↓
触发懒加载
    ↓
提取主图URL
    ↓
提取SKU图URL + 属性名称
    ↓
提取详情图URL
    ↓
提取视频URL
    ↓
原图URL转换
    ↓
自动分类归档

20.2 支持的平台

平台 图片 SKU图 详情图 视频 特殊处理
淘宝 原图获取
天猫 原图获取
京东 m3u8视频合并
拼多多 webp转jpg
1688 需登录
抖音 ⚠️ JS动态渲染
亚马逊 ⚠️ 变体图分类

二十一、最终总结

21.1 三条技术路线综合对比

技术路线 稳定性 维护成本 适用范围 成功率 推荐指数
爬虫方案 ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐ 70-80% ⭐⭐
浏览器插件 ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ 85-90% ⭐⭐⭐
浏览器方案 ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐ 99%+ ⭐⭐⭐⭐⭐

21.2 核心要点回顾

  1. 爬虫方案:TLS指纹检测、强依赖DOM结构、无法执行JS,已不适合2026年的电商采集场景

  2. 浏览器插件:依赖Chrome版本、权限过大、Manifest V3限制,未来发展受限

  3. 浏览器方案:独立运行、真实浏览器指纹、不改版影响,是最稳定的选择

21.3 选型建议

使用场景 推荐方案 理由
技术学习、研究 爬虫方案 练手好项目
偶尔采集(每周<10次) 浏览器插件 安装方便
日常高频采集(每天>20次) 浏览器方案 稳定、成功率99%+
需要抖音/亚马逊等平台 浏览器方案 插件大多不支持
需要SKU图自动分类 浏览器方案 自动识别+命名

浏览器方案是架构层面最稳健的选择。它不需要模拟浏览器------因为它自己就是浏览器。

火蚁一键存图正是基于浏览器方案开发的,支持淘宝、天猫、京东、拼多多、1688、抖音、亚马逊等主流电商平台,一次下载即可获取主图、SKU图、详情图和主图视频,全部自动分类归档。

百度搜索"火蚁一键存图"即可找到。

免责声明:本文内容仅供技术交流和学习参考。电商平台的数据采集行为可能涉及平台服务条款、著作权法等法律问题。请确保遵守目标网站的《用户协议》和相关法律法规。因不当使用引发的法律风险由使用者自行承担。