免费图库集成 - Pexels API实战


04 - 免费图库集成 — Pexels API 实战


本章目标

  • 注册 Pexels API 并获取免费 Key
  • 用 Go 的 net/http 调用 REST API
  • JSON 反序列化处理 API 响应
  • 下载图片并限制文件大小
  • 将 PexelsService 注册为 Wails Service

4.1 Pexels API 简介

Pexels 是全球最大的免费图库之一,提供高质量的照片和视频。它的 API 对开发者非常友好:

  • 免费 — 无需信用卡,注册即用
  • 速率限制 — 每月 200 次请求(免费版),开发测试完全够用
  • 高质量 — 照片均由专业摄影师上传
  • 无需署名 — 虽然建议署名,但不强制

注册获取 API Key

  1. 访问 pexels.com/api
  2. 点击「Join Pexels」注册账号
  3. 登录后到 pexels.com/api/new 创建应用
  4. 复制生成的 API Key(格式类似:AbCDeFgHiJkLmNoPqRsTuVwXyZ123456

4.2 API 接口分析

搜索图片

GET https://api.pexels.com/v1/search?query=dogs&per_page=5

Headers:
  Authorization: YOUR_API_KEY

Response:
{
  "total_results": 10000,
  "page": 1,
  "per_page": 5,
  "photos": [
    {
      "id": 1234567,
      "width": 4000,
      "height": 6000,
      "url": "https://www.pexels.com/photo/...",
      "photographer": "John Doe",
      "src": {
        "original":  "https://images.pexels.com/photos/...",
        "large":     "https://images.pexels.com/photos/...?w=1920",
        "medium":    "https://images.pexels.com/photos/...?w=800",
        "small":     "https://images.pexels.com/photos/...?w=400",
        "portrait":  "https://images.pexels.com/photos/...?w=600",
        "landscape": "https://images.pexels.com/photos/...?w=1200",
        "tiny":      "https://images.pexels.com/photos/...?w=200"
      },
      "alt": "A cute dog playing in the park"
    }
  ]
}

💡 知识点: Pexels 对同一张图片提供了 7 种尺寸。我们选择 medium(800px 宽)用于 Word 文档嵌入,既能保证清晰度,又不会让文档体积过大。


4.3 Go 结构体设计

创建 backend/pexels.go

package backend

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "time"
)

// PexelsPhoto Pexels 图片信息
type PexelsPhoto struct {
    ID              int    `json:"id"`
    Width           int    `json:"width"`
    Height          int    `json:"height"`
    URL             string `json:"url"`
    Photographer    string `json:"photographer"`
    PhotographerURL string `json:"photographer_url"`
    Src             struct {
        Original  string `json:"original"`
        Large     string `json:"large"`
        Medium    string `json:"medium"`
        Small     string `json:"small"`
        Portrait  string `json:"portrait"`
        Landscape string `json:"landscape"`
        Tiny      string `json:"tiny"`
    } `json:"src"`
    Alt string `json:"alt"`
}

// PexelsResponse Pexels API 响应
type PexelsResponse struct {
    TotalResults int           `json:"total_results"`
    Page         int           `json:"page"`
    PerPage      int           `json:"per_page"`
    Photos       []PexelsPhoto `json:"photos"`
    NextPage     string        `json:"next_page"`
}

💡 知识点: Src 字段使用了内嵌匿名 structSrc struct {...} json:"src")。当 JSON 嵌套层级较深且不想创建独立类型时,这种写法很方便。


4.4 创建 PexelsService

// PexelsService Pexels 配图服务
type PexelsService struct {
    configService *ConfigService
    httpClient    *http.Client
}

// NewPexelsService 创建 Pexels 服务
func NewPexelsService(cs *ConfigService) *PexelsService {
    return &PexelsService{
        configService: cs,
        httpClient: &http.Client{
            Timeout: 30 * time.Second,
        },
    }
}

⚠️ 注意:http.Client 设置超时很重要。如果不设超时,网络异常时 goroutine 会永久阻塞。


4.5 实现图片搜索

