基于云存储自建随机图 API
文章发布于 76 天前,部分内容可能已过时或需要更新,参考时请注意。

前言

这个实践方案来自我自己的需求——网站首页需要图片背景,然而,放两张固定图未免显得有些枯燥乏味,网上其他的随机图 API 则质量良莠不齐,风格不统一,更重要的是加载速度不够稳定,偶尔遇上抽风,就会严重影响首页的加载速度和浏览体验。

实际上,对于自建随机图片 API,全网已经有不少成熟方案,大部分情况下,我们只需要 copy 过来做简单修改即可,但浏览一圈,对比我自己的需求,大部分方案如果要用,都得进行不小的更改。因此,考虑到功能不算复杂,我决定自己亲自动手设计。

我的需求

设计之前,得先明确需求,但在这之前,我们不妨先来梳理一下,一个随机图片 API 是如何工作的。

访问者首先输入 API 的调用域名,通过配置好的 DNS 解析连接到对应的服务器 IP,服务器调出对应页面,再根据页面的处理逻辑来返回图片资源。

这个流程最重要的部分有两处:一个是存储图片的资源库,一个是 API 的主页面,我们的需求也主要是针对这两个部分。

首先,对于图片库,我希望它能够建立在 API 服务器之外的其他云端存储容器上,以远程读取的方式实现调用, 原因也很好理解:服务器的存储空间本身“寸土寸金”,把这些容量单纯拿来放图片,实在是有些浪费;其次,服务器本身如果要实现高频、快速的图片读取,要花费额外的时间做优化,但很多第三方云存储平台(如腾讯云 COS、阿里云 OSS、又拍云存储等)已经考虑到用户将其作为图床的使用需求,针对图片调取已经有了很多省心的设计(比如能够一键开启的 Referer、User-Agent 等多种形式的防盗链、方便的 CORS 跨域请求配置等),既然如此,我们当然应该善加利用。

确定了图片库的部署位置,接下来我们就该确定 API 应该实现的功能,在最初阶段,我希望它至少能够实现以下几点:

  • 对接图片库,实现定时自动同步
  • 针对来自不同的设备调用请求,能够返回不同比例的图片(图片自适应)

在此基础上,我们就可以开始正式设计了。

建立图片库+API 设计

我个人使用的是又拍云存储,所谓的建立存储库,无非也就是把图片上传到里面而已,但在此过程中,还是有一些需要注意的地方: 首先,图片在上传时就应该分门别类地存放,这样在设计调用逻辑时会减少很多无谓的麻烦。 像我就只依据比例做了宽屏和窄屏的区分,把他们放在不同的文件夹,如果你有更高级的需求(比如需要依据题材或画风分类调用),那就可能需要做更精细的分类;其次,图片的质量、格式最好在上传前或上传时就进行统一,以免在调用时出现问题。 我的图片用于网页,所以我一般习惯在上传时统一将其转换成浏览器支持的、更小更先进的 webp 格式,同时设置压缩质量为 75% 以适当控制大小(相信我,你不会想看到 20MB 的图片在你的首页一块一块加载的),这部分各位可以根据需求自行调整。

又拍云存储本身支持通过 REST API、FORM API 和各类 SDK 上传图片,并进行异步或同步的预处理,其他各类云存储平台也有类似的接口,在批量上传时进行压缩、格式处理等操作还是相对轻松的。当然,如果你用不惯这类接口,想通过更直观的方式进行,也可以参考我的往期文章:搭建一个属于你的免费图床(PicList+Cloudflare R2),类似 PicList 的大多数图床管理软件也具有类似的功能。

上文提到,云存储平台往往有自己支持的各类接口,因此,我们可以通过这些接口对内容进行远程调用,实现我们想要的效果。所以,是不是我们只要在收到调用请求后,不停地通过接口返回图片就可以了呢?理论上似乎行得通,但这对接口的负担未免太重,更何况,大多数此类接口都有频次限制,短时间高频调用,难免会出现各种各样的问题——也许,还有更好的思路?

