2026/10/9 4:06:02

Python自动下载壁纸:完整爬虫脚本与定时更新实战

Python自动下载壁纸:完整爬虫脚本与定时更新实战 1. 为什么非要写个脚本去下载壁纸先说个真实场景。我电脑里的壁纸文件夹大概攒了三千多张全是这些年从各个壁纸站、摄影社区、Reddit的wallpaper板块一张张手动存的。手动下载这事儿偶尔干一次还行攒多了就知道有多烦先打开网页翻半天看到喜欢的点进去等大图加载右键另存为还要手动选目录、确认文件名不重复。一周不整理文件夹里就是一片壁纸(1).jpg、壁纸(2).png的混乱现场。后来我换了新电脑壁纸一张没备份想重新攒一批Manually翻了两个小时就崩溃了。当时就想这种重复劳动就该交给脚本。正好那段时间在给一个爬虫项目调逻辑顺手写了这个自动下载壁纸的Python脚本。结果一发不可收拾——它不仅解决了我换壁纸焦虑的问题还让我把Python的requests、并发下载、异常重试、定时任务这些知识点串起来练了一遍。这篇文章就把整个脚本的思路、代码、踩坑过程都拆开讲清楚。写出来不是给你一个死板的源码就完事而是把我当初为什么这么设计哪里容易翻车的思考过程也一并说出来。不管你是刚学Python没多久的新手还是已经写了一阵子想找个练手项目的同学照着这篇文章走一遍你就能获得一个能自动从壁纸站拉高清大图的脚本顺便把Python网络请求的很多细节搞明白。2. 环境准备先把Python和pip这关过了2.1 安装Python时最容易踩的坑我知道肯定有人会跳过这一步直接往下看代码但根据我在各种技术群里看到的问题90%的人跑不起来脚本不是代码的问题是环境就没配好。尤其是两个高频报错一个是pip : 无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。这个错误在Windows的PowerShell里极其常见原因就一个Python安装的时候那个Add Python to PATH的勾选框没有被勾上或者你用的Python版本太老3.4以前压根没有自动配置PATH。系统不知道pip命令在哪自然就报这个错。另一个是python not found或者python 不是内部或外部命令。同样的道理Python解释器本身没进系统环境变量。解决办法很直接去Python官网下载安装包就是热搜词里那个python官网下载一定不要从各种Python中文网Python下载站下载那些站点太多捆绑和不靠谱的版本认准python.org官方域名。安装的时候第一屏就把底部的Add Python to PATH勾上这是全网重复了无数遍但依然有人会忽略的一步。装完后重新开一个终端窗口一定要重开旧窗口读不到新环境变量输入python --version确认能看到版本号。注意如果你用的是Windows自带的Microsoft Store版本Python路径可能会指向WindowsApps里的一个空壳导致python命令能执行但装不了包。建议还是老老实实去官网装。2.2 pip装包的正确姿势环境通了之后就是装依赖库。这个脚本主要用三个库requests发HTTP请求、下载图片比Python自带的urllib好用太多beautifulsoup4解析HTML提取图片链接lxml解析器给BeautifulSoup提供底层解析引擎安装命令就一行pip install requests beautifulsoup4 lxml如果你用的是Python 3.4以上版本pip一般自带了。但这里有个实际问题——在国内网络环境下直接pip install经常慢到怀疑人生或者直接timeout。解决办法是临时指定清华镜像源pip install requests beautifulsoup4 lxml -i https://pypi.tuna.tsinghua.edu.cn/simple至于为什么用清华源而不是其他源单纯是因为我用下来它的同步速度最稳、包最全。阿里源和腾讯源也不错但如果某个包在清华源找不到可以随时切-i参数换源这才是-i参数的正确打开方式。再补充一个很多人不知道的小技巧pip本身是可以升级的python -m pip install --upgrade pip。有时候pip install什么包都报错先升一下pip就解决了。这算是我装过无数次环境之后的一个心得新手遇到装包失败第一反应往往是怀疑网络其实有时候就是pip版本太老对新的wheel包格式支持不好。3. 拆解需求壁纸脚本到底要做什么写代码之前先想清楚需求。很多人一上来就写代码结果写着写着方向就走偏了。我这边的需求拆解下来就四条从壁纸站拿图自动获取一个页面上所有壁纸的高清大图URL。筛选质量只下载符合分辨率要求的图比如至少1080p或者我指定3840x2160的4K图。整理文件下载的图片按日期、按主题分类存到本地目录文件名尽量不要重复。反复运行脚本不能下载一次就废要能每次跑的时候自动跳过已经下载过的图片增量下载不然你跑了三次定时任务硬盘里三张一模一样的图。这四个需求是不是听起来都很简单但真正实现起来每个点都有可以优化的细节。就拿获取图片URL来说不同网站的页面结构完全不一样有的图片URL直接写在img标签里有的则是懒加载——图片真实地址放在>import requests from bs4 import BeautifulSoup def get_wallpaper_urls(keyword, pages3): base_url https://wallhaven.cc/search headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } urls [] for page in range(1, pages 1): params { q: keyword, categories: 010, purity: 100, sorting: date_added, order: desc, page: page } resp requests.get(base_url, headersheaders, paramsparams, timeout10) resp.raise_for_status() soup BeautifulSoup(resp.text, lxml) for figure in soup.select(figure): a_tag figure.select_one(a[href*wallhaven.cc/w/]) if a_tag: detail_url a_tag[href] urls.append(detail_url) return urls这只是一个拿详情页链接的步骤。为什么先拿详情页链接而不是直接拿图片URL因为缩略图页面上虽然能拿到图但拿到的都是压缩过的预览图不是原图。你要的是壁纸对吧那就必须去详情页蹲原图。4.2 详情页解析找到真正的原图地址详情页的解析逻辑也很直接def get_original_image(detail_url): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(detail_url, headersheaders, timeout10) soup BeautifulSoup(resp.text, lxml) # 原图所在的img标签有一个idwallpaper的属性 img soup.select_one(img#wallpaper) if img and img.get(src): return img[src].strip() return None这里面的img#wallpaper选择器是关键。不同的壁纸站用的标识不一样有的用classdownload-btn有的用属性>import os def download_image(url, save_path): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://wallhaven.cc/ # 防盗链 } # 跳过已存在的文件实现增量下载 if os.path.exists(save_path): return False resp requests.get(url, headersheaders, streamTrue, timeout30) resp.raise_for_status() # 流式写入 with open(save_path, wb) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk) return True那个iter_content(chunk_size8192)就是流式下载的核心一次读取8KB写入磁盘不会让内存被一个超大图片直接撑爆。这里还有个小心思——os.path.exists(save_path)检查放在下载之前如果本地已经有这个文件了就直接跳过。这个设计就是为了满足第3章说的增量下载需求不然每次跑脚本都重新下一遍硬盘迟早扛不住。关于防盗链我在Headers里加了Referer: https://wallhaven.cc/。为什么加这个因为很多图片服务器会检查HTTP请求头里的来源地址如果Referer不是站内地址直接返回403。这个经验在处理几乎所有壁纸站、图床类网站时都适用。遇到图片URL能打开但返回错误状态码的情况第一反应就是补Referer十有八九能解决。4.4 用并发加速下载单张下载没问题但一次要下几十张图串行下载速度太慢了。壁纸原图动不动几MB一张一张下下完10张可能就要一分钟。用线程池改成并发下载速度可以提升好几倍。from concurrent.futures import ThreadPoolExecutor def batch_download(tasks): tasks: [(url, save_path), ...] with ThreadPoolExecutor(max_workers5) as executor: futures [ executor.submit(download_image, url, path) for url, path in tasks ] for future in futures: future.result() # 如果下载出错这里会抛出异常max_workers5表示同时开5个线程下载。为什么不设20因为很多壁纸站都有并发限制你一下子开太多线程请求太快容易被服务器封IP。5到8个是比较安全的区间既能提速又不容易触发风控。提示并发下载时我建议把超时时间适当调大一点。比如单线程时timeout30并发时可能一个线程等30秒就放弃但整体下载队列还没处理完。我一般会加到60秒给慢速网络留足够缓冲。5. 从一次性脚本到自动化定时下载与壁纸轮播5.1 让脚本定时跑起来下载脚本写完了但我不想每次手动运行它。我的目标是每天凌晨自动拉一批新壁纸到本地然后系统自动换桌面壁纸。这样我每次打开电脑桌面都是新的。Windows下有两种做法一种是任务计划程序一种是写一个.bat文件放到启动文件夹。我推荐任务计划程序因为可以控制触发时间和频率。步骤很简单把脚本保存成download_wallpaper.py在脚本目录下创建一个.bat启动文件内容就一行python download_wallpaper.py --keyword landscape打开任务计划程序创建基本任务触发器选每天时间设为凌晨3点这个时间网速快、打扰少操作选启动程序程序选你的.bat文件完成但这里有一个新手极其容易忽略的坑任务计划程序跑Python脚本时工作目录可能跟脚本所在目录不一致。如果你的脚本用相对路径去保存文件比如./wallpapers/那脚本可能会把图片写到C盘Windows目录或者别的地方。解决办法很粗暴在脚本开头强制切换工作目录import os os.chdir(os.path.dirname(os.path.abspath(__file__)))这句代码的意思是把当前工作目录切到脚本文件所在的目录。加了这一句之后你脚本里所有的相对路径都变得可靠了。这个细节我当时没注意定时任务跑了一天发现图片下载到了一个莫名其妙的路径排查半天才反应过来。5.2 自动更换桌面壁纸定时下载只是第一步真正让我觉得值了的是自动换壁纸。Windows上用Python切壁纸非常简单直接调Windows APIimport ctypes def set_wallpaper(image_path): image_path os.path.abspath(image_path) # 设置桌面壁纸 ctypes.windll.user32.SystemParametersInfoW(20, 0, image_path, 0)这里的20是SPI_SETDESKWALLPAPER参数的数值Windows API里固定用它来表示设置桌面壁纸这个操作。第四参数0表示不更新配置文件生效即时。如果你一次性下载了10张壁纸配合Windows系统自带的幻灯片放映功能把壁纸文件夹指向你的下载目录播放间隔设成30分钟桌面就会自动轮播。Linux下也可以用类似思路不同桌面环境命令不同GNOME用gsettings set org.gnome.desktop.background picture-uri file:///path/to/imageKDE有窗口管理器命令。不过Windows用户占大多数这里就不展开Linux的配置了。到这里整个自动下载壁纸自动换壁纸的闭环就完整了。但是等一下脚本真到这一步就算完美了吗我实测跑了几周发现还有一堆真实环境的问题要处理下一章就专门讲这些坑。6. 实测中踩过的坑反爬、超时、乱码与重复下载6.1 403 Forbidden盗链和请求频率的双重陷阱我脚本第一版写完高高兴兴跑起来结果下载不到10张图后面全部403。当时很懵前面几张好好的怎么突然就全挂了逐个排查后发现两个原因第一个是请求频率太高。即使加了并发短时间内打了上百个请求壁纸站的反爬策略直接把整个IP段临时封了。解决方式就是降低并发数加随机延时import time import random # 在每次请求之前随机sleep 0.5到1.5秒 time.sleep(random.uniform(0.5, 1.5))这个随机延时不仅能把请求频率摊开还能让行为看起来更像真人操作。它跟并发下载是配合关系——并发太多就没什么延时的意义我最终调整为并发3个线程每个线程内部请求前随机sleep整体效果既快又不被封。第二个是Referer被检查。这个前面已经说了Wallhaven的图片CDN会检查Referer不带站内Referer的请求直接403。这种问题会让新手一头雾水因为你在浏览器里打开图片URL是正常的用requests下载却失败。记住浏览器会自动带上Referer而requests默认不会。所以凡是下载图片这类操作手动headers里补上Referer是一个基础素养。6.2 文件名乱码和重复下载下载下来的图片名我一开始直接用URL的最后一节作为文件名。但Wallhaven的图片文件名都是xx-xxxx-数字.jpg这种格式毫无辨识度。我后来又改了逻辑把搜索关键词和所在页码加进文件名前缀import re from urllib.parse import urlparse def generate_filename(url, keyword, index): # 从URL中提取原始文件名 parsed urlparse(url) original os.path.basename(parsed.path) if not original.endswith(.jpg) and not original.endswith(.png): ext .jpg # 默认扩展名 else: ext os.path.splitext(original)[1] # 清理关键词里的非法字符 safe_keyword re.sub(r[\\/:*?|], _, keyword) return f{safe_keyword}_{index:03d}{ext}给文件名加上{index:03d}序号的好处是同一批下载的图片即使在下载过程中因为并发导致顺序错乱最终文件名也能稳定对应。另外Windows文件名不允许包含\/:*?|这几个字符关键词里如果有空格或者特殊符号必须先清洗否则写入图片的时候会直接报错。这个坑特别坑因为报错信息会让你误以为是文件写入权限的问题。6.3 请求超时和失败重试壁纸站在晚上八九点高峰期响应特别慢单张图下载到一半也可能断连。这时候没有超时保护的话脚本会卡在那里半天不动没有重试逻辑的话偶尔一张图失败整个任务就中断了。超时很好设置requests.get的timeout参数就可以。重试我用了一个简单的装饰器import functools def retry(max_retries3, delay1): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception as e: if attempt max_retries - 1: raise time.sleep(delay * (attempt 1)) # 递增等待 return None return wrapper return decorator retry(max_retries3, delay2) def fetch_page(url, headers): return requests.get(url, headersheaders, timeout10)time.sleep(delay * (attempt 1))的意思是第一次失败等2秒第二次失败等4秒第三次失败等6秒。递增等待的目的很简单给服务器一个恢复时间。如果连续重试还失败就直接抛异常让主流程知道同时记一个日志。为什么不无限重试因为如果服务器已经明确封了你的IP重试一万次都是白费只会加重自己的负担。如果你想要更成熟的重试方案tenacity库是现成的retry(stopstop_after_attempt(3), waitwait_exponential())一行搞定。但自己写一遍重试逻辑还是有价值的能让你真正理解重试机制的原理后面用任何库都不虚。6.4 完整性校验下载了但是图是损坏的这个坑很隐蔽。有时候requests.get一切正常文件也写进去了打开一看却是一张损坏的图——文件大小明显偏小比如一张4K壁纸只有几KB。为什么会这样一般是服务端返回了错误页面但状态码是200或者下载过程中连接被切断但代码没发现。简单有效的解决办法检查文件大小。import os def validate_image(save_path, min_size50*1024): 验证图片文件大小是否合理, 小于50KB的壁纸文件大概率有问题 if not os.path.exists(save_path): return False size os.path.getsize(save_path) return size min_size下载完成后调用一下validate_image不通过的直接删掉重下。这个min_size阈值可以根据你的实际场景调整4K壁纸一般都在1MB以上我把阈值设成50KB已经是比较宽裕了。为什么不校验文件头你可以更进一步用imghdr或PIL打开文件验证格式from PIL import Image def validate_image(save_path): try: with Image.open(save_path) as img: img.verify() # 验证文件是否为有效图片 return True except Exception: return FalsePIL的verify()方法不会真正加载整张图只是验证文件头信息和完整性速度很快。如果装了Pillow库这个方案更靠谱。7. 最终脚本合并与使用说明前面拆了一堆细节现在把整个脚本串起来。我的最终版本长这样你稍微改改就能直接用import os import re import time import random import ctypes import functools import requests from bs4 import BeautifulSoup from concurrent.futures import ThreadPoolExecutor from urllib.parse import urlparse # 确保脚本所在目录为工作目录 os.chdir(os.path.dirname(os.path.abspath(__file__))) HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://wallhaven.cc/ } def retry(max_retries3, delay2): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception: if attempt max_retries - 1: raise time.sleep(delay * (attempt 1)) return None return wrapper return decorator retry() def fetch_page(url, paramsNone): return requests.get(url, headersHEADERS, paramsparams, timeout10) def get_wallpaper_urls(keyword, pages2): base_url https://wallhaven.cc/search detail_urls [] for page in range(1, pages 1): resp fetch_page(base_url, params{ q: keyword, categories: 010, purity: 100, sorting: date_added, order: desc, page: page }) soup BeautifulSoup(resp.text, lxml) for figure in soup.select(figure): a_tag figure.select_one(a[href*wallhaven.cc/w/]) if a_tag: detail_urls.append(a_tag[href]) time.sleep(random.uniform(0.5, 1.5)) return detail_urls retry() def get_original_image(detail_url): resp fetch_page(detail_url) soup BeautifulSoup(resp.text, lxml) img soup.select_one(img#wallpaper) return img[src].strip() if img and img.get(src) else None def generate_filename(url, keyword, index): parsed urlparse(url) original os.path.basename(parsed.path) ext os.path.splitext(original)[1] if os.path.splitext(original)[1] else .jpg safe_keyword re.sub(r[\\/:*?|], _, keyword) return f{safe_keyword}_{index:03d}{ext} def download_image(url, save_path): if os.path.exists(save_path): return False resp requests.get(url, headersHEADERS, streamTrue, timeout60) resp.raise_for_status() with open(save_path, wb) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk) if os.path.getsize(save_path) 50 * 1024: os.remove(save_path) raise Exception(f文件过小疑似异常: {save_path}) return True def get_tasks(keyword, pages2): tasks [] detail_urls get_wallpaper_urls(keyword, pages) for i, detail_url in enumerate(detail_urls): img_url get_original_image(detail_url) if not img_url: continue path os.path.join(wallpapers, generate_filename(img_url, keyword, i)) tasks.append((img_url, path)) return tasks def main(keywordlandscape, max_workers3): os.makedirs(wallpapers, exist_okTrue) tasks get_tasks(keyword, pages2) print(f发现 {len(tasks)} 张壁纸需要下载) with ThreadPoolExecutor(max_workersmax_workers) as executor: futures [executor.submit(download_image, url, path) for url, path in tasks] for future in futures: try: future.result() except Exception as e: print(f下载失败: {e}) if __name__ __main__: main(keywordlandscape)这个版本的代码我没有把set_wallpaper放进去因为壁纸切换是另一个独立话题。你如果只想跑下载把这个文件存成wallpaper_downloader.py在终端执行python wallpaper_downloader.py想换关键词改main(keywordlandscape)里面的词就行。想下载更多页把pages2调大。8. 进阶扩展脚本还能怎么玩如果上面的基础版你已经跑通了这里有几个我后来自己加的扩展点你可以按需选择按分辨率分组存放爬取详情页的时候把图片的分辨率也提取出来img#wallpaper的>