图生图 (Image-to-Image) :Python 和 C++ 两种实现方式

图生图:Python 和 C++ 两种实现方式


一、完整代码

1.1 Python 版本

python 复制代码
"""
图生图 (Image-to-Image) Demo
使用火山引擎 Ark API / 豆包 Seedream 4.5 模型
"""

import os
import argparse
import base64
import urllib.request
from openai import OpenAI

MODEL = "doubao-seedream-4-5-251128"

client = OpenAI(
    base_url="https://ark.cn-beijing.volces.com/api/v3",
    api_key="6e69285a-1461-459e-954e-a492db0d8d3b",
)


def img2img(image_path: str, prompt: str, size: str = "2K", watermark: bool = True):
    """本地图片 Base64 编码后传入图生图接口"""
    with open(image_path, "rb") as f:
        image_b64 = base64.b64encode(f.read()).decode("utf-8")

    ext = os.path.splitext(image_path)[1].lower()
    mime_map = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp"}
    mime = mime_map.get(ext, "image/png")

    response = client.images.generate(
        model=MODEL,
        prompt=prompt,
        size=size,
        response_format="url",
        extra_body={
            "image": f"data:{mime};base64,{image_b64}",
            "watermark": watermark,
        },
    )
    return response.data[0].url


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="图生图 Demo - 豆包 Seedream 4.5")
    parser.add_argument("-i", "--input", required=True, help="输入图片路径")
    parser.add_argument("-o", "--output", default="output.png", help="输出图片路径 (默认: output.png)")
    parser.add_argument("-p", "--prompt", required=True, help="提示词")
    parser.add_argument("-s", "--size", default="2K", help="输出尺寸 (默认: 2K)")
    parser.add_argument("--no-watermark", action="store_true", help="去除水印")
    args = parser.parse_args()

    url = img2img(
        image_path=args.input,
        prompt=args.prompt,
        size=args.size,
        watermark=not args.no_watermark,
    )
    print(f"下载中: {url}")
    urllib.request.urlretrieve(url, args.output)
    print(f"已保存到: {args.output}")

用法:

bash 复制代码
python img2img_demo.py -i ./test.png -p "转化为油画风格"
python img2img_demo.py -i ./test.png -p "赛博朋克" -o cyber.png --no-watermark

1.2 C++ 版本

cpp 复制代码
/**
 * 图生图 (Image-to-Image) Demo - C++ 版本
 * 使用火山引擎 Ark API / 豆包 Seedream 4.5 模型
 *
 * 编译: cmake -B build && cmake --build build
 * 使用: ./img2img -i input.png -p "提示词" -o output.png
 */

#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include <cstring>
#include <curl/curl.h>
#include <nlohmann/json.hpp>

using json = nlohmann::json;

const std::string MODEL = "doubao-seedream-4-5-251128";
const std::string BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";
const std::string API_KEY = "6e69285a-1461-459e-954e-a492db0d8d3b";

// ============================================================
// Base64 编码
// ============================================================
const std::string BASE64_CHARS =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

std::string base64_encode(const unsigned char* data, size_t len) {
    std::string result;
    result.reserve(4 * ((len + 2) / 3));

    for (size_t i = 0; i < len; i += 3) {
        unsigned int n = (unsigned int)data[i] << 16;
        if (i + 1 < len) n |= (unsigned int)data[i + 1] << 8;
        if (i + 2 < len) n |= (unsigned int)data[i + 2];

        result.push_back(BASE64_CHARS[(n >> 18) & 0x3F]);
        result.push_back(BASE64_CHARS[(n >> 12) & 0x3F]);
        result.push_back((i + 1 < len) ? BASE64_CHARS[(n >> 6) & 0x3F] : '=');
        result.push_back((i + 2 < len) ? BASE64_CHARS[n & 0x3F] : '=');
    }
    return result;
}