事实上,如果在云存储平台绑定了域名,其中的所有的资源都会有一个唯一的访问链接,通过链接访问图片,就和通过域名访问服务器资源是一样的。所以,如果我们只是通过接口,获得对应文件夹内所有图片的访问连接,再在收到调用请求时通过该链接返回图片,不就能避免对接口的高频调用了吗?

配图

运行流程

这似乎是个不错的思路,围绕这一点,经过规划,我将 API 的运作分为了两大模块:

  1. 获取图片链接。以又拍云存储为例,文件的链接形式一般是“自定义域名 + 文件夹路径 + 文件名”,只要通过调用接口,遍历特定文件夹下文件的路径和名称,再与自定义域名进行 url 拼接,即可获得其访问链接,将其存储在 API 服务器上,即可作为调用图片的地址来源。这一部分的功能可以通过简单的脚本完成,设置成定时任务,即可实现定时文件同步。
  2. 调用图片。有了第一步存储的地址,我们就可以把调用的目标设置为存储有链接的本地文件,通过随机逻辑返回不同链接的内容,即可完成“调用随机图片”的动作。但在这一步之前,我们还需要解决发起请求的设备判断问题。

鉴于我正在使用 wordpress 博客系统,服务器上已经有 php 环境,因此我选择了又拍云的 PHP SDK 完成此任务。同时,又由于本人的 php 水准实在够呛,因此,大部分的代码实际上由 AI 编写,此处放出,仅供参考:

获取图片链接(部分):

<?php
require_once &#039;/file path/autoload.php&#039;;

use Upyun\Upyun;
use Upyun\Config;

