1. 从手动一张张存图,到一纸脚本全部搞定
作为一个常年折腾自动化脚本的人,我太清楚手动存壁纸是什么体验了。看到一张好看的图,右键另存为,选路径,命名,一套动作下来,十张图五分钟就没了,而且来回切窗口特别打断状态。于是动手写了个Python脚本自动下载壁纸,一次跑完,文件夹里躺着几十张高清图,直接挑,省下的时间够我调两轮代码。
这篇文章就把我踩过的坑和最终落地的方案完整写出来,适合刚接触Python、想写第一个“能实际干活”脚本的新手,也给那些已经会写简单爬虫、想找个练手项目巩固正则表达式和文件批量处理的人。核心内容就是三件事:怎么分析网页图片链接规律、怎么写一个稳定的下载脚本、以及那些网上教程不会明说的反爬和编码坑。
先说结论:这个脚本的本质,就是把“人从网页上找图片地址、逐个下载”这件事,拆成“请求网页 → 解析HTML → 提取图片链接 → 批量保存”四个步骤。每步都不难,但组合起来,能让你每天开机自动换一批新壁纸。
2. 动手前必须想清楚的几件事
2.1 库的选择:为什么不装几十个依赖
很多人一上来就上Scrapy、BeautifulSoup、Selenium,先不说安装依赖就要花半天,光是为了下载几十张壁纸引入一整套爬虫框架,纯属牛刀杀鸡。我的原则是:能用标准库解决的,绝不多装一个包。
这个脚本最核心的两个库是requests和re。前者负责发HTTP请求拿网页和图片数据,后者用正则表达式从HTML里抠出图片链接。如果你连requests都没装,命令行跑一句pip install requests就行,没有它,你用标准库urllib也能写,只是代码略啰嗦,处理超时、重试、请求头会麻烦一些。
有人可能会问,为什么不推荐用BeautifulSoup解析HTML?因为壁纸站点的HTML结构往往很乱,标签嵌套不规范,但图片链接的格式通常是稳定的,比如https://xxx.com/uploads/2024/12/xxx.jpg。正则表达式直接匹配URL模式,反而比解析DOM树更抗结构变动。等你真遇到需要提取页面某个区域内容的需求,再考虑上解析库也不迟。
2.2 认准一个目标站点,比到处试探强十倍
不管写什么脚本,第一步永远是“找规律”,而不是“写代码”。我选了一个壁纸站点做实验,它的图片列表页URL结构非常友好,长这样:
https://example.com/wallpaper/listing/1 https://example.com/wallpaper/listing/2打开第一个页面,按F12看网络请求,发现每张预览图都在img标签的>headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Referer": "https://example.com/" }
User-Agent是告诉服务器“我是什么客户端”,Referer是告诉服务器“我从哪个页面跳过来的”。很多图片服务器会校验Referer,不带的话直接拦。这两个字段是我写下载类爬虫时默认必带的,算是最低限度的“礼貌访问”。
2.4 文件命名与保存路径:最容易被忽略的崩溃现场
下载图片只是第一步,怎么存才见细节。壁纸站点的图片ID通常不会重复,我一开始直接用ID当文件名,后来发现还得带上尺寸或者站点标识,否则你从两个不同站点下载的图混在一个文件夹里,同名文件互相覆盖,追悔莫及。
我的命名规则是:
filename = f"wallpaper_{img_id}_{width}x{height}.jpg"既要保留原图信息,又要保证唯一性。路径方面,脚本放在项目根目录,下载目录用./downloads/,每次运行自动创建,免得手动建文件夹。
3. 核心代码一步步拆解
3.1 获取页面HTML源码
写个fetch_page(url)函数,用requests.get拿网页内容,加超时控制,加请求头,再加上异常处理。这里有个细节:页面编码。很多壁纸站是UTF-8编码没什么问题,但保不齐有GBK的,所以我用response.encoding = response.apparent_encoding自动探测编码,虽然稍微慢一点,但能避免中文乱码导致正则匹配失败。
import requests def fetch_page(url, headers, timeout=15): try: response = requests.get(url, headers=headers, timeout=timeout) response.raise_for_status() response.encoding = response.apparent_encoding return response.text except requests.exceptions.RequestException as e: print(f"[错误] 请求失败: {url},原因: {e}") return Noneraise_for_status()是必须的,它会在HTTP状态码为4xx或5xx时抛出异常,避免你拿着一个404页面当正常内容解析,白白浪费时间。
3.2 用正则表达式提取图片地址
拿到HTML后,提取图片链接。我通常会写一个parse_image_urls(html)函数,根据站点实际情况设计正则。比如某些站点的缩略图地址长这样:
<img>import re def parse_image_urls(html): pattern = re.compile(r'data-src="(https://example\.com/images/\d{4}/\d{2}/\d+\.jpg)"') return pattern.findall(html)这里有几个容易踩的坑。第一,正则里圆括号是分组捕获,我只保留需要的那部分链接,不要整个标签都捕获下来。第二,图片URL里的.在正则里是“任意字符”的意思,需要转义成\.,否则匹配到奇怪的字符也不自知。第三,如果图片URL有https://和http://两种协议,最好把正则改成https?://,省得因为协议不同漏掉图。
如果页面有多页,循环翻页即可。我这里加个控制变量,比如只下载第一页的30张图,免得一跑停不下来。
3.3 下载并保存图片的完整流程
拿到图片URL列表后,写个download_image(url, filepath)函数。这里要说明一下,直接stream=True是更稳妥的做法,它不是一次把整个文件加载进内存,而是一块块写盘,下载大壁纸时内存占用很低。
def download_image(url, filepath, headers, timeout=20): try: response = requests.get(url, headers=headers, timeout=timeout, stream=True) response.raise_for_status() content_type = response.headers.get("Content-Type", "") if "image" not in content_type: print(f"[跳过] 不是图片资源: {url},Content-Type: {content_type}") return False with open(filepath, "wb") as f: for chunk in response.iter_content(chunk_size=8192): f.write(chunk) print(f"[完成] 已保存: {filepath}") return True except requests.exceptions.RequestException as e: print(f"[失败] 下载异常: {url},原因: {e}") return False这里加了一个Content-Type校验,防止某些链接返回的是HTML页面而不是图片。比如网站做了防盗链,返回一个/error.html页面,你直接写盘得到的是一堆HTML代码,打开图片文件报错。这是我在实际运行中最先遇到的坑。
3.4 把所有环节串起来
主流程就清晰了:
def main(): base_url = "https://example.com/wallpaper/listing/{}" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Referer": "https://example.com/" } os.makedirs("./downloads", exist_ok=True) all_image_urls = [] for page in range(1, 4): page_url = base_url.format(page) html = fetch_page(page_url, headers) if not html: continue urls = parse_image_urls(html) print(f"[信息] 第 {page} 页找到 {len(urls)} 张图片") all_image_urls.extend(urls) print(f"[信息] 共找到 {len(all_image_urls)} 张图片,开始下载...") for idx, img_url in enumerate(all_image_urls): img_id = idx + 1 filepath = f"./downloads/wallpaper_{img_id}.jpg" download_image(img_url, filepath, headers)跑起来的效果就是:控制台打印每页找到了几张图,然后逐个下载、保存,进度一目了然。如果中途某张图挂了,不会影响后面的图。
上面这是最简版,能做到“能用”。但离“好用”还差几步。
4. 进阶优化:让脚本更快、更稳、更自动
4.1 多线程提速:三行代码让下载快4倍
顺序下载的问题很明显:这几秒钟一张图,要下30张就得一分多钟,关键期间网络带宽没用满,大部分时间都在等待服务器响应。Python里的concurrent.futures模块能轻松搞定并发下载。
from concurrent.futures import ThreadPoolExecutor, as_completed def download_task(img_url, idx): filepath = f"./downloads/wallpaper_{idx}.jpg" download_image(img_url, filepath, headers) return idx with ThreadPoolExecutor(max_workers=5) as executor: tasks = [executor.submit(download_task, url, idx) for idx, url in enumerate(all_image_urls)] for future in as_completed(tasks): future.result()我不建议把线程数调太高,一般5~10就够。线程太多,首先可能被服务器封IP,其次你自己的网络带宽就那么大,并发高了反而互相挤占。
4.2 增量下载:重复运行不重复下
每次跑脚本都重新下载全部图片,显然很蠢。我的做法是:下载前检查目标文件是否已存在,存在就跳过。
filepath = f"./downloads/wallpaper_{idx}.jpg" if os.path.exists(filepath) and os.path.getsize(filepath) > 0: print(f"[跳过] 已存在: {filepath}") continue如果想更精细,还可以把已下载的图片ID记录到一个.txt文件里,每次只下载新增的ID。这就是最简单的增量更新逻辑了。对于壁纸这种每天更新的资源,增量逻辑是刚需,不然每次全量跑不仅浪费时间,还容易触发风控。
4.3 失效链接自动重试
网络请求难免有偶发失败,我加了三秒后的重试逻辑,最多重试两次。原理很简单,requests.get如果失败就except后重新调用。
def download_with_retry(url, filepath, headers, retries=2): for attempt in range(retries + 1): if download_image(url, filepath, headers): return True time.sleep(3 * (attempt + 1)) return False这里用到了“递增退避”策略,第一次失败等3秒,第二次失败等6秒。原因很简单,频繁重试会导致服务器压力增大,也容易被反爬机制盯上。
4.4 对接系统定时任务:全自动换壁纸闭环
脚本能跑只是第一步,让它“跑完自动把壁纸换上”才是完整闭环。Windows上可以用计划任务,Linux和macOS上用cron配合一个小工具(比如macOS的osascript配合wallpaper命令)来随机切换壁纸目录里的图片。如果你用的是GNOME桌面,直接调gsettings设置壁纸路径即可。
这一步事实上把“下载”扩展成了“下载 + 换肤”,让脚本的价值翻倍。我就把脚本挂在系统定时任务里,每天凌晨跑一次增量下载,再用系统命令随机挑一张当天下载的图设为壁纸。起床看到的就是新桌面。
5. 常见问题与排查技巧实录
5.1 请求超时:连接被重置怎么办
这个脚本刚写完第一次跑的时候,我遇到的最多问题就是Connection timed out。原因无外乎三种:目标站点响应慢、本地网络波动、并发线程过多触发服务器限制。
排查步骤我习惯这样走:
- 先把
timeout参数调大,从默认的3秒改成20秒,看是不是单纯太慢。 - 抓单张图片下载,排除是不是并发导致的问题。
- 如果单张也超时,用浏览器手动访问试试。浏览器能打开而脚本打不开,那就是请求头问题;浏览器也打不开,那是站点或网络问题。
5.2 下载下来全是403
403这个状态码基本就是指“服务器认识你,但不让你进”。最典型的场景就是我前面说的——没带User-Agent或Referer。如果你带了还是403,检查两点:
- 图片直链是否有防盗链限制,很多图床要求
Referer必须匹配某个域名,哪怕你的脚本真的是合法访问。 - 目标站点是否校验
Accept和Accept-Language请求头。某些严格的服务器会看这两个字段。遇到这种情况,把请求头补齐成完整浏览器头即可。
我的做法是直接把Chrome的请求头整体复制过来,User-Agent、Accept、Accept-Language、Referer一字不差,实测能解决九成403问题。
5.3 下载的文件打不开,用十六进制一看不是图片
这种场面特别让人头大。你以为下载的是jpg,双击却提示“文件已损坏”。检查文件头,发现内容是<!DOCTYPE html>开头的HTML。原因通常是请求的URL不是图片,而是一个需要登录跳转的页面,或者被防盗链重定向到了404页面。
我的解决方法是下载前先检查response.headers["Content-Type"],不是image/开头就直接放弃。这就是3.3节代码里那段校验的实际意义。另外,注意stream=True模式下,response.headers在第一次读取内容前就能访问,不用等整个文件下载完再判断。
5.4 正则表达式匹配不到图片地址
新手最容易在这里卡住。匹配不到,九成是正则有误,但我建议你按这个顺序排查:
- 把HTML源码保存成一个
.html文件,用编辑器搜索图片URL,确认地址到底是什么格式,是不是动态加载的。 - 如果页面源码里压根没有图片URL,说明图片是异步加载的。这时候就要看接口地址,去Network面板里找到返回JSON的XHR请求,解析JSON提取URL,而不是解析HTML。
- 正则写好后,先用一小段HTML做测试,确认无误再跑全文。别傻乎乎拿整个页面调试正则。
5.5 编码问题导致匹配失败或者文件名乱码
有次换了个站点,页面编码是GBK,我用默认UTF-8解码,一页下来全是乱码,正则里的中文关键字也匹配不上。解决方式就是前面提到的:
response.encoding = response.apparent_encoding这行代码背后的逻辑是,requests库会自动从HTTP头里猜编码,但很多站点不返回正确的Content-Type头,所以让requests用Chardet从内容采样中猜,更靠谱。
5.6 下载图片不完整,文件比预期小
这类问题多出现在对流式下载处理不当。如果你用response.content而图片较大,内存占用高但勉强能完成;如果你用了stream=True但写入不及时中断了,文件就会残缺。多数情况是你没加超时,或者服务器主动断流了。
这类问题不太好追溯。我的建议是下载完成后,用os.path.getsize(filepath)校验文件大小,太小的文件一律删了重下。数值经验:小于50KB的jpg基本就是残次品或者占位图,直接丢弃。
6. 最后分享一点个人实操体会
这脚本我前后改了三轮。第一轮是功能版,跑通下载;第二轮加了增量、重试、并发,解决稳定性;第三轮加了定时换壁纸,让整个流程闭环。
我最深的体会是:爬虫脚本的核心不在爬,而在边界处理。正常场景下的下载逻辑半小时就能写完,但各种意外情况——超时、编码、防盗链、重定向、断流——才是真正消耗时间的地方。写这类脚本,前期宁可多花20分钟分析页面和网络请求规律,也不要急着敲代码。规律摸透了,代码一次就能稳定跑很久;规律没摸透,后面就是无穷无尽的调试循环。
如果这个脚本运行一段时间之后图片链接失效,大概率是站点改了HTML结构,到时只需要调整parse_image_urls里的正则表达式就行,不用动其他部分。这也是我坚持把解析逻辑单独抽成函数的原因,维护成本最低。我的经验是,结构固定的站点,一套正则能安稳用上大半年,等真失效的时候看一眼新页面,改一行模式串,一分钟就能恢复。