// ============================================================
// 读取文件为字节数组
// ============================================================
std::vector<unsigned char> read_file(const std::string& path) {
    std::ifstream file(path, std::ios::binary);
    if (!file) {
        std::cerr << "无法打开文件: " << path << std::endl;
        exit(1);
    }
    return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
}

// ============================================================
// 根据扩展名推断 MIME 类型
// ============================================================
std::string get_mime(const std::string& path) {
    auto dot = path.find_last_of('.');
    if (dot == std::string::npos) return "image/png";
    std::string ext = path.substr(dot);
    if (ext == ".jpg" || ext == ".jpeg") return "image/jpeg";
    if (ext == ".webp") return "image/webp";
    return "image/png";
}

// ============================================================
// libcurl 回调:将响应写入 string
// ============================================================
static size_t write_callback(void* contents, size_t size, size_t nmemb, void* userp) {
    ((std::string*)userp)->append((char*)contents, size * nmemb);
    return size * nmemb;
}

// ============================================================
// 调用图生图 API,返回结果图片 URL
// ============================================================
std::string img2img(const std::string& image_path,
                    const std::string& prompt,
                    const std::string& size = "2K",
                    bool watermark = true) {
    // 1. 读取图片并 Base64 编码
    auto raw = read_file(image_path);
    std::string b64 = base64_encode(raw.data(), raw.size());
    std::string mime = get_mime(image_path);
    std::string data_uri = "data:" + mime + ";base64," + b64;

    // 2. 构造 JSON 请求体
    json body;
    body["model"] = MODEL;
    body["prompt"] = prompt;
    body["size"] = size;
    body["response_format"] = "url";
    body["image"] = data_uri;
    body["watermark"] = watermark;

    std::string body_str = body.dump();

    // 3. 发送 HTTP POST
    CURL* curl = curl_easy_init();
    if (!curl) {
        std::cerr << "curl 初始化失败" << std::endl;
        exit(1);
    }

    std::string response_data;
    struct curl_slist* headers = nullptr;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = curl_slist_append(headers, ("Authorization: Bearer " + API_KEY).c_str());

    curl_easy_setopt(curl, CURLOPT_URL, (BASE_URL + "/images/generations").c_str());
    curl_easy_setopt(curl, CURLOPT_POST, 1L);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body_str.c_str());
    curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body_str.size());
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_data);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 120L);

    CURLcode res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        std::cerr << "请求失败: " << curl_easy_strerror(res) << std::endl;
        exit(1);
    }

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (http_code != 200) {
        std::cerr << "HTTP " << http_code << ": " << response_data << std::endl;
        exit(1);
    }

    // 4. 解析返回的图片 URL
    auto resp = json::parse(response_data);
    return resp["data"][0]["url"];
}

// ============================================================
// 下载图片到本地
// ============================================================
void download(const std::string& url, const std::string& output_path) {
    CURL* curl = curl_easy_init();
    if (!curl) {
        std::cerr << "curl 初始化失败" << std::endl;
        exit(1);
    }

    std::ofstream out(output_path, std::ios::binary);
    if (!out) {
        std::cerr << "无法创建文件: " << output_path << std::endl;
        exit(1);
    }

    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 120L);

    auto file_callback = [](void* ptr, size_t size, size_t nmemb, void* stream) -> size_t {
        auto* ofs = (std::ofstream*)stream;
        ofs->write((char*)ptr, size * nmemb);
        return size * nmemb;
    };
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, +file_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &out);

    CURLcode res = curl_easy_perform(curl);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        std::cerr << "下载失败: " << curl_easy_strerror(res) << std::endl;
        exit(1);
    }
}

// ============================================================
// CLI
// ============================================================
struct Args {
    std::string input;
    std::string output = "output.png";
    std::string prompt;
    std::string size = "2K";
    bool no_watermark = false;
};

