旨在通过COS客户端实现图片随机访问。

废话不多说,咱们开始吧,搭建一个属于自己的随机图片图床。

@HoR 119305932-ysmj.gif

一、环境和工具介绍

  • 操作系统:Ubuntu 24.04 LTS

  • 运维面板:1Panel 社区版 v1.10.23-lts

  • 代理服务器 OpenResty:1.21.4.3-3-3-focal

  • PHP版本:8.3.8

  • Python版本:3.10

  • Composer版本:v2.8.5

小伙伴们可以自行选择对应的环境和工具,以上仅供参考。

二、前期准备

2.1 COS的部署

因为相关的教程,网上已经有很多了,所以这里我不在赘述了。

推荐一个相关教程:https://www.cnblogs.com/txycsig/p/18512703

注意这里创建存储桶时,因为图片要外部访问,所以要勾选公共读私有写。

后续可以根据自己的业务需求选择相应的配置。


2.2 PHP网站的搭建

首先创建一个运行环境

登录1panel运维面板,点击网络 -> 运行环境 -> 创建运行环境

扩展按照自己的选择进行添加。


接下来就是创建网站:

运行环境的主域名设置成你的IP:端口

这里和PHP8端口冲突了,换一个就行,比如改成9001,后面的反向代理就是通过这个端口来获取资源。

然后进入该网站目录的index文件夹,之后就可以上传你的php项目了:


接下来可以选择的做反向代理:

代理地址的端口号改成你自己所配置的PHP-FPM的端口。

至此PHP网站搭建完成。


三、代码的编写

3.1 图片分类和处理

这里我想做个api分类,要用到三类图,横屏、竖屏、方图,分别对应文件夹landscape,portrait,square

可以写个py脚本来实现分类:

需要用到的库:

import io
import os
from PIL import Image
import time
from tqdm import tqdm
from concurrent.futures import ThreadPoolExecutor

可能要处理的图片比较多,所以我这里用到了线程池处理:

def classify_images(source_folder):
    """
    将图片分类到横屏、竖屏和方形目录
    :param source_folder: 待处理图片的文件夹路径
    """
    image_files = [os.path.join(source_folder, f) for f in os.listdir(source_folder) if
                   f.lower().endswith(('.png', '.jpg', '.jpeg'))]

    with tqdm(total=len(image_files), desc="Sorting images") as pbar:
        with ThreadPoolExecutor(max_workers=4) as executor:
            futures = [executor.submit(process_image, file_path) for file_path in image_files]
            for future in futures:
                future.result()  # 等待线程完成
                pbar.update(1)

图片我是根据时间戳重命名处理,方便以后的管理:

def process_image(file_path):
    """处理单张图片"""
    try:
        with Image.open(file_path) as img:
            # 获取图片尺寸
            width, height = img.size

            # 根据尺寸分类
            if abs(width - height) < 150:
                destination = 'square'  # 方形
            elif width > height:
                destination = 'landscape'  # 横屏
            else:
                destination = 'portrait'  # 竖屏

            # 如果图片过大则调整大小,取消注释使用
            # img = resize_image(img)

            # 生成时间戳和新文件名
            timestamp = int(time.time() * 1000)
            base_name = os.path.splitext(file_path)[0][-6:]
            ext = img.format.lower().replace('jpeg', 'jpg').replace('png', 'png')
            new_filename = f"{base_name}{timestamp}.{ext}"
            new_file_path = os.path.join(destination, new_filename)

            # 保存图片
            img.save(new_file_path)

如果你觉得原始图片大小过大,可以自己添加限制,这里我限制在5M以内:

def resize_image(img, max_size=5 * 1024 * 1024):
    """调整图片大小以满足最大文件大小限制"""
    if img.size[0] * img.size[1] < max_size:
        return img
    quality = 95
    img_byte_arr = io.BytesIO()

    try:
        if img.format == 'PNG':
            img = img.convert('RGB')
            img.save(img_byte_arr, format='JPEG', optimize=True)
            img = Image.open(img_byte_arr)
        else:
            img.save(img_byte_arr, format=img.format, optimize=True)

        if img.format == 'JPEG':
            while img_byte_arr.getbuffer().nbytes > max_size and quality > 10:
                img_byte_arr = io.BytesIO()  # 重置字节流
                img.save(img_byte_arr, format='JPEG', quality=quality, optimize=True)
                quality -= 5

        img_byte_arr.seek(0)
        return Image.open(img_byte_arr)
    except Exception as e:
        print(f"Error resizing image: {e}")
        return img

