Python自动化脚本实战:从零实现壁纸自动下载与定时任务
写壁纸自动下载的Python脚本最吸引人的一点是它把一个“每天手动逛网站、右键保存图片”的重复动作简化成一行命令或者一个定时任务。这个项目特别适合刚学Python的人拿来练手因为它能把网络请求、JSON解析、文件读写、异常处理这些基础知识全部串起来。真正跑通之后你对Python脚本的理解就不再是“照着教程敲代码”而是能主动控制程序去完成一件实实在在的事。这个脚本要解决的痛点其实很明确壁纸网站很多但每天一张张打开太麻烦有些网站图片分辨率大小不一手动挑选很费时间还有的人希望开机或定时自动更新壁纸。把这些需求汇总一下就是一个自动下载器应该具备的能力从某个稳定的图片来源获取当天壁纸列表解析出图片地址按规则保存到本地文件夹最后能在无人值守的情况下定时运行。接下来我会按实际开发顺序来讲从环境准备到完整代码再到自动化和常见坑尽量让你今天下午就能跑起来。1. 写在动手前这个脚本到底要做什么1.1 需求拆解与方案选型先把“自动下载壁纸”这句话拆开它其实包含三个能力获取壁纸信息源也就是图片从哪儿来解析出真正的图片直链下载并保存到本地最好还能按日期归类。这三个能力对应着Python里不同的模块和库。获取信息用requests发HTTP请求解析内容如果返回的是JSON就用标准库json如果是网页HTML就要用BeautifulSoup保存文件用标准库os加文件写入操作。看起来不难但实际运行中会遇到超时、反爬、文件名非法字符、磁盘权限、编码乱码等一堆问题这些只有真正写过一遍才能体会到。方案选型上我一般会分成两类第一类是有现成JSON接口的公开数据源比如必应每日壁纸这种最稳定适合第一版第二类是普通网页图片站需要自己解析链接适合进阶练习。第一版我强烈建议选前者因为不用处理复杂的反爬机制能让你把注意力集中在“Python脚本”本身的主干流程上。等主干跑通了再慢慢加爬虫技术遇到问题时也更容易判断是哪里出错。1.2 为什么选择必应每日壁纸这个数据源必应搜索首页每天会换一张高质量背景图同时官方有一个公开接口可以获取最近N天的壁纸信息返回的是JSON格式非常友好。接口地址长这样https://cn.bing.com/HPImageArchive.aspx?formatjsidx0n8mktzh-CNformatjs表示返回JSONidx0表示从今天开始n8表示取8张mktzh-CN表示中文市场。也就是说你不去抓网站HTML只需要向这个地址发一个GET请求就能拿到壁纸标题、版权说明、图片URL等信息。我把这个接口作为第一版的数据源还有一个重要原因是它不需要注册账号也不需要申请API Key个人学习使用非常省心。当然用公开接口也要注意频率别写个死循环每秒请求一次那样无论对哪个服务都不友好。脚本里加上适度延时控制请求频率既是代码习惯也是基本的网络礼仪。1.3 整体流程梳理这个脚本的流程可以画成一条直线发送请求 - 接收JSON - 解析图片链接 - 拼接完整地址 - 逐个下载 - 保存到文件夹每一步之间的数据依赖很清晰。发送请求后拿到的是字符串用response.json()变成Python字典接着从字典里取images字段它是一个列表列表里每个元素又是一张壁纸的信息壁纸信息里有url字段注意这个url通常是相对路径需要和站点域名拼接成完整的https://地址最后把完整地址传给下载函数用二进制流的方式写入文件。我在教朋友写这类脚本时会让他们先把每一步的结果都打印出来看一下而不是急着写完整代码。比如先只写请求接口的代码打印返回的JSON结构再写解析代码打印每个图片链接最后再写保存文件。这种“先观察、再编码”的习惯能帮你省掉大量调试时间。1.4 用到的核心知识点如果你正处于Python入门阶段这个项目覆盖的知识点足够你消化一阵子HTTP请求与响应requests.get、状态码、请求头、超时JSON数据处理字典取值、列表遍历、数据清洗文件操作创建目录、二进制文件写入、判断文件是否存在异常处理try/except包裹网络请求保证单张图片失败不会中断整个脚本函数封装与命令行参数让脚本可以复用、可以传参。很多人学Python入门时语法都看懂了但一合上教程就不知道怎么写东西。这个项目的价值就在于它逼着你把这些知识点组合在一起组合过程中你才会发现哪些地方是真懂了哪些地方只是“看着眼熟”。2. 环境准备Python、依赖和常见命令问题2.1 Python安装与环境变量第一步自然是装Python。去官网下载对应你操作系统的安装包安装时一定要勾选“Add Python to PATH”这一项。不要小看这个勾选框很多新手最后卡在“输入python没反应”上八成就是没勾它。装完后打开命令行分别执行下面两条命令python --version pip --version正常情况下会输出Python版本号和pip版本号。如果你用的是Windows有时候系统里装了多个Python或者从Microsoft Store安装了别名版本直接输入python可能打开的是应用商店。这种情况下可以试试py命令py --versionpy是Windows上Python官方安装包自带的启动器尤其在多个Python版本共存时用它来指定版本会更安全。2.2 用虚拟环境隔离依赖我见过不少新手图省事直接把requests装进全局Python环境时间一长全局环境里堆了一堆不知道谁家的包。后来某个项目需要不同版本的库互相冲突只能痛苦地重装Python。所以现在我做项目开头第一件事永远是建虚拟环境。在项目目录下执行python -m venv venvWindows下激活虚拟环境venv\Scripts\activateLinux或macOS下激活source venv/bin/activate激活后命令行提示符前面会出现一个(venv)的标志。此时再安装的库都只会进这个独立空间不会污染全局。如果你用的是VSCode还需要在编辑器右下角或命令面板里选择解释器指定到venv目录下的Python这样终端和编辑器的运行环境才一致。这个“vscode python环境配置”的问题本质就是解释器没选对。2.3 安装requests库这个项目离不开requests这个库它把HTTP请求封装得非常简洁几乎成了Python网络请求的事实标准。激活虚拟环境后执行pip install requests如果下载速度很慢可以临时指定国内镜像源pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后可以验证一下是否成功pip show requests看到版本信息就说明装好了。Windows上如果提示pip不是内部或外部命令不要慌多半是PATH没配好去系统环境变量里把Python的Scripts目录加进去比如C:\Users\你的用户名\AppData\Local\Programs\Python\Python312\Scripts然后重开命令行。2.4 命令行识别不了python/pip先别重装系统这几年我在带新手的过程中听到最多的一句报错是无法将“python”项识别为 cmdlet、函数、脚本文件或可运行程序的名称或者pip 不是内部或外部命令。这个问题的原因很统一系统在PATH环境变量里找不到对应的程序路径。解决办法按优先级排列重新运行Python安装包选择Modify把“Add Python to PATH”勾上安装完成后重启命令行手动打开“系统属性 - 环境变量”在Path里新增Python安装目录和它的Scripts目录如果还是不行检查是不是用了别名或快捷方式直接用py --version测试。还有一个很容易忽略的坑Windows PowerShell默认执行策略可能禁止运行.ps1脚本文件报错内容会提示“禁止运行脚本”。你只是想跑自己的.py脚本本来不需要管这个但如果你去激活虚拟环境时遇到类似的权限报错可以在PowerShell里执行下面命令Set-ExecutionPolicy -Scope CurrentUser RemoteSigned这条命令允许本地脚本运行同时仍然阻止未签名的远程脚本相对安全。实际用的时候要谨慎不要为了图省事把执行策略设成Unrestricted。3. 第一版下载必应每日壁纸的完整实现3.1 请求接口并解析JSON现在开始写正式代码。新建一个文件就叫bing_wallpaper.py。先写第一个函数专门负责请求接口并返回JSON数据。import requests def fetch_bing_index(num8): api_url https://cn.bing.com/HPImageArchive.aspx params { format: js, idx: 0, n: num, mkt: zh-CN, } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } resp requests.get(api_url, paramsparams, headersheaders, timeout10) resp.raise_for_status() return resp.json()这里几个细节说一下。params里定义了接口参数requests会帮你拼到URL后面不需要手动拼接字符串这也是为什么推荐用params而不是直接在URL里写死。timeout10表示连接和读取最多等10秒防止接口卡住让脚本永远挂在那里。resp.raise_for_status()是检查HTTP状态码如果返回4xx或5xx直接抛异常避免拿到错误内容继续往下走。拿到JSON后建议现在先打印看看结构if __name__ __main__: data fetch_bing_index() print(data.keys()) print(data[images][0])你会看到类似这样的信息{startdate: 20260601, url: /th?idOHR.SomePlace_ZH-CN1234567890_UHD.jpg, title: 某地风景, copyright: 某地风景 (© 摄影师), ...}这一步很关键因为后续所有解析逻辑都要基于真实返回的字段来写而不是靠猜。3.2 下载图片文件而不是拿到URL拿到图片链接之后要把它下载到本地。这里强调一个原则下载图片用流式读写不要一次性把整个文件读进内存。一张高清壁纸可能十几兆如果一次性resp.content接收内存占用会很难看而且文件一大就没法做分批写入。import os import requests def download_file(image_url, filename, headersNone): if headers is None: headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } with requests.get(image_url, headersheaders, streamTrue, timeout30) as resp: resp.raise_for_status() with open(filename, wb) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk)streamTrue会保持连接并让响应内容以流的方式提供iter_content(chunk_size8192)每次读8KB写进文件。注意if chunk这个判断因为有些情况下迭代可能产生空块写入空块虽然不会报错但没必要。还有一个非常现实的问题文件名怎么生成。壁纸接口返回的title是中文Windows文件系统不允许文件名里出现\ / : * ? |这些字符。处理办法是加一个清洗函数def clean_filename(name): invalid_chars r\/:*?| cleaned .join(_ if c in invalid_chars else c for c in name).strip() return cleaned if cleaned else wallpaper同时为了防止重名覆盖保存时最好加上日期和序号。3.3 主流程与命令行入口把前面的函数串起来加上命令行参数让脚本可以通过参数指定下载数量和保存目录。用argparse标准库处理不需要装额外依赖。import argparse import os import requests from datetime import datetime API_URL https://cn.bing.com/HPImageArchive.aspx BASE_URL https://cn.bing.com HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } def fetch_bing_index(num8): params { format: js, idx: 0, n: num, mkt: zh-CN, } resp requests.get(API_URL, paramsparams, headersHEADERS, timeout10) resp.raise_for_status() return resp.json() def clean_filename(name): invalid_chars r\/:*?| cleaned .join(_ if c in invalid_chars else c for c in name).strip() return cleaned if cleaned else wallpaper def download_file(image_url, filename): with requests.get(image_url, headersHEADERS, streamTrue, timeout30) as resp: resp.raise_for_status() with open(filename, wb) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk) def main(): parser argparse.ArgumentParser(description自动下载必应每日壁纸) parser.add_argument(--num, typeint, default8, help下载壁纸数量) parser.add_argument(--out, defaultwallpapers, help保存目录) args parser.parse_args() os.makedirs(args.out, exist_okTrue) data fetch_bing_index(args.num) images data.get(images, []) today datetime.now().strftime(%Y%m%d) for index, item in enumerate(images, start1): title clean_filename(item.get(title, fwallpaper_{today})) image_path item.get(url, ) if image_path.startswith(/): image_path BASE_URL image_path filename os.path.join(args.out, f{today}_{index:02d}_{title}.jpg) print(f正在下载: {filename}) try: download_file(image_path, filename) except Exception as exc: print(f下载失败: {image_path}, 错误: {exc}) if __name__ __main__: main()这段代码已经是一个完整可用的脚本了。注意我在download_file外层加了try/except保证某张图出错时循环不会中断其余壁纸继续下载。3.4 运行脚本并检查下载结果在虚拟环境中运行python bing_wallpaper.py --num 8 --out wallpapers运行时你会看到类似输出正在下载: wallpapers/20260601_01_某地风景.jpg 正在下载: wallpapers/20260601_02_另一处风景.jpg ...下载完成以后进入wallpapers目录检查文件。Windows下直接打开文件夹Linux/macOS可以用file命令查看图片信息file wallpapers/*.jpg如果看到JPEG image data说明下载成功。如果发现文件打开提示损坏大概率是下载到的不是图片而是错误页面HTML这种情况我会在后面的常见问题里专门说。4. 加一点“爬虫味”从通用网页提取壁纸链接4.1 没有JSON接口怎么办用BeautifulSoup提取图片链接必应接口好用但并不是所有壁纸站点都提供JSON接口。如果你想下载某个普通网页壁纸站里的图片就需要从HTML里自己提取链接。这里用requests加BeautifulSoup来做。先安装解析库pip install beautifulsoup4 lxml然后写一个解析页面中图片链接的函数from urllib.parse import urljoin from bs4 import BeautifulSoup def parse_image_urls(page_url, html): soup BeautifulSoup(html, lxml) image_urls [] for img in soup.select(img): # 很多站点使用懒加载真实地址在>headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Referer: https://example-wallpaper-site.com/, Accept: image/avif,image/webp,image/apng,image/*,*/*;q0.8, }这里的关键是把Referer设置成图片所在的页面地址。有些站点还需要登录Cookie那就要用到requests.Session()来维持会话先访问一次页面拿到Cookie再下载图片。Session和普通get的区别在于它会自动保存并携带服务端返回的Cookie适合模拟浏览器的连续行为。注意学习爬虫技术的目的是为了自动化采集公开数据不是让你去抓需要登录或付费才能访问的内容更不能用脚本高频请求拖垮对方服务器。4.3 下载策略优化重试、超时、文件校验通用网页下载器要考虑比必应接口更多的情况建议做好三件事重试、超时、文件校验。先写带重试的下载函数import time import requests def download_with_retry(url, filename, headersNone, retries3, delay2): for attempt in range(1, retries 1): try: with requests.get(url, headersheaders, streamTrue, timeout(5, 15)) as resp: resp.raise_for_status() content_type resp.headers.get(Content-Type, ) if image not in content_type: print(f警告: 返回内容不是图片, Content-Type{content_type}) return False with open(filename, wb) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk) return True except (requests.RequestException, TimeoutError) as exc: print(f第 {attempt} 次下载失败: {exc}) time.sleep(delay * attempt) return False这里超时设置成元组(5, 15)表示连接超时5秒读取超时15秒。重试间隔用delay * attempt也就是第一次失败等2秒第二次等4秒第三次等6秒避免一次次紧挨着重试。文件校验靠Content-Type里是否包含image来判断这能拦截掉“下载了一堆HTML错误页”的坑。5. 自动化的进阶玩法5.1 用Windows任务计划程序定时运行脚本能手动跑通只是第一步真正的“自动”要让它按计划自己跑起来。在Windows上我推荐用任务计划程序不需要安装额外软件。步骤很简单按Win R输入taskschd.msc回车右侧选择“创建基本任务”输入名称比如“下载每日壁纸”触发器选择“每天”设置一个合适的时间操作选择“启动程序”程序填python的完整路径参数填脚本的完整路径和参数起始于填脚本所在目录。这里一个坑是直接填python可能找不到因为任务计划程序不一定继承你命令行里的PATH。所以更稳妥的做法是填Python的绝对路径比如C:\Users\你的用户名\venv\Scripts\python.exe后面参数写D:\projects\bing_wallpaper.py --num 8 --out D:\wallpapers如果你不想要运行脚本时弹出黑色命令行窗口可以把python换成pythonw.exe它是Python自带的无窗口版本。注意pythonw.exe如果出错不会显示任何界面所以建议先正常运行几天确认稳定了再切换成无窗口模式。5.2 用cron和shell脚本定时运行在Linux或macOS上定时任务用cron更顺手。先写一个run_wallpaper.sh脚本文件#!/bin/bash cd /home/你的用户名/wallpaper-project source venv/bin/activate python bing_wallpaper.py --num 8 --out /home/你的用户名/wallpapers给脚本加执行权限chmod x run_wallpaper.sh然后用crontab -e添加定时任务0 8 * * * /home/你的用户名/wallpaper-project/run_wallpaper.sh /home/你的用户名/wallpaper-project/cron.log 21意思就是每天早上8点执行一次日志写入cron.log。把标准输出和错误都重定向到日志文件这一点非常重要否则cron任务出错时你根本不知道发生了什么。shell脚本配合cron是Linux下很常用的组合也是“shell脚本”入门时会反复出现的场景。5.3 多线程批量下载壁纸如果一次要下载几十张壁纸串行下载会明显变慢因为每张图的大部分时间都花在等待网络响应上。这种场景适合用多线程。Python实现多线程下载非常简单用concurrent.futures模块from concurrent.futures import ThreadPoolExecutor def download_many(url_file_pairs, max_workers4): with ThreadPoolExecutor(max_workersworkers) as executor: futures [] for url, filename in url_file_pairs: futures.append(executor.submit(download_file, url, filename)) for future in futures: try: future.result() except Exception as exc: print(f一个任务失败: {exc})为什么下载图片适合多线程而不是多进程因为这是IO密集型任务大多数时间都卡在等待网络上CPU基本闲着。多线程足以充分利用这段时间。Python有GIL对CPU密集任务多线程效果有限但网络下载场景恰恰不受影响。executor.submit会把任务丢给线程池执行future.result()会等待任务结束并取出结果或异常。我这里特意把异常处理放在主线程避免某个线程里的异常导致程序崩溃。线程数不建议开太高4到8个就够。开太多并发请求对方服务器很容易触发限流反而下载失败。5.4 更优雅封装成类并正确传递参数当脚本逻辑越来越多函数满天飞时就需要考虑封装成类。类的好处是可以把公共参数比如请求头、保存目录、日志对象集中管理代码可读性和可测试性都会提高。改造后的核心结构类似class WallpaperDownloader: def __init__(self, save_dirwallpapers, num8, max_workers4): self.save_dir save_dir self.num num self.max_workers max_workers self.session requests.Session() self.session.headers.update(HEADERS) os.makedirs(self.save_dir, exist_okTrue) def run(self): data self.fetch_index() pairs self.build_url_file_pairs(data) self.download_many(pairs)配合命令行参数你已经能实现类似“python bing_wallpaper.py --num 12 --out D:/壁纸 --workers 6”这样灵活的调用。这个“给另一个脚本传递参数”的能力在自动化运维中非常常用也是脚本从“写死”走向“通用”的关键一步。6. 常见问题速查与排查实录6.1 高频错误对照表我把这类脚本里最常出现的错误整理成一张表方便你遇到问题时直接对照。报错信息原因解决办法ModuleNotFoundError: No module named requests当前环境没有安装requestspip install requests注意确认激活了虚拟环境SSLError证书校验失败或本机证书问题先尝试升级pip install --upgrade certifi仍不行再临时用verifyFalseJSONDecodeError接口返回的不是JSON可能是HTML错误页打印响应内容检查URL和User-AgentUnicodeEncodeErrorWindows控制台编码不支持中文输出在脚本开头加sys.stdout.reconfigure(encodingutf-8)HTTP 403 Forbidden缺少User-Agent或Referer被防盗链拦截补全请求头尝试带Referer必要时用Session带CookieConnection timed out网络不通或目标地址响应过慢设置timeout参数并做重试不要无限制等待下载后文件打不开实际下载到的是HTML错误页检查Content-Type实现文件有效性校验pip 不是内部或外部命令Python的Scripts目录不在PATH里重新安装时勾选Add Python to PATH或手动加环境变量无法将“python”项识别为 cmdlet、函数、脚本文件或可运行程序的名称命令不在PATH中用py代替或修复PATH后重启终端这张表基本上覆盖了从安装到运行的绝大多数坑。如果你报错不在表里我的老办法是先看完整堆栈的第一行和最后一行第一行告诉你错误类型最后一行告诉你具体是哪一行代码触发。不要一上来就复制整段报错到处问自己先定位一下进步会快很多。6.2 我在实际使用中踩过的几个坑第一个坑是必应接口的图片URL是相对路径。我第一版脚本里直接拿返回的url去下载结果报了404后来打印出来才发现前面少了https://cn.bing.com。这个问题现在已经写进解析逻辑里了。第二个坑是Windows文件名非法字符。有张壁纸标题里带了斜杠下载时直接报错类似的还有问号、冒号。中文标题、空格、括号这些其实都能正常保存真正麻烦的是那几个Windows保留字符所以要加清洗函数。第三个坑是下载“成功”了但图片打不开。这种情况多半是服务器返回了HTML而不是图片但响应状态码依然是200。那时候我还没检查Content-Type直到批量下载完几百张图发现里面有几十个只有几KB的“图片”全是用图片链接访问后返回的提示页。从那以后我再写下载函数都会强制校验文件头或Content-Type。第四个坑是懒加载图片。有些网站HTML里的img标签src字段是占位图真实图片地址在>