Args parse_args(int argc, char* argv[]) {
    Args args;
    for (int i = 1; i < argc; i++) {
        std::string arg = argv[i];
        if ((arg == "-i" || arg == "--input") && i + 1 < argc) {
            args.input = argv[++i];
        } else if ((arg == "-o" || arg == "--output") && i + 1 < argc) {
            args.output = argv[++i];
        } else if ((arg == "-p" || arg == "--prompt") && i + 1 < argc) {
            args.prompt = argv[++i];
        } else if ((arg == "-s" || arg == "--size") && i + 1 < argc) {
            args.size = argv[++i];
        } else if (arg == "--no-watermark") {
            args.no_watermark = true;
        }
    }
    return args;
}

int main(int argc, char* argv[]) {
    if (argc < 2) {
        std::cout << "图生图 Demo - 豆包 Seedream 4.5\n"
                  << "用法: ./img2img -i <输入图> -p <提示词> [-o <输出>] [-s <尺寸>] [--no-watermark]\n"
                  << "示例: ./img2img -i test.png -p \"转化为油画风格\"\n"
                  << "      ./img2img -i test.png -p \"赛博朋克\" -o cyber.png --no-watermark\n";
        return 1;
    }

    auto args = parse_args(argc, argv);

    if (args.input.empty()) {
        std::cerr << "错误: 缺少 -i/--input" << std::endl;
        return 1;
    }
    if (args.prompt.empty()) {
        std::cerr << "错误: 缺少 -p/--prompt" << std::endl;
        return 1;
    }

    std::string url = img2img(args.input, args.prompt, args.size, !args.no_watermark);
    std::cout << "下载中: " << url << std::endl;
    download(url, args.output);
    std::cout << "已保存到: " << args.output << std::endl;

    return 0;
}

编译和用法:

bash 复制代码
# 安装依赖
sudo apt install libcurl4-openssl-dev nlohmann-json3-dev cmake g++

# 编译
cmake -B build && cmake --build build

# 运行
./build/img2img -i ./test.png -p "转化为油画风格"
./build/img2img -i ./test.png -p "赛博朋克" -o cyber.png --no-watermark

二、什么是图生图 / 这个代码在做什么

2.1 图生图(Image-to-Image)

图生图就是给 AI 一张参考图 + 一段文字描述,让 AI 基于参考图生成新图片

  • 文生图:只给文字 → AI 凭空画
  • 图生图:给图片 + 文字 → AI 照着图片改、换风格、换背景

2.2 整体流程

无论 Python 还是 C++,走的都是同一条链路:

复制代码
┌──────────┐     ┌──────────┐     ┌───────────────┐     ┌──────────┐     ┌──────────┐
│ 本地图片  │ ──→ │ Base64   │ ──→ │ JSON body     │ ──→ │ HTTP POST│ ──→ │ 火山引擎  │
│ test.png │     │ 编码     │     │ {image, prompt}│     │          │     │ Ark API  │
└──────────┘     └──────────┘     └───────────────┘     └──────────┘     └────┬─────┘
                                                                            │
                                                                   返回结果图片 URL
                                                                            │
                                                              ┌─────────────┘
                                                              │
                                                        ┌─────▼──────┐
                                                        │ 下载到本地  │
                                                        │ output.png │
                                                        └────────────┘

为什么要 Base64 编码: JSON 只能传文本,图片是二进制。Base64 把图片的每一个字节翻译成可打印字符,这样图片就能"塞进"JSON 字符串里,和 API Key、prompt 一起打包发给服务器。


三、Python 方式的实现

Python 方式的核心优势:有现成的 OpenAI SDK

3.1 SDK 做了什么

python 复制代码
client = OpenAI(
    base_url="https://ark.cn-beijing.volces.com/api/v3",
    api_key="...",
)

这行创建了一个 HTTP 客户端对象。关键是 base_url 指向火山引擎,而不是 OpenAI 官网------火山引擎的 API 协议兼容 OpenAI 格式,所以能用同一个 SDK。

