资讯动态

Python爬虫实战:用requests和BeautifulSoup自动批量下载壁纸

发布时间:2026/10/6 9:37:37 来源:尧图企业网站定制
写一个Python脚本自动下载壁纸听起来像是“小题大做”但真正手动找过壁纸的人都懂打开网站、翻几页、右键另存为、挑选分辨率、去水印一整套操作下来十分钟起步还不一定每次都能找到满意的图。这个脚本解决的就是这个重复劳动一次性把搜索、翻页、筛选、下载全部自动化让电脑自己把壁纸囤好。这篇文章面向两类人刚学爬虫的Python爱好者还有纯粹想省事、希望电脑每天都有新壁纸的普通用户。我会把从环境准备到脚本运行的完整过程拆开讲清楚包括页面解析原理、下载逻辑设计、常见报错排错最后再聊几个进阶玩法。整个过程不依赖任何重型框架用requests加BeautifulSoup就能跑通代码结构也是一步步搭起来的方便你在此基础上改成自己的版本。1. 先想清楚需求再动手下载方案的整体设计思路1.1 明确脚本要解决的四个关键问题写脚本之前我习惯先把需求拆成问题清单。自动下载壁纸这个需求表面上看只是“把图片保存到本地”但落到代码层面至少要回答四个问题图片从哪来、怎么找到图片的真正下载地址、保存成什么格式、以及怎么避免被网站限制。第一个问题“图片从哪来”决定了整个方案的技术路线。市面上的壁纸站大致分两类一类是纯静态页面图片链接直接写死在HTML里比如很多个人维护的壁纸合集站另一类是动态渲染站点图片信息需要通过接口异步加载比如一些瀑布流社区。选静态站点做入门案例是最稳的做法解析难度低、逻辑清晰、不容易被复杂的反爬机制劝退。第二个问题是核心也是最容易被新手卡住的地方。壁纸站的列表页通常展示的是缩略图也就是一张被压缩过的小图直接下载下来根本没法当壁纸用。真正的原图链接往往藏在详情页里或者可以通过缩略图链接规律推导出来。这一步需要观察URL结构我会在第三节详细演示怎么找规律。第三个问题看似简单实则隐藏着坑。图片的扩展名不一定是.jpg有的站用的是.webp或者带参数的无后缀链接。保存文件时要根据Content-Type响应头动态判断或者干脆从URL里提取扩展名不能写死在代码里。第四个问题是爬虫脚本能否长期跑起来的关键。有些站对请求频率敏感短时间内请求太多会返回403或429状态码。解决方案无非是放慢节奏、加随机延时、设置合理的请求头。这些细节虽然不起眼但少了任何一环脚本用不了几次就会罢工。1.2 为什么选择requests加BeautifulSoup这套组合确定了“静态页面解析”这个方向后技术选型就顺理成章了。Python生态里做爬虫的库很多但requests加BeautifulSoup的组合对于这个场景来说性价比最高。requests负责HTTP请求它把底层的TCP链接、SSL握手、cookie管理这些复杂细节全部封装好了。你只需要告诉它“我要访问这个URL带上这些请求头”就能拿到HTML源码。BeautifulSoup负责解析HTMl它把文档转换成一棵可遍历的树结构支持通过CSS选择器查找元素代码写起来非常直观。为什么不选Scrapy这种重型框架因为脚本的规模摆在那里总共就几十行代码Scrapy的工程化结构反而成了负担。为什么不直接用正则表达式提取图片链接因为正则表达式对HTML结构变化极度敏感稍有改动就要重写匹配规则BeautifulSoup这类解析器只关心DOM结构和class类名健壮性好得多。为什么不试试Selenium模拟浏览器那是给JavaScript重度渲染的站点准备的方案静态解析能搞定的事情没必要开一个浏览器性能和稳定性都是浪费。1.3 脚本运行的整体流程设计这个脚本的运行流程可以简化为五个环节构造请求、解析列表、提取链接、下载保存、输出结果。构造请求这一步需要确定三样东西目标URL、请求头、超时时间。目标URL包含搜索关键词和页数参数请求头至少要带User-Agent用来模拟真实浏览器访问。解析列表就是拿到HTML后用BeautifulSoup找到所有图片条目所在的元素遍历它们提取缩略图链接。提取链接是找到原图地址可能是直接解析属性可能是做字符串替换也需要检查链接的有效性。下载保存是最需要稳住的一步文件流式写入、异常重试、断点续传都在这层处理。最后输出结果把下载成功的图片列表和统计信息打印出来。这个流程的好处在于模块化。每个环节都是独立的函数后续想调整哪一部分比如换成其他壁纸站只需要改对应的函数不需要动整体框架。2. 环境搭建与依赖准备装好Python和爬虫三件套2.1 检查Python环境并安装依赖库如果你从来没装过Python直接去官网下最新稳定版安装包就行。安装的时候有一个非常关键的勾选项Add Python to PATH很多人漏掉这一步导致后续在命令行里输python指令报错找不到命令。下载安装教程到处都有这里不多说只说一个验收入口。装完之后打开终端输入python --version如果输出了版本号就说明环境没问题。然后需要安装三个第三方库requests、beautifulsoup4、lxml。lxml是解析器比Python自带的html.parser快不少大页面解析时能明显感觉到差别。pip install requests beautifulsoup4 lxml在国内网络环境下如果pip下载速度慢可以临时指定镜像源pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests beautifulsoup4 lxml2.2 项目目录与虚拟环境的小建议虽然这个脚本只有一个文件但我还是建议按一个小型项目的标准来组织目录。不用太复杂一个项目文件夹里放脚本本体、一个存放壁纸的子目录、一份requirements.txt就够了。wallpaper_downloader/ ├── wallpaper_downloader.py ├── requirements.txt └── wallpapers/requirements.txt的内容就是把三个依赖库写进去方便以后在别的机器上重建环境。虚拟环境这个东西在你只跑一个脚本的时候确实可有可无。但如果你以后会接触更多Python项目建议还是养成用虚拟环境的习惯每个项目一套独立依赖互不干扰。创建和激活命令也很简单python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate2.3 为什么请求头里的User-Agent这么重要这算是我最早踩过的坑。一开始写爬虫脚本直接requests.get(url)一把梭结果返回的是403 Forbidden。原因很简单服务器在检查请求的User-Agent你没带上这个标识服务器判断你是机器请求直接拒绝。User-Agent本质上是浏览器在HTTP请求中附带的“自我介绍”。正常用浏览器访问时每个请求都会带上当前浏览器版本、操作系统等信息。我们写爬虫时如果不手动指定requests库会用默认的python-requests/版本号这种标识在服务端看来基本等同于“我是机器人”的标签。解决办法是在请求头里放一个真实浏览器的User-Agent。从浏览器开发者工具里复制一份或者直接用网上整理的常用UA字符串都行。这个细节会贯穿整个脚本所有HTTP请求都要带上我在第三节的代码里会演示具体实现。3. 核心代码实现解析图片直链并批量下载3.1 观察目标站点的URL与页面结构规律为了让教程具有可复现性我选一个结构规律性强、在公开网络教程中经常被解析的壁纸站来做演示。不同站的页面结构差异很大但解析思路是通用的先从列表页的URL参数开始观察再找到缩略图链接和原图链接之间的关系。在这个壁纸站上按关键词搜索的URL是这种格式https://wallhaven.cc/search?qlandscapecategories010purity100page2参数含义如下q搜索关键词这里搜的是landscape也就是风景壁纸categories内容分类010代表generalanimepurity内容纯度100表示只显示safe等级的壁纸page页码从1开始翻页这个URL结构有两个对脚本友好的点。第一搜索词和页码都是URL参数翻页就是改page的值循环构造URL非常简单。第二壁纸列表页的缩略图链接规律性极强每一张壁纸的缩略图地址是这个样子https://th.wallhaven.cc/small/xx/wallhaven-123456.jpg而原图链接是这个样子https://w.wallhaven.cc/full/xx/wallhaven-123456.jpg比较一下就能发现把th.wallhaven.cc/small替换成w.wallhaven.cc/full缩略图就变成了原图地址。这就是我前面提到的“链接规律”。在代码里体现为一行.replace操作非常优雅。如果不依赖于这种替换更通用的做法是访问每一张壁纸的详情页在详情页中找到原图链接的meta标签。这个方案更稳但会多出很多请求速度慢一些。具体用哪种方案要看站点的特性能走捷径就走捷径。3.2 请求与解析模块拿到HTML并提取图片链接现在开始写代码。先把请求操作封装成一个函数带上User-Agent加超时控制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.0.0 Safari/537.36 } def fetch_html(url): resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text这里有两个细节值得注意。resp.raise_for_status()会把4xx和5xx的HTTP状态码变成异常抛出来避免后续代码拿到一个错误页面还继续解析。resp.encoding那一行是为了避免中文乱码对于纯英文的壁纸站影响不大但保留这个习惯没坏处。接下来是解析模块。用BeautifulSoup读取HTML定位到包含图片列表的区域提取缩略图链接def parse_image_links(html): soup BeautifulSoup(html, lxml) links [] for figure in soup.select(figure.thumb): img figure.select_one(img) if img and img.get(data-src): thumb_url img[data-src] full_url thumb_url.replace(th.wallhaven.cc/small, w.wallhaven.cc/full) links.append(full_url) return links这里用figure.thumb作为选择器是因为在该站点中每一张壁纸卡片都是一个figure标签class属性包含thumb选择器写起来比较精简。如果换一个站点就是改这个选择器的事整个函数的结构可以原样复用。img标签的图片地址从data-src取而不是src。原因在于很多站点做了懒加载优化真正的图片地址存在data-src属性里src只是一个占位图。这个坑很容易踩到解析出来的链接全是同一个占位图下回来全是无用文件。3.3 下载模块流式写入、重试机制与文件名处理拿到原图链接列表后进入下载模块。这个模块最核心的三个功能是大文件用流式写入避免内存暴涨、下载失败要自动重试、文件名要唯一且合法。先看代码import time import os def download_image(img_url, save_dir, timeout30, retries3): fname img_url.rsplit(/, 1)[-1] save_path os.path.join(save_dir, fname) if os.path.exists(save_path): print(f[跳过] {fname} 已存在) return False for attempt in range(retries): try: resp requests.get(img_url, headersHEADERS, streamTrue, timeouttimeout) 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 except Exception as e: wait 2 ** attempt print(f[重试] {fname} 第 {attempt 1} 次失败{e}{wait} 秒后重试) time.sleep(wait) return False分块写入的原理可以这样理解如果不用streamTruerequests会把整个图片一次性读进内存。一张壁纸动辄几MB一次下载几十张内存占用就会很夸张。设置streamTrue后响应体以字节流的方式分批到达每拿到一个块就立刻写入磁盘整个流程内存占用非常稳定。重试机制用了一个简单的指数退避策略。第一次失败等1秒第二次等2秒第三次等4秒。这种策略在应对服务器偶发抖动时很有效不会因为集中重试把对方接口打爆。跳过逻辑是检查目标路径是否已存在这是增量下载的基础保证脚本跑第二次时不会重复下载。3.4 主流程翻页、循环下载与结果统计把前面几个模块组合起来再加上控制翻页数量的逻辑就是完整的脚本了。def main(): import argparse parser argparse.ArgumentParser(descriptionPython自动下载壁纸) parser.add_argument(-q, --query, defaultlandscape, help搜索关键词) parser.add_argument(-p, --pages, typeint, default3, help下载页数) parser.add_argument(-o, --output, defaultwallpapers, help保存目录) args parser.parse_args() os.makedirs(args.output, exist_okTrue) total_downloaded 0 for page in range(1, args.pages 1): search_url ( fhttps://wallhaven.cc/search?q{args.query} fcategories010purity100page{page} ) print(f[信息] 正在解析第 {page} 页{search_url}) try: html fetch_html(search_url) except Exception as e: print(f[失败] 第 {page} 页请求失败{e}) continue image_links parse_image_links(html) print(f[信息] 第 {page} 页找到 {len(image_links)} 张图片) for idx, link in enumerate(image_links, 1): ok download_image(link, args.output) if ok: total_downloaded 1 print(f[进度] 第 {page} 页{idx}/{len(image_links)} 张) time.sleep(0.5) # 礼貌等待避免请求过于频繁 print(f[完成] 本次共下载 {total_downloaded} 张壁纸到 {args.output} 目录)用argparse做命令行参数解析是为了让脚本的复用性更好。以后想下载其他关键词的壁纸不用改代码直接执行python wallpaper_downloader.py -q nature -p 5 -o nature_wallpapers主流程里每个请求之间加了0.5秒的延时。很多人觉得这是浪费时间但实测下来这个延时能大幅降低被服务器限流的概率。下载频率过高导致IP被临时封禁得不偿失零点几秒的等待换来长期稳定运行这笔账很划算。3.5 脚本运行演示与效果验证在终端运行脚本后输出效果大致是这样[信息] 正在解析第 1 页https://wallhaven.cc/search?qlandscapecategories010purity100page1 [信息] 第 1 页找到 24 张图片 [进度] 第 1 页1/24 张 [重试] wallhaven-852741.jpg 第 1 次失败HTTP 4291 秒后重试 [进度] 第 1 页2/24 张 ... [完成] 本次共下载 68 张壁纸到 wallpapers 目录下载完成后进到wallpapers目录里看文件名保留的是站点原生的命名格式比如wallhaven-852741.jpg。这种命名的好处是链接唯一、不会有重复坏处是没法直接看出图片内容。想要更友好的文件名可以在保存时拼接关键词和缩略图尺寸比如landscape_3840x2160_001.jpg这就是需求层面的事情了脚本里加一个格式化函数就行。4. 踩坑实录常见报错与排查技巧4.1 HTTP 403和429被服务器拒绝时的应对策略这两个状态码是爬虫脚本最常撞上的墙。403代表服务器拒绝了你的请求通常是因为User-Agent被识别、IP被限制或者缺少必要的Cookie。429代表请求太频繁触发速率限制服务器明确告诉你“太快了歇一会儿”。403的排查步骤很简单先用浏览器手动打开同一个URL确认页面正常。然后把浏览器请求头里的User-Agent和Cookie复制到脚本里看问题是否解决。如果还是403大概率是IP被暂时限制那就只能降低请求频率或者换个网络环境再试。429的处理思路则是退避。脚本里的指数退避逻辑就是为这个场景准备的。当服务器返回429时说明当前请求速率过快不要硬闯等几秒再试。还有一种稳妥的做法是把延时策略加剧抖动比如在固定延时基础上加随机数模拟人类操作节奏time.sleep(0.5 random.random())4.2 文件名乱码与非法字符处理壁纸站点常常使用UTF-8编码的文件名如果图片URL中包含中文或其他非ASCII字符在Windows的默认编码环境下保存文件时可能抛出UnicodeEncodeError或者保存成乱码文件名。最简单的解决思路是从URL里只取最后的文件名片段用os.path.basename做提取。如果提取出来的文件名仍然包含不合法字符比如Windows系统不允许文件名包含: * ? |这些符号就做一次清洗import re def clean_filename(name): return re.sub(r[\\/*?:|], _, name)另外如果脚本的print输出中文乱码可以在脚本开头加一行import sys sys.stdout.reconfigure(encodingutf-8)这个处理在Windows终端下特别常用能避免控制台输出看到一堆乱码。4.3 下载到一半连接断开大文件下载时最常见的异常是连接中断。服务器端主动关闭连接、网络波动、超时时间设置过短都可能导致请求失败。处理办法在download_image函数中已经内置了循环重试机制。但有一个隐藏问题值得注意文件已经写了一半下次重试时又重新从头开始写导致磁盘上残留一个损坏的半截文件。解决方法是把下载过程拆成两步先下载到临时文件全部完成后改名成目标文件名。tmp_path save_path .part with open(tmp_path, wb) as f: for chunk in resp.iter_content(chunk_size8192): f.write(chunk) os.rename(tmp_path, save_path)如果下载中断tmp文件会被覆盖不会污染最终文件。严谨的写法是每次都覆盖tmp文件这样下次重试相当于断点续传的简化版虽然是从头开始但不会留下半成品。4.4 requests常见异常类型速查我把日常使用requests时最常碰到的异常类型整理成了一张表方便排查时对照异常类型触发场景典型原因requests.exceptions.ConnectTimeout请求超时网络不通、目标域名解析失败requests.exceptions.ReadTimeout服务器响应超时服务器处理慢、图片文件过大requests.exceptions.ConnectionError连接建立失败网络中断、目标IP不可达requests.exceptions.HTTPError状态码4xx或5xx链接失效、被拒绝、限制访问requests.exceptions.TooManyRedirects重定向次数过多URL逻辑错误、被恶意跳转排查思路记住一个顺序先确认网络通不通再确认目标URL是否有效最后确认有没有被服务器限制。网络问题检查代理设置、DNS解析URL问题换浏览器打开试一下服务器限制看状态码对应调整请求策略。按这个顺序排查绝大部分问题都能定位到具体原因。5. 进阶扩展多线程提速、定时任务与脚本维护5.1 多线程并发下载提速单线程下载一张壁纸需要几秒下载几十张就是几分钟速度确实慢。升级方案很简单把下载任务丢到线程池里并行执行。Python的ThreadPoolExecutor用起来非常顺手from concurrent.futures import ThreadPoolExecutor, as_completed def download_many(image_links, save_dir, workers4): with ThreadPoolExecutor(max_workersworkers) as executor: futures { executor.submit(download_image, link, save_dir): link for link in image_links } for future in as_completed(futures): result future.result()线程数不是越大越好。服务器有承受上限设置的线程数越多请求越密集越容易触发429限流。我实测下来4到8个线程是比较平衡的值速度快了2到4倍同时触发限流的概率还在可接受范围内。想真正大幅提速还是得靠调整请求策略而不是无限堆线程。5.2 让脚本按计划运行定时换壁纸脚本只能手动跑自动化程度还不够。把脚本挂到系统定时任务里每天自动下载一批新壁纸才算真正闭环。Windows用户打开任务计划程序创建一个基本任务触发器设为每天某个固定时间操作选择启动程序程序填python参数填脚本路径加上命令行参数。就这一套配置就能让脚本在指定时间自动运行。macOS和Linux用户直接写crontab更方便。在终端输入crontab -e添加一行定时规则0 8 * * * cd /path/to/project python wallpaper_downloader.py -q landscape -p 2 -o wallpapers这段规则的含义是每天早上8点执行一次。配合系统自带的壁纸轮换功能把壁纸目录指向脚本保存的文件夹每天睁眼就有新壁纸可用。这一步就是把“自动下载壁纸”从工具升级成服务。5.3 适配其他壁纸站的通用思路这套脚本看起来只能用于特定壁纸站但解析逻辑其实可以抽象成一套适用于大多数图片站的模板。换站的时候只需要重写三个函数构造搜索URL、解析图片列表、从缩略图推导原图链接。构造搜索URL的逻辑在main函数里核心就是搞明白目标站的翻页参数名和搜索参数名。解析图片列表就是用BeautifulSoup找到图片卡片的DOM容器和class名。推导原图链接则要观察缩略图和原图的URL结构差异可能是替换域名可能是去掉裁剪参数也可能是拼接ID。这三个点换掉一个通用爬虫就变成了另一个站点的爬虫剩下的下载、重试、增量保存这些基础设施完全不用动。这就是前面把流程模块化的好处以后换站不是从零开始而是替换零件。5.4 长期维护脚本的两点心得第一个建议是控制请求频率不要去挑战服务器的容忍度。很多人写爬虫总嫌弃延时浪费生命把间隔时间调成0结果就是几分钟后整个IP被拉黑脚本彻底不能用了。自动化脚本的核心是稳定产出细水长流远比一波跑完持久。第二个建议是保留运行日志。早期版本我习惯用print输出运行状态脚本崩溃后根本不知道跑到哪一步、失败原因是什么。后来我开始在关键节点写日志文件用Python内置的logging模块记录时间、操作、结果排查问题效率提升了不知道多少倍。脚本学习阶段用print没毛病但如果你想让它长期定时跑logging一定要尽早加上。import logging logging.basicConfig( filenamedownloader.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s )把日志文件放在项目目录几个月跑下来壁纸下载记录一目了然。哪个链接失效了、哪天触发了限流、下载总量是多少都有迹可循。这个概念会一直跟着你进入更复杂的爬虫项目。我自己在维护这类脚本的过程中最大的体会是脚本是否好用比拼的不是技术多炫酷而是边界情况处理得多完善。一开始下载失败会直接中断整个脚本后来才改成失败重试加跳过的机制。一开始不懂加请求头被403拦截后一脸茫然后来才明白有些服务器是按用户代理识别访问者的。这些都是踩坑换来的经验。你照着这份教程把脚本跑通只是第一步把它用起来、根据实际需求改造成适合自己的工具才是真正有收获的过程。墙裂建议你动手写一遍遇到问题不要怕现在的报错信息比从前友好太多了跟着提示改代码很快就能找到感觉。跑通了之后不妨给它加一两个自己的小功能比如按分辨率过滤、按颜色分类、记录下载历史你会发现这个脚本以后是你桌面环境里利用率最高的自动化程序之一。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