获取微软必应每日背景图
介绍
微软必应(Microsoft Bing
)首页每日都会更新一张背景图,看起来非常漂亮。
现在有个需求,需要把博客首页图片设置为和微软必应类似,每日更新为最新的背景图片。
思考 像图片这种静态资源,大公司肯定有CDN
加速的,所以我们就没必要从其他地方访问资源,直接引用微软必应墙纸图片的URL
即可。
项目地址:Github bingwallpaper 项目效果:获取今日微软bing壁纸
查看每日图片地址
检查元素获取
- 打开浏览器,访问https://cn.bing.com/
- 右键选择
检查
页面元素。
<div class="hp_top_cover" style="background-image: url("https://s.cn.bing.net/th?id=OHR.CadizSpain_ZH-CN0032172399_1920x1080.webp&qlt=50"); opacity: ; display: block;"><div class="hp_top_cover_dim" style="opacity: 0;"></div></div>
可以看到图片的地址是https://s.cn.bing.net/th?id=OHR.CadizSpain_ZH-CN0032172399_1920x1080.webp&qlt=50
。
接口获取
手动获取局限性比较大,我们不可能每天都去获取一下图片的地址,然后再更新到我们的博客中。
还好我们可以使用get
请求接口https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1
获取最近的图片。
接口中的参数:
format
:返回的数据格式,js
表示返回json
格式。idx
:返回的图片索引,0
表示返回最新的图片,1
表示昨天的图片。n
:返回的图片数量,1
表示返回最新一张图片,1
表示返回最新两张图片(即今天和昨天的图片)。
接口响应如下:
{
"images": [
{
"startdate": "20250113",
"fullstartdate": "202501131600",
"enddate": "20250114",
"url": "/th?id=OHR.CadizSpain_ZH-CN0032172399_1920x1080.jpg&rf=LaDigue_1920x1080.jpg&pid=hp",
"urlbase": "/th?id=OHR.CadizSpain_ZH-CN0032172399",
"copyright": "Village of Zahara de la Sierra, Cádiz province, Spain (© SEN LI/Getty Images)",
"copyrightlink": "https://www.bing.com/search?q=%E8%90%A8%E9%98%BF%E6%8B%89%C2%B7%E5%BE%B7%E6%8B%89%E8%B0%A2%E6%8B%89&form=hpcapt&mkt=zh-cn",
"title": "宁静之地",
"quiz": "/search?q=Bing+homepage+quiz&filters=WQOskey:%22HPQuiz_20250113_CadizSpain%22&FORM=HPQUIZ",
"wp": true,
"hsh": "281d51d6434aba29274dbf3434ddf2c2",
"drk": 1,
"top": 1,
"bot": 1,
"hs": []
}
],
"tooltips": {
"loading": "正在加载...",
"previous": "上一个图像",
"next": "下一个图像",
"walle": "此图片不能下载用作壁纸。",
"walls": "下载今日美图。仅限用作桌面壁纸。"
}
}
其中url
字段就是图片的地址。在前面加上域名,https://cn.bing.com
或https://s.cn.bing.net
就是图片的地址。
如上面示例中的图片地址为:https://cn.bing.com/th?id=OHR.CadizSpain_ZH-CN0032172399_1920x1080.jpg&rf=LaDigue_1920x1080.jpg&pid=hp
或https://s.cn.bing.net/th?id=OHR.CadizSpain_ZH-CN0032172399_1920x1080.jpg&rf=LaDigue_1920x1080.jpg&pid=hp
。
自定义接口做转发
上面的https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1
接口虽然能获取最近的图片信息。但是我们需要的是一个图片的url
。我们又不能每天手动去获取地址再更新到博客中。我们需要能自动获取到最新的图片地址,我们可以写一个接口来获取最新的图片,然后重定向到最新的图片地址。
我们只需要每天显示最新的图片即可,在接口中我们请求https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1
地址,获取到最新的图片地址,然后重定向到最新的图片地址即可。
提示 使用了接口重定向之后,我们还可以在接口中添加其他逻辑,比如:重定向到一周之内的一个随机图片,或者重定向到指定日期(比如去年同期)的图片等。这样可操作空间就比较大了。
实现
这里我选用Python
的fastapi
框架来实现接口。项目框架直接使用我之前写的其他项目框架改造而来。对于实现这个项目来说有些冗余,但是具有很好的衍生性。
思路
实现的思路是,GET
请求接口/wallpaper/bing/last
。先查询Redis
中是否有缓存,如果有就直接重定向到缓存里对应的图片地址,如果没有缓存就去获取最新的图片地址,然后保存到Redis
中,并重定向到最新的图片地址。
部署的测试接口获取今日微软bing壁纸
接口里面还可以实现很多逻辑,比如:
- 随机返回一张,最近一周或几天的图片
- 随机返回一张,今年、去年等同年同月同日的图片
- 随机返回一张,你喜欢的图片
- 甚至可以做到,通过用户名称、喜好匹配一张它可能喜欢的图片。(这里需要接入用户信息,匹配可以通过
AI
系统来完成)
提示 没有做不到,只有想不到,更多的思路需要我们自己去发掘。
目录结构
项目结构如下:
$ tree -I "venv"
.
├── Dockerfile
├── LICENSE
├── README.md
├── app
│ ├── __init__.py
│ ├── config
│ │ ├── __init__.py
│ │ ├── app_config.py
│ │ └── validate_template_config.py
│ ├── errors
│ │ ├── __init__.py
│ │ ├── app_error.py
│ │ ├── http_error.py
│ │ └── validation_error.py
│ ├── middleware
│ │ ├── __init__.py
│ │ └── usetime_middleware.py
│ ├── router
│ │ ├── __init__.py
│ │ └── wallpaper
│ │ ├── __init__.py
│ │ └── api.py
│ ├── server
│ │ ├── __init__.py
│ │ ├── app_fastapi.py
│ │ └── app_server.py
│ ├── types
│ │ ├── __init__.py
│ │ ├── enum_types.py
│ │ ├── redis_types.py
│ │ ├── response
│ │ │ ├── __init__.py
│ │ │ └── http_response.py
│ │ └── wallpaper_types.py
│ └── utils
│ ├── __init__.py
│ ├── redis_util.py
│ └── time_util.py
├── gunicorn_config.py
├── hooks
│ └── auto-requirements.sh
├── logs
│ ├── log_2025-01-17.log
│ └── log_2025-01-18.log
├── main.py
└── requirements.txt
13 directories, 34 files
这里忽略了虚拟环境venv
目录。我们只需要关注具体的实现文件app/router/wallpaper/api.py
即可,其他基本都是项目的支持文件。
核心代码
app/router/wallpaper/api.py
文件内容如下:
'''
Author: matiastang
Date: 2024-04-22 10:19:59
LastEditors: matiastang
LastEditTime: 2025-01-17 12:14:03
FilePath: /bingwallpaper/app/router/wallpaper/api.py
Description: bing壁纸
'''
import json
import requests
from loguru import logger
from fastapi import APIRouter
from starlette.responses import RedirectResponse
from app.types.response.http_response import HttpResponse, ResponseFail
from app.types.wallpaper_types import BingWallpaperImage
from app.types.enum_types import TimezoneName
from app.utils import redis_util, time_util
# 缓存key
WALLPAPER_BING_REDIS_KEY = 'last'
# 路由
router = APIRouter(
prefix="/wallpaper/bing",
tags=["wallpaper.bing"],
)
async def get_bing_wallpaper(idx: int = 0, num: int = 1):
"""
获取bingwallpaper的壁纸信息
Args:
idx (int, optional): 图片索引. Defaults to 0.
num (int, optional): 图片数量. Defaults to 1.
Returns:
list[BingWallpaperImage]: 图片列表
"""
# 目标URL
url = 'https://cn.bing.com/HPImageArchive.aspx'
# 请求头
params = {
'format': 'js',
'idx': idx,
'n': num,
}
# 发送GET请求
res = requests.get(url, params=params)
# 打印响应内容
data = res.json()
images = data.get('images', [])
return [BingWallpaperImage(**image) for image in images]
@router.get('/last')
async def get_bing_wallpaper_last() -> HttpResponse:
""" 获取bing的最新一张壁纸 """
info = await redis_util.get_cache(WALLPAPER_BING_REDIS_KEY)
if info:
try:
images = json.loads(info)
if isinstance(images, list) and len(images) > 0:
wallpapers = [BingWallpaperImage(**image) for image in images]
first_wallpapers = wallpapers[0]
wallpaper_url = first_wallpapers.url
url = f"https://cn.bing.com{wallpaper_url}"
return RedirectResponse(url=url, status_code=302)
except Exception as e:
logger.error('-' * 10 + '[LAST]解析缓存壁纸信息失败' + '-' * 10)
logger.error(e)
wallpapers = await get_bing_wallpaper()
if len(wallpapers) > 0:
try:
json_str = json.dumps([wallpaper.model_dump() for wallpaper in wallpapers])
ex = time_util.get_remaining_seconds_in_day(TimezoneName.SHANGHAI)
status = await redis_util.set_cache(WALLPAPER_BING_REDIS_KEY, json_str, ex)
if not status:
logger.error('-' * 10 + '[LAST]壁纸缓存失败' + '-' * 10)
first_wallpapers = wallpapers[0]
wallpaper_url = first_wallpapers.url
url = f"https://cn.bing.com{wallpaper_url}"
return RedirectResponse(url=url, status_code=302)
except Exception as e:
logger.error('-' * 10 + '[LAST]处理壁纸结果失败' + '-' * 10)
logger.error(e)
return ResponseFail('获取壁纸失败', 500)
app/types/enum_types.py
文件内容如下:
'''
Author: matiastang
Date: 2025-01-17 12:09:15
LastEditors: matiastang
LastEditTime: 2025-01-17 12:11:24
FilePath: /bingwallpaper/app/types/enum_types.py
Description: 枚举
'''
from enum import Enum
class TimezoneName(Enum):
"""时区名称"""
UTC = 'UTC'
SHANGHAI = 'Asia/Shanghai'
"""上海"""
app/utils/time_util.py
文件内容如下:
#!/Users/matias/matias/MT/MTGithub/bingwallpaper/venv/bin/python3
# coding=utf-8
'''
Author: matiastang
Date: 2025-01-17 11:26:02
LastEditors: matiastang
LastEditTime: 2025-01-17 12:55:38
FilePath: /bingwallpaper/app/utils/time_util.py
Description: 时间
'''
import pytz
from datetime import datetime
from app.types.enum_types import TimezoneName
def get_remaining_seconds_in_day(timezone_name: TimezoneName = TimezoneName.UTC) -> int:
"""
获取指定时区当天剩余的时间(秒数)
Args:
timezone_name (str): 时区名称(如 "Asia/Shanghai", "UTC")
Raises:
ValueError: 未知时区
Returns:
int: 当天剩余时间的秒数
"""
try:
# 获取指定时区
timezone = pytz.timezone(timezone_name.value)
# 当前时区时间
# now = datetime.now(timezone)
now = datetime.now()
# 当天结束时间(午夜)
# end_of_day = datetime(now.year, now.month, now.day, 23, 59, 59, tzinfo=timezone)
end_of_day = datetime(now.year, now.month, now.day, 23, 59, 59)
end_of_day_test = datetime(now.year, now.month, now.day, 23, 59, 59, tzinfo=timezone)
# 转换时间
now_time_tz = timezone.localize(now)
end_of_day_time_tz = timezone.localize(end_of_day)
print(end_of_day, end_of_day_test, end_of_day_time_tz)
# 计算剩余时间
print(end_of_day, now)
print(end_of_day_time_tz, now_time_tz)
# remaining_time = end_of_day - now
remaining_time = end_of_day_time_tz - now_time_tz
return int(remaining_time.total_seconds())
except pytz.UnknownTimeZoneError:
raise ValueError(f"未知时区: {timezone_name}")
if __name__ == "__main__":
"""
测试
python3 -m app.utils.time_util
"""
print(get_remaining_seconds_in_day(TimezoneName.SHANGHAI))
注意 这里有个细节,就是Redis
的缓存时间,我这里是取的当天剩余的时间,这样就会同时更新。(这里时区默认使用的东八区,我也默认认为https://cn.bing.com/HPImageArchive.aspx
接口获取的图片是每天0点更新,如果不是后面可以调整逻辑)
完整项目代码可以查看Github bingwallpaper
思考
这里使用了一个框架实现一个简单的接口,想想都觉得麻烦。其实在做这个项目之前,我就在想这种简单的处理,应该通过Nginx
的配置加上脚本就能实现,我也去查了一下,暂时得出的结论是可以的,但我没有试过,等后面有空了再试一下。
Nginx
可以通过 cgi-bin
配置运行简单的脚本(例如 Shell
或 Python
脚本)。不过,这需要使用第三方模块如 fcgiwrap
,并不常用。
既然感觉有更简单的方式,那我为什么还要用FastAPI
这个相对复杂一点儿的框架来做呢?主要还是想对之前做FastAPI
项目做一下简单的整理,记录一下。