3.2 图片上传

首先需要下载qcloud_cos依赖:

pip install qcloud_cos  

需要用到的库:

import os
from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
from qcloud_cos.cos_exception import CosClientError, CosServiceError

一些参数:

SECRET_ID = ''
SECRET_KEY = ''
REGION = '' # 例如ap-shanghai
BUCKET = '' # 桶名字
FOLDER_PATH = ['landscape', 'portrait', 'square'] # 文件夹名字
ALLOWED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.bmp'}

# 初始化配置和客户端
config = CosConfig(
    Region=REGION,
    SecretId=SECRET_ID,
    SecretKey=SECRET_KEY
)
client = CosS3Client(config)

上传的方法:

def upload_images_to_cos():
    try:
        for folder in FOLDER_PATH:
            for root, dirs, files in os.walk(folder):
                for filename in files:
                    ext = os.path.splitext(filename)[1].lower()
                    if ext not in ALLOWED_EXTENSIONS:
                        continue

                    local_path = os.path.join(root, filename)

                    # 构造对象键(保留完整文件夹结构)
                    object_key = os.path.relpath(local_path, start='.')  # 获取相对当前目录的路径
                    object_key = object_key.replace('\\', '/')  # 统一使用斜杠

                    # 上传文件
                    client.upload_file(
                        Bucket=BUCKET,
                        LocalFilePath=local_path,
                        Key=object_key,
                        PartSize=10,
                        MAXThread=10,
                        EnableMD5=False
                    )
                    print(f'上传成功: {local_path}')

    except CosClientError as client_error:
        print(f'客户端错误: {client_error}')
    except CosServiceError as service_error:
        print(f'服务端错误: [{service_error.get_status_code()}] {service_error.get_error_message()}')
    except Exception as e:
        print(f'其他错误: {str(e)}')

3.3 php的依赖引入

  1. 建议中国大陆地区的用户先设置腾讯云镜像源:

composer config -g repos.packagist composer https://mirrors.tencent.com/composer/
  1. 在项目的目录下通过Composer下载SDK依赖,下载成功之后会在项目根目录下自动生成 vendor目录、composer.json和composer.lock文件:

composer require qcloud/cos-sdk-v5    
  1. 通过Composer下载yaml依赖(可选),我是用来存放个人秘钥信息的:

composer require symfony/yaml 

创建一个CosConfig.yaml,填入你的个人秘钥信息:

secretId: '' # 替换为你的 SecretId
secretKey: '' # 替换为你的 SecretKey
region: '' # 替换为你桶所在的地域。例:ap-shanghai
bucket: '' # 替换为你的桶名
domainCDN: '' # 替换为你的CDN加速域名(可选),默认放空。例:https://example.com

3.4 API编写

这里我编写了一个图片加载器(cos.php),通过连接腾讯云COS客户端,实现不同类型图片的访问。

<?php
require_once 'vendor/autoload.php';

use Symfony\Component\Yaml\Yaml;
use Qcloud\Cos\Client;

class ImageLoader
{
    private Client $cosClient;
    private array $config;
    private string $prefix;

    public function __construct(string $yamlUrl, string $dirUrl)
    {
        // 读取 YAML 配置文件
        $this->config = Yaml::parseFile($yamlUrl);
        // 初始化COS客户端
        $this->cosClient = $this->createCosClient();
        // COS文件夹名字
        $this->prefix = $dirUrl;
    }

    // 创建COS客户端
    private function createCosClient(): Client
    {
        return new Client([
            'region' => $this->config['region'],
            'credentials' => [
                'secretId' => $this->config['secretId'],
                'secretKey' => $this->config['secretKey'],
            ],
            'scheme' => 'https', // 强制使用 HTTPS
        ]);
    }