// 初始化又拍云服务配置
$serviceConfig = new Config(&#039;bucket&#039;, &#039;user&#039;, &#039;key&#039;);
$client = new Upyun($serviceConfig);

// 目录路径
$pcPath = &#039;/file path/pc/&#039;;
$mobilePath = &#039;/file path/mobile/&#039;;

// 获取图片 URL(生成半成品 URL)
function getImageUrls($client, $path) {
    $urls = [];
    $response = $client-&gt;read($path, null, [&#039;X-List-Iter&#039; =&gt; &#039;&#039;, &#039;X-List-Limit&#039; =&gt; 100]);

    if (!empty($response) &amp;&amp; is_array($response) &amp;&amp; isset($response[&#039;files&#039;]) &amp;&amp; is_array($response[&#039;files&#039;])) {
        foreach ($response[&#039;files&#039;] as $item) {
            if (is_array($item) &amp;&amp; isset($item[&#039;name&#039;]) &amp;&amp; $item[&#039;type&#039;] === &#039;N&#039;) {
                // 生成半成品 URL(不含格式转换参数)
                $halfUrl = $path . &#039;/&#039; . $item[&#039;name&#039;];
                $urls[] = $halfUrl;
            }
        }
    }
    return $urls;
}

// 获取图片 URL 并保存到 txt 文件
$pcUrls = getImageUrls($client, $pcPath);
$mobileUrls = getImageUrls($client, $mobilePath);

// 保存半成品 URL 到 txt 文件
saveUrlsToTxt($pcUrls, &#039;/file path/half_pc.txt&#039;);
saveUrlsToTxt($mobileUrls, &#039;/file path/half_mobile.txt&#039;);

function saveUrlsToTxt($urls, $filePath) {
    if (!empty($urls)) {
        $content = implode(&quot;\n&quot;, $urls);
        file_put_contents($filePath, $content);
    }
}

URL 拼接,获得完整链接:

#!/bin/bash

# 定义半成品txt文件的路径
HALF_PC=&quot;/file path/half_pc.txt&quot;
HALF_MOBILE=&quot;/file path/half_mobile.txt&quot;

# 定义最终txt文件的路径
FINAL_PC=&quot;/file path/pc.txt&quot;
FINAL_MOBILE=&quot;/file path/mobile.txt&quot;

# 定义又拍云的域名
UPYUN_DOMAIN=&quot;domain&quot;

# 清空最终文件
> &quot;$FINAL_PC&quot;
> &quot;$FINAL_MOBILE&quot;

# 函数:为URL添加.webp参数,如果URL不以.webp结尾
addWebpParam() {
    local path=&quot;$1&quot;
    local name=&quot;$2&quot;
    # 构造完整的URL
    local formattedUrl=&quot;${UPYUN_DOMAIN}${path}${name}&quot;
    # 如果文件扩展名不是.webp,则添加参数
    if [[ $name != *.webp ]]; then
        formattedUrl=&quot;${formattedUrl}!/format/webp&quot;
    fi
    echo &quot;$formattedUrl&quot;
}

# 构造PC图片的最终URL并保存到最终txt文件
echo &quot;Processing PC images...&quot;
while IFS= read -r line || [ -n &quot;$line&quot; ]; do
    # 读取路径和文件名
    path=&quot;${line%/*}&quot;
    name=&quot;${line##*/}&quot;
    # 构造合法URL并在最后添加参数
    finalUrl=&quot;$(addWebpParam &quot;$path&quot; &quot;$name&quot;)&quot;
    echo &quot;$finalUrl&quot; &gt;&gt; &quot;$FINAL_PC&quot;
done &lt; &quot;$HALF_PC&quot;

# 构造Mobile图片的最终URL并保存到最终txt文件
echo &quot;Processing Mobile images...&quot;
while IFS= read -r line || [ -n &quot;$line&quot; ]; do
    # 读取路径和文件名
    path=&quot;${line%/*}&quot;
    name=&quot;${line##*/}&quot;
    # 构造合法URL并在最后添加参数
    finalUrl=&quot;$(addWebpParam &quot;$path&quot; &quot;$name&quot;)&quot;
    echo &quot;$finalUrl&quot; &gt;&gt; &quot;$FINAL_MOBILE&quot;
done &lt; &quot;$HALF_MOBILE&quot;

# 清空半成品txt文件
echo &quot;Clearing half product files...&quot;
> &quot;$HALF_PC&quot;
> &quot;$HALF_MOBILE&quot;

# 连接到Redis
echo &quot;Connecting to Redis...&quot;
redis-cli -h 127.0.0.1 -p 6379

# 清空Redis中的图片链接集合
echo &quot;Clearing Redis sets...&quot;
redis-cli -h 127.0.0.1 -p 6379 del pc_images &gt; /dev/null 2&gt;&amp;1
redis-cli -h 127.0.0.1 -p 6379 del mobile_images &gt; /dev/null 2&gt;&amp;1

# 从txt文件中读取链接并存储到Redis
echo &quot;Populating Redis with image URLs...&quot;
while IFS= read -r line || [ -n &quot;$line&quot; ]; do
    redis-cli -h 127.0.0.1 -p 6379 sAdd pc_images &quot;$line&quot; &gt; /dev/null 2&gt;&amp;1
done &lt; &quot;$FINAL_PC&quot;

while IFS= read -r line || [ -n &quot;$line&quot; ]; do
    redis-cli -h 127.0.0.1 -p 6379 sAdd mobile_images &quot;$line&quot; &gt; /dev/null 2&gt;&amp;1
done &lt; &quot;$FINAL_MOBILE&quot;

echo &quot;Processing complete.&quot;

API 主页面:

<?php
require_once &#039;/file path/autoload.php&#039;;

//通过获取 User-Agent 判断设备类型
$userAgent = $_SERVER[&#039;HTTP_USER_AGENT&#039;];
$isMobile = preg_match(&#039;/(android|iphone|ipad|ipod|blackberry|windows phone)/i&#039;, $userAgent);

// 选择 txt 文件路径(自适应返回图片)
$txtFilePath = $isMobile ? &#039;/file path/mobile.txt&#039; : &#039;/file path/pc.txt&#039;;

// 尝试从 Redis 获取图片链接
$redis = new Redis();
$redis-&gt;connect(&#039;127.0.0.1&#039;, 6379); // 根据你的 Redis 服务器地址和端口进行调整

$key = $isMobile ? &#039;mobile_images&#039; : &#039;pc_images&#039;;
$randomUrl = $redis-&gt;sRandMember($key);

// 如果从 Redis 获取失败,则从 txt 文件中读取
if ($randomUrl === false) {
    // 读取 txt 文件中的 URL 并随机选择一个
    $urls = file($txtFilePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    if (!empty($urls)) {
        $randomUrl = $urls[array_rand($urls)];
    } else {
        header(&#039;Content-Type: text/plain&#039;);
        echo &quot;No images available.&quot;;
        exit;
    }
}

// 获取图片内容
$imageContent = file_get_contents($randomUrl);

// 检查是否成功获取图片内容
if ($imageContent === false) {
    header(&#039;Content-Type: text/plain&#039;);
    echo &quot;Failed to retrieve image content.&quot;;
    exit;
}

// 获取图片的 MIME 类型
$imageInfo = getimagesize($randomUrl);
$mime = $imageInfo[&#039;mime&#039;] ?? &#039;application/octet-stream&#039;; // 如果无法获取 MIME 类型,则默认为 application/octet-stream

// 输出图片内容
header(&#039;Content-Type: &#039; . $mime);
echo $imageContent;

计划任务命令(定时更新链接):

/usr/bin/php /file path/get-url.php &amp;&amp; /file path/url.sh

优化

当然,上面的代码中还有一些之前没有提到的部分,这是后来做的一点额外改进。此外,还有一些我计划想要加入的优化,现在还没有添加进去,也一并写出来,有能力的读者可以自己实现。

性能优化

在最开始设计的处理流程中,我把图片的调用链接存储在 API 服务器上的 txt 文件里,相比直接把图片存在服务器里,读取负担已经小了很多,但这是不是就是最好的方法了呢?

我们知道,在更大型的项目中,对于那些需要高频读写的文件,我们一般会把它放在缓存中,比起受限于 I/O 的硬盘读写,缓存读写的性能要高不止一个数量级,所以,我们为什么不采用这种方式来存储链接呢?

可以看到,在上面的代码中,我使用了基于 Redis 的服务器缓存来实现这一目标,获取链接后,先将其写在 txt 文件中,随后再存进 Redis 缓存里,之后直接从缓存里读取链接,仅在缓存被清理或出现问题时,才会从 txt 中读取,从而能够提高整体的响应速度。

各位可以回顾 API 的整个处理逻辑,我相信其中的每一步都还有再优化的空间,本文提到的部分只能算是抛砖引玉,有能力的读者可以尝试设计自己的改进方案。

处理逻辑优化

首先要说的是,我的代码其实缺了一个很重要的部分——错误处理。

是的,这本是设计之初就应该考虑的重要问题,我却不知为何直接略过了这一部分,希望各位以我为戒😣。

如果 API 因为某种原因无法读取到任何链接,它应该如何回应调用请求?如果链接是无效链接,或者并不包含图片内容,又该如何处理?这些都是应该要考虑的问题。

除此之外,各位也可以尝试做一些功能性的改进,例如,有没有可能在调用时附带一些参数,指定需要的图片的宽高比?或者让 API 不返回图片本身,而是返回图片的 exif 信息?

总而言之,这个项目虽然小,但有很多发挥的空间和改进的余地,很适合作为新手搭建在线业务的练习,建议各位自己上手玩玩看,也许从此就踏入了一方新天地也说不定(笑)。

本文作者: kaiki
本文链接: https://www.zbf1009.top/archives/287
版权声明: 本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 4.0 许可协议。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