写 Python 脚本自动下载壁纸,这件事我最早是手动干的:每天打开壁纸站,点开“4K”分类,挑两张顺眼的右键另存为,再用看图工具统一改分辨率,壁纸稍微多几台设备就得重复折腾一遍。后来实在烦了,干脆写了个脚本,把“找图、下载、重命名、去重”整个链路自动化,跑一次就能攒下几十张图。这篇文章就聊聊这个脚本从需求拆解到落地踩坑的完整过程,适合对自动化脚本感兴趣、又不想天天手动存图的人参考。
1. 先说清楚:这个脚本到底解决什么问题
1.1 为什么手动下壁纸让人崩溃
壁纸这个需求听起来小,真正手动攒过图的人都知道有多磨人。搜图要逐页翻,预览图和大图经常不是同一个地址,下载时文件名又五花八门,.jpg、.png、.webp混在一起。更难受的是同一张图换个平台名字就变了,本地磁盘里存了一堆重复文件,往往等到硬盘塞满才发现占空间的其实是同一张图的不同版本。
手动下载的问题本质是“重复劳动没有沉淀价值”。每次打开网站、右键另存、新建文件夹、重命名,这些动作固定在浪费生命。写脚本的核心目标不是去跟某个网站较劲,而是把这一整套动作抽成可复用的流程:给定一个搜索关键词或分类页,脚本自己去解析网页、提取图片地址、按规则保存到本地。
1.2 适合谁?脚本要满足哪些需求
在我的经验里,这类脚本最适合三种人。第一种是像我一样有大量壁纸收集需求的人,需要定时换一批图;第二种是做内容素材整理的编辑或设计师,需要按主题批量抓图;第三种是刚学 Python 的初学者,想通过一个“看得见效果”的项目理解 requests、BeautifulSoup、文件读写这些基本功。
既然目标人群里有小白,脚本就不能只给自己用。我的要求比较明确:
- 输入一个网址就能开始跑,不需要改太多配置。
- 下载的图片能自动分类保存,比如按日期或者分类建子目录。
- 重复下载同一批图片时不会覆盖已有文件。
- 中间失败的网络请求能够自动重试,不至于跑一半就中断。
- 最后能看到统计信息:成功几张、跳过几张、失败几张。
这些需求决定了脚本不能只是“能跑”,还得面对真实网络环境里的各种幺蛾子。
2. 整体设计:选源、抓取与下载的取舍
2.1 壁纸源怎么选:API优先,其次静态页面
写下载脚本第一个问题不是怎么写代码,而是从哪里下。壁纸源选错,后面所有代码都是浪费时间。我试过几类来源,经验是按优先级排序:
| 来源类型 | 优点 | 缺点 | 推荐程度 |
|---|---|---|---|
| 官方 API | 结构清晰、字段稳定 | 部分需要 token、有次数限制 | 最推荐 |
| 公开 JSON/embed 页面 | 直接能拿数据,不用解析 HTML | 很少见,依赖特定站点 | 推荐 |
| 静态 HTML 页面 | 无需认证,门槛低 | 页面结构易变,需要维护选择器 | 可用 |
| 图片直链聚合页 | 简单直接 | 质量参差、防盗链严重 | 不推荐 |
我常用的是支持关键词检索的公共 API 站点,直接请求一个接口拿到 JSON,再从中提取图片 URL,解析成本几乎为零。但考虑到有些朋友只是想练手,或者想抓某个具体站点的分类页,静态页面方案也不能绕开。
我的建议是:把“抓取源”抽象成一个可以替换的函数,内部返回统一格式的图片地址列表。这样今天写死一个网站,明天想换另一个源,只需要改动一小块函数。
2.2 抓取策略:先列表页再详情页,而不是直接扫全站
很多论坛社区的抓图脚本容易写成“从第一页爬到最后一页,见图片就下载”。这种思路在小型站点可行,在稍大一点的壁纸站就很容易触发限制,而且会下载大量低质量缩略图。
正确的姿势是分两层。第一层先抓列表页,列表页里通常只包含缩略图和详情页链接;第二层进入详情页,从详情页里提取原图的真实地址。这样看起来多了一步请求,但换来的是更准确的图片质量。
举个例子,列表页里图片地址可能长这样:https://example.com/thumbs/12345.jpg,只有几百像素宽。详情页里的真实地址可能是https://example.com/wallpapers/12345_4k.jpg。直接抓缩略图图省事,但下载到本地你会后悔。
所以我在设计脚本时,把“解析列表页”和“解析详情页”写成两个函数,列表页函数返回一组 dict,包含详情页链接和图片标题;详情页函数再根据链接返回原图地址。这样的结构让调试变得很舒服。
2.3 下载策略:命名、去重、断点续传
下载看起来最简单,其实最容易踩坑。图片 URL 里的文件名往往是一长串数字,直接存下来,下次整理时根本不知道哪张是哪张。
我的命名规则是:日期_分类_序号_原文件名。比如20250601_city_001_8291233.jpg。这样按名称排序时天然按日期归拢,查找也方便。
去重方面,我维护了一个简单的历史名单。下载前先检查目标文件名是否存在于本地,存在就跳过;存在但文件体积为 0,则删除后重新下载。另外,对已经下载的文件也可以做一次哈希校验,把相同 MD5 的重复文件合并,不过这一步初期可以不做,先保证不重复覆盖。
断点续传策略是重点。下载图片时不要一次性把整个文件读进内存,而是用流式请求,按块写入磁盘,并在临时文件后缀加.part。下载完成后把.part改名成正式文件。如果中途失败,下次可以直接删除.part重新下载,至少不会把坏文件当成好文件。
3. 核心实现细节:用到的库、代码与关键参数
3.1 环境准备:Python版本与依赖
整个项目基于 Python 3.8+ 写的,在 3.10 和 3.11 上实测都没问题。主要用到三个库:requests负责发 HTTP 请求,beautifulsoup4负责解析 HTML,lxml作为 BeautifulSoup 的解析引擎。
pip install requests beautifulsoup4 lxml如果你还没把 Python 加入系统环境变量,直接敲这条命令很容易遇到pip : 无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称这类报错。这时候别急着重装 Python,先打开系统环境变量,把 Python 安装目录和它的Scripts子目录加进Path,再新开一个终端窗口试试pip --version。这个报错我在 Windows 上碰到太多次了,基本就是这个原因。
3.2 三步抓取:请求头、解析、匹配
抓取的第一步是让服务器觉得“你是个正常人”。直接用 Python 默认的 User-Agent,很多壁纸站会直接返回 403。我的请求头一般这样设置:
headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36", "Accept-Language": "zh-CN,zh;q=0.9", }第二步是解析。BeautifulSoup 的用法很直白,从soup.find到select_one都行。我在代码里会优先用 CSS 选择器,因为结构变化时只需要改一行字符串。
第三步是匹配真实图片地址。壁纸站经常搞懒加载,列表页里img标签的src是占位图,真实地址藏在>import os import re import time import requests from bs4 import BeautifulSoup HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36", } BASE_URL = "https://example-wallpaper-site.com/category/city" SAVE_DIR = "wallpapers" MAX_PAGES = 3 TIMEOUT = 10 def get_soup(url): resp = requests.get(url, headers=HEADERS, timeout=TIMEOUT) resp.raise_for_status() resp.encoding = resp.apparent_encoding return BeautifulSoup(resp.text, "lxml") def extract_image_urls(soup): urls = [] for a in soup.select("a img"): src_candidates = [ a.get("src"), a.get("data-src"), a.get("data-original"), a.get("data-url"), ] for src in src_candidates: if src and re.search(r"\.(jpg|jpeg|png|webp)(\?|$)", src, re.I): urls.append(src) break return urls def download_image(url, referer, save_path): headers = HEADERS.copy() headers["Referer"] = referer resp = requests.get(url, headers=headers, timeout=TIMEOUT, stream=True) resp.raise_for_status() tmp_path = save_path + ".part" with open(tmp_path, "wb") as f: for chunk in resp.iter_content(chunk_size=8192): if chunk: f.write(chunk) if os.path.getsize(tmp_path) > 0: os.rename(tmp_path, save_path) return True return False def main(): os.makedirs(SAVE_DIR, exist_ok=True) downloaded = 0 for page in range(1, MAX_PAGES + 1): page_url = f"{BASE_URL}?page={page}" print(f"正在处理第 {page} 页: {page_url}") soup = get_soup(page_url) img_urls = extract_image_urls(soup) print(f"发现 {len(img_urls)} 张图片") for idx, img_url in enumerate(img_urls, 1): if not img_url.startswith("http"): img_url = requests.compat.urljoin(page_url, img_url) filename = os.path.basename(img_url.split("?")[0]) save_path = os.path.join(SAVE_DIR, f"{time.strftime('%Y%m%d')}_{page}_{idx}_{filename}") if os.path.exists(save_path): print(f"跳过已存在: {filename}") continue try: ok = download_image(img_url, page_url, save_path) if ok: downloaded += 1 print(f"下载成功: {filename}") except Exception as e: print(f"下载失败: {filename}, 错误: {e}") time.sleep(0.5) print(f"本次共下载 {downloaded} 张图片") if __name__ == "__main__": main()
4.2 关键函数逐段说明
extract_image_urls是整个脚本能否抓对图的核心。我把它单独抽出来,因为它最常需要改动。不同站点的图片标签不同,有的站在a上面套一层div,有的在picture标签里嵌套多个source。抽成函数后调试时可以直接在命令行里测试一段选择器,不需要跑完整流程。
download_image里有两个设计要点。第一是stream=True,图片下载是 IO 密集任务,没必要一次性把整个响应读进内存,分块写盘对大图更友好。第二是.part临时文件机制,这个习惯来自我早期下载大文件经常下到一半断掉,结果留下一个“看似能打开但其实是残缺”的图片,临时文件方案能避免这种尴尬。
主流程里我加了time.sleep(0.5)来控制请求频率。很多新人会觉得这是没用的延迟,实际上这是防止被服务器限流的重要手段。对目标站点保持礼貌,比歇斯底里地并发狂下更持久。
5. 实际运行中踩过的坑与排查实录
5.1 pip 识别不了?先把环境变量理顺
文章第二部分提过一次 pip 报错,这里再展开讲。很多朋友刚装完 Python 就兴冲冲打开终端,敲pip install requests,结果 PowerShell 直接丢出来“无法将 pip 项识别为 cmdlet、函数、脚本文件或可运行程序的名称”。大多数人第一反应是卸载重装,没必要。
打开“系统属性 - 环境变量”,在Path里追加两条:
C:\Users\你的用户名\AppData\Local\Programs\Python\Python311\ C:\Users\你的用户名\AppData\Local\Programs\Python\Python311\Scripts\保存后重新打开终端。如果还是不行,再检查是否存在python.exe和pip.exe这两个文件。有时候只装了 Python,但没有装 pip,可以用python -m ensurepip --upgrade补上。
另外一个隐藏很深的坑:如果你的电脑装了多个 Python 版本,pip可能指向旧版本,而你的项目需要新版本才有的特性。这时候不要直接敲pip,改用python -m pip,这样能保证 pip 跟当前运行的 Python 是同一个环境。
5.2 SSL证书报错、请求超时怎么处理
运行脚本我遇到过最常见的报错是requests.exceptions.SSLError: HTTPSConnectionPool(host='...', port=443): Max retries exceeded with url: ... (Caused by SSLError("... certificate verify failed ..."))。
这个报错不一定是证书真的有问题,更多是目标站点的证书链不完整,或者本地没有安装对应的 CA 证书。牺牲一下安全级别,在请求时加verify=False,并屏蔽掉警告:
import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) requests.get(url, headers=HEADERS, verify=False)同时还要给请求设置超时。我在代码里写的TIMEOUT = 10不是拍脑袋,而是基于实测:超过 10 秒没能建立连接,大概率是网络问题或站点限制,再等下去只会让脚本卡死。如果连续失败三次,脚本应该停止当前下载,不要无限重试。
5.3 图片重名、0字节文件的应对
重名问题最容易出现在用原文件名保存的时候。同一个数字编号的图片在不同分类下会出现,或者只改了图片格式后缀。我习惯在文件名里拼上“页码_序号”,这样基本可以避免冲突。
0 字节文件是另一个隐蔽问题。服务器可能返回了 200 状态码,但实际内容是空的,或者反爬机制给你返回一个空文件。如果写完文件后直接改名,本地就多了一个废文件。所以在下载函数里我加了文件大小检查,小于 1KB 的如果不是 GIF 这类特殊格式,直接视为异常,把临时文件删掉重来。
6. 让脚本更像“产品”:进阶优化玩法
6.1 并发下载提速
单个下载加 0.5 秒延迟,下 50 张图就得等 25 秒。如果图源稳定,可以改成线程池来提速。Python 自带concurrent.futures.ThreadPoolExecutor,代码改动很小。
from concurrent.futures import ThreadPoolExecutor def safe_download(item): url, page_url, save_path = item try: return download_image(url, page_url, save_path) except Exception as e: print(f"下载失败: {e}") return False with ThreadPoolExecutor(max_workers=8) as executor: tasks = [(img_url, page_url, save_path) for img_url, save_path in ...] results = list(executor.map(safe_download, tasks))并发下载时更要注意不要一次开几十个线程,否则轻则被服务器封 IP,重则把你自己的带宽打满。我习惯控制在 5 到 8 个并发,稳定性和速度比较平衡。
6.2 定时任务实现壁纸自动更新
脚本写好后,我给它配了定时任务,每周自动跑一次。Windows 上可以用“任务计划程序”,新建一个基本任务,触发条件选“每周”,操作选“启动程序”,程序填python.exe的完整路径,参数填脚本完整路径。
Linux 或 macOS 更简单,直接用 crontab:
# 每周一早上 8 点执行下载 0 8 * * 1 /usr/bin/python3 /home/user/wallpaper_downloader.py定时任务跑起来之后,脚本的日志输出很重要。我在代码里只用了print,但实际挂机运行时建议把输出重定向到日志文件,不然出错时根本不知道哪一步挂了。最省事的方式是给print加上时间戳,然后让系统重定向输出。
6.3 从固定源换成自定义配置
脚本里最不应该写死的部分是BASE_URL和保存规则。我后来把配置抽到config.py里:
# config.py SAVE_DIR = "wallpapers" MAX_PAGES = 5 DOWNLOAD_CONCURRENCY = 8 REQUEST_INTERVAL = 0.5 SOURCE_URL = "https://example-wallpaper-site.com/category/city"这样换源的时候只改配置,不动主逻辑。更进一步的思路是把解析规则也做成配置项,比如“图片标签的 CSS 选择器”“原图地址所在的属性名”。这样做的好处是,同一个脚本可以适配多个站点,不用为每个站点复制一份代码。
我自己在实际操作里的体会是:这类脚本不要一开始就追求大而全,先跑通一个最小闭环,再逐步加并发、加定时、加配置化,比闷头写一整天更靠谱。每加一个功能,都要跑一次真实下载验证,否则很容易出现“改完代码,旧图全废”的尴尬。最后再分享一个小技巧:下载完成后,抽几个样本文件用PIL打开看一下尺寸和格式,能最快发现解析规则是否出错,省得几百张图全下回来才发现全都是缩略图。