    // 获取图片列表,支持分页
    public function getImgList(): ?array
    {
        return $this->executeCosRequest(function () {
            $imgs = [];
            $nextMarker = null;
            do {
                // 每次请求获取最多 1000 个文件
                $params = [
                    'Bucket' => $this->config['bucket'],
                    'Prefix' => $this->prefix,
                    'MaxKeys' => 1000,
                ];
                if ($nextMarker) {
                    $params['Marker'] = $nextMarker;  // 分页时传递上次请求的Marker
                }
                $result = $this->cosClient->listObjects($params);
                if (isset($result['Contents'])) {
                    $files = array_slice($result['Contents'], 1);  // 跳过第一个文件
                    foreach ($files as $file) {
                        // 判断是否使用 CDN
                        if (!empty($this->config['domainCDN'])) {
                            $imgs[] = $this->config['domainCDN'] . "/" . $file['Key'];
                        } else {
                            $imgs[] = $file['Key'];
                        }
                    }
                }
                // 检查是否还有下一页
                $nextMarker = $result['NextMarker'] ?? null;
            } while ($nextMarker); // 如果有下一页,继续分页
            return $imgs ?: null;
        });
    }

    // 获取图片内容
    public function getImageContent(string $imgKey): ?array
    {
        return $this->executeCosRequest(function () use ($imgKey) {
            if (!empty($this->config['domainCDN'])) {
              // 使用 cURL 获取 CDN 图片内容
                $ch = curl_init();
                curl_setopt($ch, CURLOPT_URL, $imgKey);
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);  // 返回响应而不是直接输出
                curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);  // 启用重定向支持
                curl_setopt($ch, CURLOPT_TIMEOUT, 30);
                curl_setopt($ch, CURLOPT_USERAGENT, 'PHP cURL');
                // 执行 cURL 请求
                $imageContent = curl_exec($ch);
                if (curl_errno($ch)) {
                    // 如果 cURL 请求失败
                    echo 'cURL Error: ' . curl_error($ch);
                    curl_close($ch);
                    return null;
                }
                // 获取响应的内容类型
                $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
                // 关闭 cURL 会话
                curl_close($ch);
                return [
                    'body' => $imageContent,
                    'contentType' => $contentType,
                ];
            } else {
                // 不通过CDN
                $result = $this->cosClient->getObject([
                    'Bucket' => $this->config['bucket'],
                    'Key' => $imgKey,
                ]);
                return [
                    'body' => $result['Body']->getContents(),
                    'contentType' => $result['ContentType'],
                ];
            }
        });
    }

    // 获取随机图片
    public function getRandomImage(): ?array
    {
        $imgList = $this->getImgList();
        if ($imgList === null) {
            echo "读取图片列表失败。";
            return null;
        }
        shuffle($imgList);
        $imageContent = $this->getImageContent($imgList[0]);
        return $imageContent ? [
            'body' => $imageContent['body'],
            'contentType' => $imageContent['contentType']
        ] : null;
    }

    // 输出随机图片
    public function outputRandomImage(): void
    {
        $imageData = $this->getRandomImage();
        if ($imageData !== null) {
            header('Content-Type: ' . $imageData['contentType']);
            echo $imageData['body'];
        } else {
            echo "没有找到图片。";
        }
    }

    // 执行COS请求,统一异常处理
    private function executeCosRequest(callable $request)
    {
        try {
            return $request();
        } catch (Exception $e) {
            echo "操作失败: " . $e->getMessage();
            return null;
        }
    }
}

以下为自适应的调用例子,根据设备的不同类型($deviceUrl)选择相应的图片类型:

<?php
require_once '../cos.php';

$yamlUrl = "../config/CosConfig.yaml";

// 获取设备类型
$deviceUrl = preg_match('/(android|iphone|ipad|mobile)/i',
    $_SERVER['HTTP_USER_AGENT']) ? 'portrait/' : 'landscape/';

$imageLoader = new ImageLoader($yamlUrl, $deviceUrl);
$imageLoader->outputRandomImage();

在设置的php默认文件中,例如index.php进行调用。

四、项目上传至服务器

代码写完之后就可以将项目部署到服务器上了。

将项目上传到之前创建的运行环境网站的index目录下:

当前主要的结构目录如下:

  • index.php

  • adapt(文件夹)

    • index.php

  • landscape(文件夹)

    • index.php

  • portrait(文件夹)

    • index.php

  • square(文件夹)

    • index.php

设置example.com的反向代理:

例如访问example.com/adapt就是自适应的api,example.com/landscape就是横屏的api。

至此就大功告成了。