资讯详情 Python爬虫实战:多线程自动下载高清壁纸脚本全解析
📅 2026/10/8 10:23:42
很多人刚接触Python时第一个想做的“有点用”的小工具往往不是爬网页数据而是“帮我自动下载壁纸”。这个需求听起来简单实际上它把Python爬虫的核心环节几乎都串了一遍网络请求、数据解析、文件保存、异常处理、并发下载。我花了一个周末把这个脚本完整写了出来并且在实际跑的过程中踩了不少坑。这篇文章就把从需求拆解到代码实现、再到问题排查的全过程都讲清楚代码可以直接复制使用有一定编程基础的也能从这里学到一套通用下载脚本的写法。1. 需求拆解与整体方案设计1.1 这个脚本到底要解决什么问题“自动下载壁纸”听起来一句话拆开来看其实是四个具体问题去哪里找壁纸需要一个稳定、免费、允许自动下载的图片来源。怎么拿到壁纸清单知道自己要哪些图而不是把整个网站搬下来。怎么下载图片一个可靠的HTTP下载环节断网、超时、被网站限制都要考虑。下载完往哪放按关键词或分辨率分类保存避免几百张图堆在一个文件夹里。这四个问题对应到技术方案上分别是数据源、解析逻辑、下载逻辑、文件管理。一开始我纠结了很久要不要直接用Scrapy这种爬虫框架来写。后来发现完全没必要——杀鸡用牛刀。Scrapy适合大规营收级爬虫壁纸下载这种小工具用requests配合Python标准库就足够了代码量更少逻辑也更直观新手读起来不会懵。最终我选择的方案是requests做网络请求配合网站提供的公开API获取壁纸列表再用concurrent.futures实现多线程下载最后用pathlib管理文件目录。全程只依赖一个第三方库其他全用Python自带模块。1.2 为什么优先选带API的壁纸源写这个脚本时我调研了好几个壁纸网站发现一个关键区别有API的和没有API的写起来完全是两种难度。有API的网站你的请求会得到一份结构化的JSON数据图片地址、分辨率、尺寸都在里面直接用字典取值就行。没有API的网站你得先抓HTML页面再用BeautifulSoup或者正则表达式去匹配图片标签还要处理各种反爬机制。两种方案我都试过有API的明显更稳也更容易调试。有些壁纸网站除了API还提供免费的关键词搜索、分辨率过滤、热度排序这些功能这些参数放在URL里就能用。选择图片来源时另一个重要标准是它是否允许自动下载。有的图片网站版权限制严格会封锁脚本有的本质上就是个素材库允许程序访问。我推荐优先找后者既省心又安全。当然不能保证每个壁纸站都有API所以我在后面第5.3节专门补了一种“只有HTML页面时怎么用BeautifulSoup解析”的通用方案。两种思路都有你以后遇到任何网站都能套用。2. 环境准备与依赖安装2.1 如果pip报错怎么办说实话很多人的脚本写好了跑不起来问题不在代码而是环境没配对。尤其是Windows用户经常在终端敲pip install requests结果冒出一句pip : 无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称当场劝退。这个报错的本质很简单你的Python装好了但它的Scripts目录没有加到系统PATH里所以终端找不到pip命令。解决办法有一个万能公式用python -m pip来代替pip。python -m pip install requestspython -m pip的意思是“让Python自己去运行pip模块”这个写法不依赖PATH只要Python本身能启动就一定有效。遇到任何pip相关报错先用这个命令试一遍大概率能解决。如果下载速度特别慢依赖包从国外服务器拉取很常见可以临时换用国内镜像源python -m pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple清华源、阿里源、豆瓣源都行挑一个你网络环境下响应快的。装完依赖之后验证安装是否成功python -c import requests; print(requests.__version__)如果打印出版本号说明环境就绪。之前我看到很多人卡在python下载cv2、numpy库安装这类问题上核心逻辑和这里完全一样——先解决python -m pip再解决镜像源基本不会被卡住。2.2 项目目录与代码文件规划我习惯动手前先把目录建好不然图片下载到一半找不到保存位置很烦。推荐结构wallpaper_downloader/ ├── downloader.py # 主脚本 ├── requirements.txt # 依赖清单 └── images/ # 下载的壁纸保存目录自动创建requirements.txt是给Python生态用的依赖清单内容只有一行requests这样别人拿到项目后执行python -m pip install -r requirements.txt就能一键装齐依赖。不管项目多小养成把依赖写进requirements的习惯以后换电脑或者分享给别人都会很省事。2.3 只会用到requests一个第三方库有些教程一上来就要求安装requests、beautifulsoup4、lxml、tqdm四五个库其实这个项目核心只用requests就够了。因为壁纸源返回的是JSON数据不需要解析HTML所以BeautifulSoup我留到第5.3节讲备用方案时才引入。tqdm进度条库也属于“锦上添花”的选项为了保持代码简洁我直接用手写的下载计数来替代。少依赖就是少故障这是我在实际项目中反复体会到的。3. 核心代码实现与逐段解析完整的代码分四个函数来写请求会话、搜索列表、下载图片、主流程。每一段我都会讲清楚设计思路方便你直接抄作业也方便改成自己的场景。3.1 构建请求会话Session复用与伪装UAimport requests from pathlib import Path # 全局会话复用TCP连接下载多张图片时性能提升明显 session requests.Session() session.headers.update({ 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, Accept: application/json, text/plain, */*, }) DOWNLOAD_DIR Path(./images) TIMEOUT (5, 15) # (连接超时, 读取超时) 单位秒 MAX_RETRY 3 # 单张图片最多重试次数为什么用全局的session而不是每次requests.get()因为requests.Session会复用底层的TCP连接下载大量图片时不需要每张图都重新建立握手速度能快不少。加上一个常见的浏览器User-Agent是为了让服务器认为请求来自浏览器而不是脚本在访问一些对爬虫敏感但本身允许自动化的图源时能降低被拒绝的概率。超时参数我特意设成元组连接超时5秒、读取超时15秒。很多人的脚本卡住就是只设置了连接超时如果服务器一直不返回数据读取阶段就永远等下去。设成元组两种情况都能兜底。3.2 搜索并解析壁纸清单def search_wallpapers(keyword: str, page: int, at_least: str 1920x1080) - list: 从壁纸API获取指定关键词、指定页数的壁纸元数据列表。 返回的每个元素是包含图片直链、分辨率的字典。 url https://wallhaven.cc/api/v1/search params { q: keyword, # 搜索关键词比如 nature、landscape categories: 111, # 三个位置分别代表通用/动漫/人物1为开启 purity: 100, # 100表示只看安全内容 atleast: at_least, # 最小分辨率 sorting: toplist, # 按热度排序 order: desc, page: page, } try: resp session.get(url, paramsparams, timeoutTIMEOUT) resp.raise_for_status() data resp.json() return [ { id: item[id], url: item[path], resolution: item[resolution], } for item in data.get(data, []) ] except requests.RequestException as e: print(f[搜索失败] 第{page}页出错: {e}) return []这段逻辑的本质是把搜索条件翻译成URL参数再把服务器返回的JSON翻译成我们需要的信息结构。params字典里的每个键值最终会被requests拼接到URL末尾所以你完全可以直接把API文档里的查询参数搬进来比如颜色过滤、宽高比过滤。resp.raise_for_status()是个好习惯它会把4xx、5xx的HTTP错误主动抛成异常避免你拿到一个”看起来成功但内容错误”的响应还继续往下跑。而resp.json()是requests自带的JSON解析方法省掉了json.loads的步骤。有的壁纸站API可能需要你在个人设置里申请一个API Key加在params里就行。优先使用官方公开参数不要自己去猜接口格式任何能在API文档里查到的内容都不要靠正则去HTML里硬抠。3.3 单张图片的下载函数流式写入与重试机制def download_image(img_info: dict, save_dir: Path) - bool: 下载单张壁纸并保存到本地。 返回True表示成功包含文件已存在的情况。 url img_info[url] # 将URL里的非法字符替换成下划线避免文件名报错 filename f{img_info[id]}_{img_info[resolution]}.jpg filepath save_dir / filename # 文件已存在且非空则跳过避免重复下载 if filepath.exists() and filepath.stat().st_size 0: print(f[跳过] {filename} 已存在) return True for attempt in range(1, MAX_RETRY 1): try: with session.get(url, streamTrue, timeoutTIMEOUT) as resp: resp.raise_for_status() # 检查响应头里给出的文件大小异常数据直接放弃 content_length int(resp.headers.get(content-length, 0)) if content_length 1000: print(f[异常] {filename} 文件太小疑似错误响应) return False with open(filepath, wb) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk) print(f[完成] {filename} ({filepath.stat().st_size} 字节)) return True except requests.RequestException as e: print(f[重试] {filename} 第{attempt}次失败: {e}) if attempt MAX_RETRY: import time time.sleep(2 ** attempt) # 指数退避2s, 4s return False下载图片的核心是streamTrue配合iter_content()。如果不用streamTruerequests会把整个响应体一次性加载到内存里。壁纸动辄几MB同时下载8张就是几十MB内存没必要。开启了流式模式图片数据会按8KB的大小切片写入磁盘内存占用恒定再大的图片也不怕。去重逻辑是检查文件名是否已经存在且文件大小大于0。这是最简单可靠的去重方式因为壁纸ID是唯一的同一个ID下载两次对应的就是同一个文件名。为什么还要检查大小因为之前可能下载失败过留下了0字节的空文件如果不检查这个坏文件会被永久跳过。重试采用指数退避策略第一次失败等2秒第二次失败等4秒第三次不再重试。这种策略比固定间隔更礼貌给服务器留出恢复窗口同时避免自己陷入无限重试的循环。3.4 并发下载与控制逻辑from concurrent.futures import ThreadPoolExecutor, as_completed def download_batch(results: list, save_dir: Path, max_workers: int 6): 并发下载一批壁纸返回成功数量。 success_count 0 with ThreadPoolExecutor(max_workersmax_workers) as executor: future_map { executor.submit(download_image, item, save_dir): item[id] for item in results } for future in as_completed(future_map): if future.result(): success_count 1 return success_count def main(): keyword nature # 搜索关键词 pages 3 # 下载前3页 at_least 1920x1080 # 最小分辨率 save_dir DOWNLOAD_DIR / keyword.replace( , _) save_dir.mkdir(parentsTrue, exist_okTrue) all_results [] for page in range(1, pages 1): page_results search_wallpapers(keyword, page, at_least) if not page_results: print(f[提示] 第{page}页没有获取到壁纸提前结束) break all_results.extend(page_results) print(f[信息] 共获取到 {len(all_results)} 张壁纸) success download_batch(all_results, save_dir, max_workers6) print(f[汇总] 成功下载 {success} 张失败 {len(all_results) - success} 张) if __name__ __main__: main()为什么用ThreadPoolExecutor而不是自己手动创建一堆线程因为线程池帮你管理线程的生命周期下载完自动回收主流程只需要关心提交任务和收集结果。这里选择多线程而不是多进程原因在于下载图片是典型的I/O密集型任务——大部分时间都在等网络响应多线程就足够了。max_workers6是我实测后觉得比较合适的值。太小下载太慢太大容易触发服务器封IP。如果你网络环境好、对方服务器也宽松可以适当调到10但6到8是大多数情况下都不会出问题的区间。一个要注意的小细节save_dir.mkdir(parentsTrue, exist_okTrue)这行代码里parentsTrue表示如果父目录不存在就一起创建exist_okTrue表示目录已存在时不报错。没有这两行脚本第一次跑就会崩溃。4. 踩坑记录与问题排查4.1 下载下来的图片打不开或者全是乱码这是一个非常经典的问题文件后缀是.jpg但实际保存的是JSON、HTML或者错误提示文本。原因通常是两种情况一是请求被服务器判定为异常请求返回了一个错误页。这时你把content-length打印出来会发现非常小。我在下载函数里加的那个if content_length 1000判断就是为了拦截这种情况。二是图片URL是一次性的签名链接过几分钟就失效。这种情况只能尽快下载或者换更稳的图源。排查思路手动用浏览器打开下载失败的URL看能不能正常显示。如果浏览器也打不开说明是URL本身失效代码没问题如果浏览器能打开但脚本下载下来是乱码说明请求头或者会话状态有问题。4.2 频繁出现连接超时和SSL错误Connection timed out和SSL: CERTIFICATE_VERIFY_FAILED是我实际运行时遇到最多的两类异常。连接超时的常规处理降低并发数。同时发起大量连接会让服务器认为你是攻击流量。增加超时时间。有些服务器响应慢5秒的读取超时确实可能不够。重试。网络抖动是常态重试2到3次就能恢复。SSL证书错误比较少见原因一般是目标站的证书链不完整或系统证书过期。可以设置verifyFalse跳过证书验证resp session.get(url, streamTrue, verifyFalse, timeoutTIMEOUT)但要注意verifyFalse会同时产生一个警告而且会让连接的安全性降低所以只在你确认目标源可信的前提下使用。4.3 同一批壁纸反复下载又占空间又浪费流量我在写完第一版后就遇到过这个问题——明明上次下载过了这次再跑还是会重新下。后来加了文件存在检查问题迎刃而解。但是单纯判断文件存不存在还不够因为中断的下载会留下0字节的空文件。所以我同时判断了st_size 0双条件才跳过。这套判断逻辑本质上是把“文件记录”当成“下载已完成的凭证”简单却有效。如果你觉得不够严谨还可以在文件名里加上文件大小哈希但普通人下壁纸没必要搞那么复杂。4.4 常见问题速查表问题现象大概率原因解决方案pip命令无法识别Python的Scripts目录没加入PATH改用python -m pip install下载速度极慢默认源在国外使用清华/阿里镜像源或者适当调大并发数图片下载后全是文本/JSON被反爬机制拦截检查User-Agent确认请求头排查是否访问了需要登录的接口连接超时频繁并发过高或目标源网络不稳降并发到3~4加长超时时间重试3次SSL证书验证失败目标站证书链异常verifyFalse绕过前提是源可信文件重名导致覆盖不同图片有相同ID字段文件名里加入分辨率或URL哈希值区分5. 进阶扩展让脚本真正自动起来5.1 自动匹配当前屏幕分辨率每个人屏幕不一样1920x1080的壁纸放到2K屏上会拉伸模糊。可以读取当前屏幕分辨率自动传入API的atleast参数这样下载的壁纸永远适配自己的显示器。Windows下读取屏幕分辨率可以用Python的ctypesimport ctypes def get_screen_resolution_windows(): user32 ctypes.windll.user32 return user32.GetSystemMetrics(0), user32.GetSystemMetrics(1)macOS和Linux也有各自的命令行工具system_profiler和xrandr都能拿到。这个功能的本质是让程序感知环境而不是让用户手动告诉程序体验感会好很多。5.2 定时运行脚本每天都有新壁纸把脚本交给任务计划程序就能实现“每天自动下载一批新壁纸”的效果。Windows用户在任务计划程序里新建任务触发器选择“按计划每天”操作里填Python解释器的路径和脚本路径程序: C:\Python312\python.exe 参数: D:\wallpaper_downloader\downloader.py 起始于: D:\wallpaper_downloaderLinux用户更简单在crontab里加一行0 8 * * * python3 /home/user/wallpaper_downloader/downloader.py这行的含义是每天早上8点执行一次脚本。配合壁纸轮播工具把图片目录指向下载目录基本等于全自动换壁纸了。5.3 没有API的壁纸站怎么改造成HTML解析版不是所有壁纸站都有API但绝大多数壁纸站都有搜索页面。解析HTML的思路是获取搜索页HTML文本用正则或BeautifulSoup提取页面里的图片链接。一个基于BeautifulSoup的通用思路from bs4 import BeautifulSoup import requests import re def parse_html_page(page_url: str): resp requests.get(page_url, timeout(5, 15)) resp.raise_for_status() soup BeautifulSoup(resp.text, html.parser) # 方法一提取所有img标签里的src或data-src img_tags soup.find_all(img) for img in img_tags: src img.get(data-src) or img.get(src) if src and src.endswith((.jpg, .png, .webp)): print(src) # 方法二用正则直接扫所有图片地址 pattern re.compile(rhttps?://[^\\s]?\.(?:jpg|png|webp)) for match in pattern.findall(resp.text): print(match)这种方案的缺点是需要针对不同网站写专用的选择器网站改版后代码可能失效。但优点是可以覆盖所有网站不依赖API。有些壁纸站的缩略图和原图地址只是尺寸参数不同把缩略图URL里的thumb/换成full/就能拿到原图这种技巧值得记住。5.4 加日志跑完才知道发生了什么脚本跑完了没有任何输出或者是输出被终端快速滚走看不到这对排查问题很不友好。加一个简单的日志功能把每次下载的结果写入download.logimport logging logging.basicConfig( filenamedownload.log, levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, encodingutf-8, ) logging.info(f开始下载关键词 {keyword}共 {len(all_results)} 张)这样就算脚本在后台由定时任务触发事后也可以翻日志看下载情况。日志里记录了成功、失败、跳过的完整信息比控制台输出更有价值。我实际把第一版脚本跑通之后回头看了下最核心的收获其实不是“下载壁纸”本身而是搭起了一条完整的下载管道请求 → 解析 → 并发下载 → 落盘 → 归档 → 日志。这套流程你后面做批量下载素材、同步头像包、备份网络图片全部可以原封不动地搬过去只需要换个数据源解析函数。如果你要拿去改成自己的版本建议先从修改main()里的keyword和pages这两个参数开始跑通一次再逐步调整并发数和筛选规则。