之后调用 client.images.generate(model=..., prompt=..., extra_body={...}) 时,SDK 在背后做了这些事:

  1. 把参数和 extra_body 合并,转成 JSON 字符串
  2. 拼完整 URL:https://ark.cn-beijing.volces.com/api/v3/images/generations
  3. 加上 Authorization: Bearer xxx 请求头
  4. 加上 Content-Type: application/json
  5. 发 HTTP POST 请求
  6. 解析返回的 JSON 响应
  7. 检查状态码,非 200 则抛异常
  8. 返回封装好的 Python 对象

你只写了一行代码,SDK 代劳了七步。

3.2 依赖的标准库

Python 版除了 openai SDK,其余的 base64argparseurllib 全是标准库自带,不需要额外安装:

用途
base64 图片二进制 → Base64 文本
argparse 解析 -i / -p / -o 命令行参数
urllib.request 下载生成的图片到本地

四、C++ 方式的实现

C++ 没有 OpenAI SDK,也没有内置 Base64,也没有内置 JSON 库,也没有内置 HTTP 库。所有东西要么手写,要么用第三方库。

4.1 依赖

Python C++
HTTP 通信 openai SDK(内部用 httpx/urllib) libcurl
JSON 内置 dict nlohmann/json(第三方头文件库)
Base64 标准库 import base64 手写 ~15 行(或用 OpenSSL)
文件读写 open() + f.read() std::ifstream + istreambuf_iterator
CLI 参数 argparse(标准库) 手写 argv 循环

4.2 关键差异:HTTP 请求

这是两个版本最大的不同点。Python 的 client.images.generate() 一行,在 C++ 里对应的是整套 libcurl 操作流程(下面有完整章节讲)。

4.3 关键差异:Base64

Python 有 base64.b64encode() 标准库,C++ 没有。要么自己写 ~15 行(如本项目),要么引入 OpenSSL、Boost 等第三方库。


五、C++ 中的 curl 深度解析

5.1 curl 是什么

curl 是一个发送网络请求的工具/库。它有两个形态:

形态 叫什么 怎么用 本项目用到
命令行工具 curl 终端里敲 curl -X POST ... 没有
C 语言库 libcurl 代码里 #include <curl/curl.h> 全部用它

它们是同一个项目(同名、同作者、同代码基础),只是调用层面不同。libcurl 就是命令行 curl 的 C 语言内核 ,你在终端敲的每个 curl 命令,底层都是通过 libcurl 的 API 执行的。

5.2 命令行 curl vs 代码中的 libcurl

如果把 C++ 代码做的事翻译成你熟悉的命令行 curl,就是:

bash 复制代码
curl -X POST \
  "https://ark.cn-beijing.volces.com/api/v3/images/generations" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer 6e69285a-..." \
  -d '{"model":"doubao-seedream-4-5-251128","prompt":"转化为油画风格","image":"data:image/png;base64,iVBORw0KGgo...","size":"2K","response_format":"url","watermark":true}'

代码里的每一行 curl_easy_setopt,在命令行 curl 里都有对应的参数:

复制代码
命令行的 -X POST       →  curl_easy_setopt(curl, CURLOPT_POST, 1L)
命令行的 -H "..."      →  curl_slist_append(headers, "...")
命令行的 -d '...'      →  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body_str.c_str())
命令行的 https://...    →  curl_easy_setopt(curl, CURLOPT_URL, "https://...")

区别只是:命令行 curl 帮你完成了 init / perform / cleanup,而代码里你得手动做。

5.3 libcurl 的四步固定流程

每次用 libcurl 发请求,必须走这四个步骤:

复制代码
curl_easy_init()          ← 创建会话对象
      │
      ▼
curl_easy_setopt() × N    ← 配置各种参数
      │
      ▼
curl_easy_perform()       ← 真正执行,阻塞等待
      │
      ▼
curl_easy_cleanup()       ← 销毁会话,释放资源

