ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python壁纸批量下载:从接口抓取到自动整理全攻略

Python壁纸批量下载:从接口抓取到自动整理全攻略 我最早写这个脚本是因为每次换电脑都要重新找一遍壁纸。系统自带的那几张图翻来覆去很容易看腻去图站上找图很爽但一张张右键另存为太折磨人尤其想攒个几百张的素材库一套流程走下来半小时就没了。后来我把这件事彻底交给了程序先通过接口拿到壁纸直链再写一个Python脚本自动下载壁纸到本地几轮跑下来就能攒出一个像样的图库。整个过程不涉及什么复杂算法就是上网请求、参数拼接、文件落盘这些基础操作但对高频换壁纸的人、做桌面素材管理的人还有想在服务器上搭个简单素材库的人来说都非常实用。下面这份方案以 Python 3.11 为基准核心依赖只需要 requests 和 Pillow适合直接抄走改一改就能用。我会把选型理由、接口抽象、翻页、去重、重试、定时执行和常见坑都串起来讲不光是贴代码重点是让你知道每一步为什么这么设计。1. 动手之前先把需求盘明白1.1 手动存图和程序下载差的不只是速度手动找图的优势是“人眼审美”你能一眼看到构图、色彩、是否适合当桌面。但它的劣势也很明显重复劳动多、跨平台同步麻烦、想按关键词批量收集时效率极低。程序下载刚好相反它不懂审美但它特别擅长“批量、定时、稳定”这三件事。只要你给它一个图片地址列表它就能按你设定的规则把文件抓回来还能自动跳过重复图、自动按尺寸分类。所以写这个脚本前你先要想清楚一个问题你的壁纸需求是“每天看几张新的”还是“定期攒一批高质量的”。前者其实不用写脚本后者才有必要。另外程序下载还有一个隐藏价值可复现。手动存图存完就结束了但脚本可以每天定时跑等于给自己建了一条持续更新的壁纸流水线。今天跑了明天还能跑换一台机器改个配置就能继续用。1.2 技术栈怎么选requests、httpx 还是 selenium做爬取/下载类脚本第一个纠结通常是用 requests 还是 selenium。我的建议很直接能用 requests 解决的问题就不要上 selenium。先看一个对比方案适用场景优点缺点requests 接口目标站提供 JSON/HTML 接口轻量、稳定、速度快遇到重 JS 渲染页面会比较吃力httpx同上支持 HTTP/2异步选项更丰富生态比 requests 略小很多功能要求新版selenium / playwright页面内容全靠 JS 动态渲染能模拟真实浏览器内存占用大启动慢维护成本高urllib 标准库简单请求零依赖语法繁琐封装度不够不建议日常用我在绝大多数壁纸源上只用了 requests。原因很简单壁纸网站为了给前端页面喂数据通常会提供接口地址返回的是 JSON字段里直接带着图片直链和分辨率。接口拿不到的页面再考虑用浏览器自动化但那些站点往往也是反爬最明显的对个人脚本并不友好。给新手的一个建议是第一次先打开浏览器开发者工具切到网络面板随便点一页壁纸列表搜索 JSON 类型的请求看看里面是不是已经有图片地址。如果有你就根本不需要去解析 HTML更不需要上 selenium。2. 数据源层的抽象让自己不被单一网站绑死2.1 什么样的数据源适合写下载脚本很多教程一上来就教你爬某个具体网站这当然能跑通但坏处是网站改一次页面结构你的脚本就废一片。我更倾向于先选数据源再写一层抽象。适合脚本化下载的壁纸源一般满足四个条件有稳定的 JSON 接口或者页面结构足够规整能批量提取图片地址。图片直链用的是 CDN 地址下载速度稳定不会一会儿超时一会儿断。接口返回的数据里包含分辨率、标签、ID 等信息方便后续去重和分类。站点对自动化下载相对友好没有明显禁止的热点检测。商业图库、付费素材站这类不建议碰。我们做这类脚本主要面向那些可以免费下载、允许个人使用的壁纸站。版权问题老生常谈我只说一句脚本本身没有原罪但下载下来的图最好只在个人设备上用别拿去二次售卖或做商业项目。2.2 用一个 Source 类把不同接口统一起来不同壁纸源的接口格式千差万别。有的源用关键词搜索有的源只提供热门榜单有的源返回的数据是嵌套结构有的源要带 API Key。如果你在业务代码里到处写具体的请求地址换一个源就要改一堆地方非常难受。我习惯的做法是给每个源单独写一个 Source 类对外统一提供一个fetch(keyword, page, per_page)方法。这样主流程根本不知道背后是哪个站点它只需要不断调用source.fetch(...)就行。下面是一个示意代码接口域名我用 example.com 占位你按自己选定的源去替换。# source.py import requests from dataclasses import dataclass dataclass class ImageItem: title: str url: str page_url: str width: int 0 height: int 0 md5: str class BaseSource: 所有壁纸源的基类子类实现 fetch 即可 def fetch(self, keyword: str, page: int, per_page: int) - list[ImageItem]: raise NotImplementedError class ExampleSource(BaseSource): 某个壁纸源的实现示例 def __init__(self, api_key: str ): self.base_url https://api.example.com/v1/wallpapers self.api_key api_key self.session requests.Session() self.session.headers.update({ User-Agent: Mozilla/5.0 (WallpaperDownloader/1.0) }) def fetch(self, keyword, page, per_page): params { q: keyword, page: page, per_page: per_page, } if self.api_key: params[key] self.api_key resp self.session.get(self.base_url, paramsparams, timeout10) resp.raise_for_status() data resp.json() items [] for item in data.get(items, []): items.append(ImageItem( titleitem.get(title, ), urlitem[url], page_urlitem.get(page_url, ), widthint(item.get(width) or 0), heightint(item.get(height) or 0), md5item.get(md5, ), )) return items很多新手会问为什么要多写一个 BaseSource直接某个类不就行了。区别在于当你第二个源、第三个源加进来的时候主流程的代码完全不用动只新增一个子类就行。这就是“面向接口编程”思想的最小实践对一个小脚本来说性价比很高。2.3 返回结果标准化一切交给 ImageItemImageItem是我对外统一返回的数据结构。它看起来只是一个 dataclass但它解决了好几个问题。第一个是字段混乱。有的源把分辨率写在width/height里有的写在resolution字符串里有的甚至只有缩略图的尺寸。所有源在进入主流程之前都必须被转换成统一的ImageItem后续代码就只用处理一种格式。第二个是信息保留。page_url很多人会嫌麻烦不存但实际跑的时候你会发现它非常有用。比如下载失败后你想看看原始页面确认图片是否还在或者想按图片来源做备份清单page_url都能帮上忙。md5字段也不是必须的但如果源本身返回了图片指纹用它做去重会比算文件哈希快得多。第三个是错误发现。如果一个源突然改了接口最常见的表现是item.get(url)返回空或者字段名变了。标准化的另一个好处就是只要解析层爆出异常你一眼就能知道是这个源的问题而不是主流程的问题。3. 核心下载逻辑翻页、去重、重试3.1 翻页抓列表一定要设上限壁纸站的分页接口一般长这样?page2per_page20。写翻页逻辑本身很简单一个for循环就能搞定但一定要设两个上限最大页数和最大图片数量。不然某次关键词匹配到了几千张图脚本会一直跑下去既占用带宽又把磁盘塞满。下面这段代码是收集图片列表的常用写法import time def collect_images(source, keyword, max_pages, per_page20): images [] for page in range(1, max_pages 1): items source.fetch(keyword, page, per_page) if not items: break images.extend(items) # 每页之间停一会儿避免对源站造成压力 time.sleep(0.5) # 到达数量上限就提前退出 if len(images) 500: break return images这里有两个小细节。第一time.sleep(0.5)看起来很不起眼但它是防止被封号的关键操作。很多站对短时间高频请求非常敏感哪怕你根本没恶意请求频率高了也会被临时限流。第二break条件一定要写接口返回空列表意味着没有更多数据了继续翻只会白白浪费时间。如果你要下载多个关键词的壁纸我建议在每个关键词之间也 sleep 一小段时间并且最好把每个关键词的图片数量控制在几十张以内。这样图库的多样性会好很多而不是某一天全被一个关键词刷屏。3.2 下载阶段的去重文件名去重与内容哈希壁纸站经常会出现同一张图出现在多个关键词、多个榜单里的情况所以去重是必须的。去重分两级文件名去重和内容去重。文件名去重最简单保存之前先检查目标路径是否存在。但这个名字可能本身就有问题比如标题里带了/、\、:这些非法字符Windows 上直接写入就会报错。我通常会先做一个安全化处理import os import re def safe_filename(title: str, max_len: int 80) - str: title re.sub(r[\\/:*?|\r\n], _, title) return title[:max_len].strip() or untitled仅仅文件名去重还不够因为同一个源里如果带 md5 字段我们优先用它去重。如果源不给 md5我会在下载完成后对文件内容做一次哈希并维护一个已见哈希集合。这一步能有效避免“标题不同但图片相同”的情况。实际做法是把哈希结果记录在一个本地清单文件里比如downloaded.txt每下载一张图就往里追加一行哈希。第一次运行可能感觉不到区别但跑到一个月之后增量下载时能节省非常多的重复传输。3.3 重试与限速稳定运行的底线下载壁纸看着简单但网络环境没那么理想超时、断流、证书错误都是家常便饭。你不能指望一次请求就成功所以重试和限速是脚本的底线能力。下面是我的下载函数模板亮点在于先下载到临时文件全部写完之后再做校验和重命名import os import time import requests def is_image_file(data: bytes) - bool: 通过文件头快速判断是不是常见图片格式 if data[:2] bBM: return True # BMP if data[:3] b\xff\xd8\xff: return True # JPEG if data[:8] b\x89PNG\r\n\x1a\n: return True # PNG if data[:4] bRIFF and data[8:12] bWEBP: return True # WebP return False def download_one(session, image, save_path, retries3): for attempt in range(retries): try: resp session.get(image.url, timeout(5, 30)) resp.raise_for_status() data resp.content if len(data) 10 * 1024: raise ValueError(文件太小疑似占位图或错误页) if not is_image_file(data): raise ValueError(内容不是图片文件) tmp_path save_path .part with open(tmp_path, wb) as f: f.write(data) os.replace(tmp_path, save_path) return True except Exception as e: if attempt retries - 1: print(f下载失败: {image.url} - {e}) return False time.sleep(2 * (attempt 1)) return False为什么要先写.part再重命名因为直接写最终文件时如果程序中途崩溃或者下载不完整会在壁纸目录里留下一堆损坏的半成品。先写临时文件全部校验通过后再os.replace原子替换就可以避免这个问题。限速这部分除了页与页之间 sleep我还会在下载时控制并发数量。宁愿慢一点也不要因为并发太高被源站拒绝。4. 从“跑一次”到“天天跑”配置、日志和定时任务4.1 把参数写进配置文件而不是改代码一个脚本如果只有自己能看懂那它只是半成品。我强烈建议把保存路径、关键词、每页数量、最大页数这些变量全部抽到一个配置文件里。我习惯用 JSON因为 Python 标准库就能解析不需要额外依赖。下面是我常用的配置结构{ save_dir: ~/Pictures/Wallpapers, keywords: [nature, minimal, city], per_page: 20, max_pages: 3, keep_latest: 500, source: example }keep_latest是保留最新多少张壁纸超过这个数量就清理旧文件。这个字段后面在磁盘管理那一节会用到。把参数放到配置文件的直接好处是你或者你的朋友换了个文件夹路径、想换个关键词只要改 JSON连脚本都不用打开。配置解析里有个小坑~/这种家目录符号在 Python 的os.path里不会自动展开。你需要用os.path.expanduserimport os save_dir os.path.expanduser(config[save_dir]) os.makedirs(save_dir, exist_okTrue)4.2 日志这么记出问题才能十分钟定位很多人喜欢用print调试临时跑一次没问题但脚本挂到定时任务里之后你根本看不到控制台输出。这时候就必须用标准库的logging把运行信息同时写到文件和终端。下面是我常用的日志初始化写法import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, handlers[ logging.FileHandler(wallpaper.log, encodingutf-8), logging.StreamHandler(), ] ) logger logging.getLogger(wallpaper) logger.info(开始抓取壁纸列表) logger.warning(某个页面返回空列表提前退出)关键是每条日志要带时间戳和等级。这样遇到问题时你能清楚地知道脚本是在哪一步失败的是列表抓取阶段还是图片下载阶段。如果你要长期挂着还可以用logging.handlers.RotatingFileHandler做日志轮转比如单个日志文件不超过 5MB超过就自动保留三个历史文件。否则日志文件会越攒越大本身也变成一种磁盘垃圾。4.3 定时执行Windows 和 Linux 各说一遍脚本写完之后最爽的用法是让它每天自动跑一次。Windows 上用“任务计划程序”就好搜schtasks或者直接打开图形界面创建基本任务触发器选每天操作选启动程序。命令行方式是这样schtasks /create /tn WallpaperFetch /tr python D:\scripts\run.py --cfg config.json /sc daily /st 06:00Linux 上最简单的是 crontab。先crontab -e再加一行0 6 * * * cd /home/user/wallpaper /usr/bin/python3 run.py --cfg config.json /var/log/wallpaper_cron.log 21这里注意两点。第一cd进去再用相对路径引用配置和日志文件避免脚本内部相对路径出错第二Python 解释器最好写绝对路径因为 cron 的环境变量比终端里少很多直接用python3可能找不到。还有一个小经验笔记本如果设置了合盖休眠定时任务不会在休眠期间执行而是等唤醒之后补跑。如果你想让脚本必须在凌晨固定时间跑最好让机器保持通电状态或者接受唤醒后补跑的行为。5. 真实上线后最容易翻车的四个问题5.1 文件名保存不了多半是非法字符和路径长度我第一次用标题直接当文件名的时候在 Windows 上遇到了OSError: [Errno 22] Invalid argument当时还以为是权限问题排查了半天发现是标题里带了一个冒号。Windows 的文件名不允许\ / : * ? |这九个字符而壁纸标题里经常出现这些符号。解法就是我前面提到的safe_filename函数先把非法字符替换成下划线再把长度截断。别忘了还有一个隐藏问题如果标题被截断了不同图片可能会撞名导致后面的覆盖前面的。所以更稳妥的命名方式是在文件名里加上尺寸或者 IDdef build_save_path(save_dir, image): name safe_filename(image.title) return os.path.join( save_dir, f{name}_{image.width}x{image.height}.jpg )5.2 403 Forbidden 但浏览器看得到UA、Referer 和 Cookie 的排查链路这是爬取类脚本最经典的坑。某天脚本突然大面积报 403但你用浏览器打开同一个图片地址明明能正常显示。这时候不要急着改代码按下面的链路排查。第一步确认是不是 IP 被封。用浏览器的无痕模式重新访问一下如果能打开说明服务端并没有封你的网络出口问题出在请求身份上。第二步检查 User-Agent。很多站的图片防盗链逻辑很直接看到非浏览器 UA 就拒绝。最简单的验证方式是把请求头里的 User-Agent 换成你当前浏览器的完整 UA 字符串再跑一次。第三步如果还报 403检查 Referer。图片 CDN 通常会校验来源页面要求请求头里带的 Referer 是它自己站内的页面地址。在 Session 的 headers 里加上Referer: https://这个壁纸源的首页/就能解决。第四步如果加了 UA 和 Referer 依然 403就要检查 Cookie 或者临时 Token 了。有些接口要求先访问一下首页获取 cookie再带着 cookie 去请求图片。你可以先在浏览器开发者工具里复制完整的 Cookie 测试如果能下载再考虑在脚本里自动维护登录态。第五步把频率降下来。如果你前面都做了还是间歇性 403大概率是触发了频率限制。这时候调大 sleep 时间、降低并发甚至换个关键词再试。如果所有办法都试过了这个数据源就不适合脚本化果断换下一个源。5.3 下载下来打不开解码后可能不是照片另一个常见问题是文件后缀是.jpg但双击打开报“文件已损坏”。这通常意味着你下载的不是图片内容而是源站返回的一个 HTML 错误页或者一个 SVG 占位图。因为你按.jpg后缀保存了所以系统识别不出来。解决思路是保存之前不要只看后缀先判断文件内容。我在前面的download_one函数里写了is_image_file就是校验文件头魔数。JPEG 文件开头通常是FF D8 FFPNG 是89 50 4E 47WebP 是RIFF...WEBP。这些魔数比文件名后缀可靠得多。我还会额外检查文件大小。正常的壁纸图片再小也有几十 KB如果一个文件只有几 KB那大概率是错误页。把这种文件直接丢弃比事后手动清理省心得多。5.4 磁盘空间满了一天比一天大壁纸分辨率越来越高一张 4K 原图动辄十几 MB跑上一个月就能吃掉几个 GB。虽然不至于撑爆硬盘但如果你长期开通 -- keep latest 的机制磁盘写满只是时间问题。我在脚本里加了两个保护。第一个是启动时检查剩余空间import shutil usage shutil.disk_usage(save_dir) if usage.free 2 * 1024 ** 3: print(磁盘剩余空间不足 2GB停止本次运行) raise SystemExit(1)第二个是按数量清理最旧文件。配置里的keep_latest就是干这个用的from pathlib import Path def clean_old_files(save_dir: str, keep_latest: int): base Path(save_dir) files sorted( base.glob(*.*), keylambda p: p.stat().st_mtime, reverseTrue, ) for old in files[keep_latest:]: old.unlink(missing_okTrue)注意文件排序是reverseTrue最新的排最前超过keep_latest的部分直接删掉。missing_okTrue是防止并发或多线程场景下重复删除报错。6. 让脚本更进一步分类整理和自动换壁纸6.1 用分辨率自动归档让壁纸库更整齐下载的壁纸越来越多以后你可能会面临一个尴尬几十张图堆在一个目录里找一张适配当前屏幕的图得看半天。这时候可以写一个简单的小工具按分辨率把文件移动到子目录里。用 Pillow 读取图片尺寸然后移动到1920x1080/、3840x2160/之类的文件夹逻辑非常简单from PIL import Image from pathlib import Path def sort_by_resolution(save_dir: str): base Path(save_dir) for path in base.iterdir(): if path.suffix.lower() not in {.jpg, .jpeg, .png, .webp}: continue try: with Image.open(path) as im: width, height im.size except Exception: continue target_dir base / f{width}x{height} target_dir.mkdir(exist_okTrue) dest target_dir / path.name if not dest.exists(): path.rename(dest)如果你不想引入 Pillow也可以直接从文件名里解析你前面拼接的宽x高部分只是没 Pillow 那么通用。Pillow 的额外好处是它还会顺带帮你检查图片文件是否完好损坏的文件在打开时就会抛异常你就能顺手清理掉。6.2 后续想迭代的方向动态换壁纸与增量订阅脚本跑到这一步已经不是“能下载壁纸”了而是一个小型的壁纸管理工具。后续可以按自己的使用习惯继续扩展。比如增量订阅的思路。每次运行前读取下载清单里的哈希集合只下载清单里不存在的图片下载完就更新清单。这样脚本每天跑也不会重复下载一张图相当于把整个壁纸站变成了你的“私人订阅源”。再比如自动换壁纸。让我觉得更好的是把下载目录设置成操作系统壁纸的图片源。Windows 和主流 Linux 桌面都有“幻灯片壁纸”功能你把目录指到脚本的save_dir它就会每隔一段时间自动切换。配合每天定时下载的任务你的桌面就会不知不觉地悄悄变化完全不用手动干预。如果你还有一台手机习惯看壁纸也可以考虑把这个目录同步到网盘或者通过自建的轻量消息服务把当天的预览图推到手机上。这一步就看你的网络环境和设备习惯了没有统一方案但前面这些代码已经给你留好了扩展点。就我个人体验而言一个壁纸下载脚本从“能跑”到“好用”的差距主要不在代码本身而在你有没有把异常和边界情况都挡在门外。多花十分钟处理非法文件名、校验文件头、限制磁盘占用之后半年你都不用管它。你先按这个方案把第一版搭起来后面再根据自己的上网习惯慢慢调比直接复制一个现成的一键脚本要顺手得多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进