// SearchImages 搜索图片(前端调用)
// query: 搜索关键词
// perPage: 返回数量(最多 80)
func (ps *PexelsService) SearchImages(query string, perPage int) ([]PexelsPhoto, error) {
    cfg, err := ps.configService.Load()
    if err != nil {
        return nil, fmt.Errorf("加载配置失败: %w", err)
    }
    if cfg.PexelsAPIKey == "" {
        return nil, fmt.Errorf("请先配置 Pexels API Key")
    }

    if perPage <= 0 {
        perPage = 5
    }
    if perPage > 80 {
        perPage = 80
    }

    // 构建请求 URL
    apiURL := fmt.Sprintf("https://api.pexels.com/v1/search?query=%s&per_page=%d",
        url.QueryEscape(query), perPage)

    req, err := http.NewRequest("GET", apiURL, nil)
    if err != nil {
        return nil, fmt.Errorf("创建请求失败: %w", err)
    }
    req.Header.Set("Authorization", cfg.PexelsAPIKey)

    resp, err := ps.httpClient.Do(req)
    if err != nil {
        return nil, fmt.Errorf("Pexels API 请求失败: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
        return nil, fmt.Errorf("Pexels API 返回错误 (状态码 %d): %s", 
            resp.StatusCode, string(body))
    }

    var result PexelsResponse
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, fmt.Errorf("解析 Pexels 响应失败: %w", err)
    }

    return result.Photos, nil
}

逐行解析

代码 说明
url.QueryEscape(query) URL 编码,将"金色 幼犬"转为 %E9%87%91%E8%89%B2+%E5%B9%BC%E7%8A%AC
req.Header.Set("Authorization", ...) Pexels 使用 Header 认证,不是 query param
defer resp.Body.Close() 确保响应体一定被关闭,防止连接泄漏
io.LimitReader(resp.Body, 1024) 限制错误响应的读取量,防止内存被大量错误信息撑爆
json.NewDecoder(resp.Body).Decode(...) 流式解码,避免将整个响应读入内存

4.6 实现图片下载

// DownloadImage 下载图片(内部使用)
func (ps *PexelsService) DownloadImage(imageURL string) ([]byte, error) {
    resp, err := ps.httpClient.Get(imageURL)
    if err != nil {
        return nil, fmt.Errorf("下载图片失败: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("下载图片返回状态码 %d", resp.StatusCode)
    }

    // 限制最大 10MB,防止恶意或异常大文件
    data, err := io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024))
    if err != nil {
        return nil, fmt.Errorf("读取图片数据失败: %w", err)
    }
    return data, nil
}

⚠️ 安全提示: 永远对从互联网下载的数据设置大小限制。不加限制的话,攻击者或错误的 API 可能返回数 GB 的数据,直接撑爆内存。


4.7 注册到 main.go

func main() {
    configService := backend.NewConfigService()
    agentService := backend.NewAgentService(configService)
    pexelsService := backend.NewPexelsService(configService)

    app := application.New(application.Options{
        Services: []application.Service{
            application.NewService(configService),
            application.NewService(agentService),
            application.NewService(pexelsService),
        },
        // ...
    })
}

4.8 前端调用示例

import { PexelsService } from "../bindings/pet-content-creator/backend";

// 搜索图片
const photos = await PexelsService.SearchImages("golden retriever puppy", 6);
photos.forEach(p => {
    console.log(p.src.medium, p.photographer);
});

因为 PexelsService 注册到了 Wails,前端绑定会自动生成,不需要写任何 IPC 代码。


4.9 错误处理最佳实践

网络层面的容错

图片搜索可能因为以下原因失败:

  • 无网络连接
  • Pexels API 超时
  • API Key 过期
  • 达到速率限制

在后续的编排章节中,我们会看到如何优雅地处理这些错误:

// 搜索失败时不阻塞整体流程
photos, err := ps.SearchImages(tag, count)
if err != nil {
    log.Printf("搜索图片失败 (tag=%s): %v", tag, err)
    continue  // 尝试下一个关键词
}

本章总结

你已经学会 对应能力
net/http 构造 GET 请求 REST API 调用
req.Header.Set() 自定义请求头
json.NewDecoder(resp.Body) 流式 JSON 解码
url.QueryEscape() URL 参数编码
io.LimitReader() 限制下载/读取大小
http.Client + Timeout HTTP 客户端超时控制
错误隔离 局部失败不影响整体

🔧 动手练习

  1. 实现按颜色搜索(Pexels 支持 color= 参数,如 redblue
  2. 实现按方向搜索(orientation=landscape/portrait/square
  3. 增加图片缓存:同一 URL 不重复下载,先检查本地缓存目录
  4. SearchImages 增加 page 参数,支持翻页

👉 下一章:纯 Go 生成 Word 文档 — OOXML 探秘

wx

关注公众号

©2017-2023 鲁ICP备17023316号-1 Powered by Hugo