步骤 1 --- curl_easy_init()

在堆上创建一个 curl 会话对象(句柄)。"easy" 不是"简单"的意思 ,是相对于 curl_multi_(多路并发接口)而言的"单次同步请求模式"。

步骤 2 --- curl_easy_setopt()

这是配置的核心。curl 用一个统一的函数来设置所有参数,通过第二个参数(option)来区分"你要设什么东西":

cpp 复制代码
curl_easy_setopt(curl, CURLOPT_URL,             "...");   // 设目标 URL
curl_easy_setopt(curl, CURLOPT_POST,            1L);      // 设 HTTP 方法为 POST
curl_easy_setopt(curl, CURLOPT_HTTPHEADER,      headers); // 设请求头
curl_easy_setopt(curl, CURLOPT_POSTFIELDS,      "...");   // 设 body 内容
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE,   123);     // 设 body 长度
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION,   callback);// 设接收回调
curl_easy_setopt(curl, CURLOPT_WRITEDATA,       &buf);    // 设回调参数
curl_easy_setopt(curl, CURLOPT_TIMEOUT,         120L);    // 设超时

每个 CURLOPT_* 控制一个维度。整个配置阶段就是不断调 setopt,把你关心的事情都设好。

步骤 3 --- curl_easy_perform()

真正的执行。调用后阻塞当前线程 ,直到请求完成、超时、或出错。返回值是 CURLcode 枚举,表示网络层面的结果(不是 HTTP 状态码)。

步骤 4 --- curl_easy_cleanup()

销毁会话,释放 libcurl 内部持有的所有资源(连接池、缓存、cookie 等)。

5.4 回调机制:libcurl 最核心的设计

这是使用 libcurl 最容易困惑的地方。大多数 HTTP 库都是"调一个函数,返回完整响应",libcurl 不是。

libcurl 的做法是:服务器返回数据是分块到达的,每收到一块,libcurl 就调一次你注册的回调函数,把这一小块数据塞给你。

cpp 复制代码
// 注册回调
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);  // "用这个函数收数据"
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_data);      // "这个指针传给回调"

// 回调的实现
static size_t write_callback(void* contents, size_t size, size_t nmemb, void* userp) {
    // contents → 刚收到的一小块数据
    // size * nmemb → 这一小块有多大
    // userp → 就是上面设置的 &response_data
    ((std::string*)userp)->append((char*)contents, size * nmemb);
    return size * nmemb;
}

为什么不直接返回完整结果: 网络数据不是一次到齐的。TCP 是流式协议,服务器可能先发 1460 字节,再发 1460 字节,再发 800 字节。回调让你每一块到达时立刻可以处理,不用等全部收完。对于大文件下载(比如图片),可以一块一块直接写磁盘,不占满内存。

WRITEDATAWRITEFUNCTION 的关系:

复制代码
curl 内部每收到一块数据:
  write_callback(
      contents,       ← 数据块地址
      size,           ← 每元素大小 (通常是 1)
      nmemb,          ← 元素个数
      &response_data  ← 你通过 CURLOPT_WRITEDATA 传入的指针
  )

WRITEFUNCTION 决定"谁来处理",WRITEDATA 决定"处理时把什么传进去"。

5.5 两次 curl 调用的回调对比

本项目中共用了两次 libcurl------一次发图生图请求,一次下载结果图片。它们的回调设计不同:

img2img(发 POST) download(GET 下载)
目的 收 JSON 响应文本 收二进制图片数据
写到哪里 std::string(内存) std::ofstream(直接写磁盘)
为什么 JSON 很小,需要解析后取 URL 图片可能很大,不占内存
实现方式 static 普通函数 C++ lambda 表达式
cpp 复制代码
// POST 时:数据拼进 string,后续 json::parse() 解析
static size_t write_callback(void* contents, size_t size, size_t nmemb, void* userp) {
    ((std::string*)userp)->append((char*)contents, size * nmemb);
    return size * nmemb;
}

// 下载时:数据直接写磁盘
auto file_callback = [](void* ptr, size_t size, size_t nmemb, void* stream) -> size_t {
    auto* ofs = (std::ofstream*)stream;
    ofs->write((char*)ptr, size * nmemb);
    return size * nmemb;
};

5.6 错误处理的两个层次

cpp 复制代码
// 第一层:网络错误
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK) {
    // "网线拔了"、"DNS 挂了"、"SSL 过期"、"超时"
    // 服务器根本没收到请求
}

// 第二层:HTTP 错误
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
if (http_code != 200) {
    // 服务器收到了请求,但拒绝了:
    //   400 → 参数错误
    //   401 → API Key 无效
    //   500 → 服务器内部错误
}

必须先检查第一层再检查第二层------网络通才有 HTTP 状态码可言。

5.7 资源释放顺序

cpp 复制代码
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);  // 1. 先读数据
curl_slist_free_all(headers);                                   // 2. 释放请求头链表
curl_easy_cleanup(curl);                                        // 3. 销毁会话

顺序不能乱:

  • getinfo 必须在 cleanup 之前(会话销毁了就读不到了)
  • free_all 可以在 getinfo 后任意时机(headers 不属于会话内部状态)

六、两个版本的关键对应关系

环节 Python C++
读文件 open(path, "rb").read() std::ifstream + istreambuf_iterator
Base64 base64.b64encode()(标准库) base64_encode()(手写 ~15 行)
拼 Data URI f"data:{mime};base64,{b64}" "data:" + mime + ";base64," + b64
构造 JSON dict 字面量 nlohmann::json,语法几乎一样
序列化 JSON SDK 内部 json.dumps() body.dump()
HTTP 请求 client.images.generate() 一行 curl_easy_* 约 20 行
设置鉴权 api_key="..." 参数 手动拼 Authorization 请求头
解析响应 .data[0].url json::parse()["data"][0]["url"]
下载图片 urllib.request.urlretrieve() curl_easy_* GET + ofstream
CLI 参数 argparse(标准库) 手写 argv for 循环

七、总结

Python 版本适合快速验证、demo。 60 行代码,SDK 代劳了大量底层操作,你可以专注于业务逻辑(拼 prompt、调参数)。

C++ 版本适合嵌入更大的 C++ 项目。 代码量大约是 Python 的 4 倍,但你能完全控制每一步------用哪个 HTTP 库、JSON 怎么解析、内存如何分配。curl 虽然用起来啰嗦,但它是一个稳定了 25 年的 C 库,几乎每台 Linux 机器上都有,没有额外的运行时依赖。

两个版本发出的 HTTP 请求是完全一样的,服务器无法区分请求是 Python 发的还是 C++ 发的------它们等价。

相关推荐
huahailing102415 小时前
Java基于【S3】协议实现游标分页查询
java·开发语言
Tokai_Teio_115 小时前
XYCTF 2024 web(1)
java·服务器·开发语言
暗冰ཏོ15 小时前
Java 从入门到高级实战全套教程
java·开发语言
过期动态15 小时前
【LeetCode 热题 100】最长连续序列
java·数据结构·spring boot·算法·leetcode·职场和发展
多加点辣也没关系15 小时前
Spring MessageSource 国际化方案
java·后端·spring
曾阿伦15 小时前
Python3 滑块验证码破解
python
程序猿七度15 小时前
IDEA2026.1中配置Codex(非官方订阅-针对国内走中转路线NewApi)
java·intellij-idea·codex·newapi
deepin_sir15 小时前
04 - 运算符与表达式
java·开发语言·python
devnullcoffee15 小时前
亚马逊卖家公开信息数据提取:反爬攻防战与 Python 批量采集实战
开发语言·python·亚马逊数据采集·亚马逊数据 api·amazon 选品数据·亚马逊卖家数据