ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Python爬虫脚本实现壁纸批量自动下载与高效去重

用Python爬虫脚本实现壁纸批量自动下载与高效去重 写 Python 脚本自动下载壁纸这件事我最早是手动干的每天打开壁纸站点开“4K”分类挑两张顺眼的右键另存为再用看图工具统一改分辨率壁纸稍微多几台设备就得重复折腾一遍。后来实在烦了干脆写了个脚本把“找图、下载、重命名、去重”整个链路自动化跑一次就能攒下几十张图。这篇文章就聊聊这个脚本从需求拆解到落地踩坑的完整过程适合对自动化脚本感兴趣、又不想天天手动存图的人参考。1. 先说清楚这个脚本到底解决什么问题1.1 为什么手动下壁纸让人崩溃壁纸这个需求听起来小真正手动攒过图的人都知道有多磨人。搜图要逐页翻预览图和大图经常不是同一个地址下载时文件名又五花八门.jpg、.png、.webp混在一起。更难受的是同一张图换个平台名字就变了本地磁盘里存了一堆重复文件往往等到硬盘塞满才发现占空间的其实是同一张图的不同版本。手动下载的问题本质是“重复劳动没有沉淀价值”。每次打开网站、右键另存、新建文件夹、重命名这些动作固定在浪费生命。写脚本的核心目标不是去跟某个网站较劲而是把这一整套动作抽成可复用的流程给定一个搜索关键词或分类页脚本自己去解析网页、提取图片地址、按规则保存到本地。1.2 适合谁脚本要满足哪些需求在我的经验里这类脚本最适合三种人。第一种是像我一样有大量壁纸收集需求的人需要定时换一批图第二种是做内容素材整理的编辑或设计师需要按主题批量抓图第三种是刚学 Python 的初学者想通过一个“看得见效果”的项目理解 requests、BeautifulSoup、文件读写这些基本功。既然目标人群里有小白脚本就不能只给自己用。我的要求比较明确输入一个网址就能开始跑不需要改太多配置。下载的图片能自动分类保存比如按日期或者分类建子目录。重复下载同一批图片时不会覆盖已有文件。中间失败的网络请求能够自动重试不至于跑一半就中断。最后能看到统计信息成功几张、跳过几张、失败几张。这些需求决定了脚本不能只是“能跑”还得面对真实网络环境里的各种幺蛾子。2. 整体设计选源、抓取与下载的取舍2.1 壁纸源怎么选API优先其次静态页面写下载脚本第一个问题不是怎么写代码而是从哪里下。壁纸源选错后面所有代码都是浪费时间。我试过几类来源经验是按优先级排序来源类型优点缺点推荐程度官方 API结构清晰、字段稳定部分需要 token、有次数限制最推荐公开 JSON/embed 页面直接能拿数据不用解析 HTML很少见依赖特定站点推荐静态 HTML 页面无需认证门槛低页面结构易变需要维护选择器可用图片直链聚合页简单直接质量参差、防盗链严重不推荐我常用的是支持关键词检索的公共 API 站点直接请求一个接口拿到 JSON再从中提取图片 URL解析成本几乎为零。但考虑到有些朋友只是想练手或者想抓某个具体站点的分类页静态页面方案也不能绕开。我的建议是把“抓取源”抽象成一个可以替换的函数内部返回统一格式的图片地址列表。这样今天写死一个网站明天想换另一个源只需要改动一小块函数。2.2 抓取策略先列表页再详情页而不是直接扫全站很多论坛社区的抓图脚本容易写成“从第一页爬到最后一页见图片就下载”。这种思路在小型站点可行在稍大一点的壁纸站就很容易触发限制而且会下载大量低质量缩略图。正确的姿势是分两层。第一层先抓列表页列表页里通常只包含缩略图和详情页链接第二层进入详情页从详情页里提取原图的真实地址。这样看起来多了一步请求但换来的是更准确的图片质量。举个例子列表页里图片地址可能长这样https://example.com/thumbs/12345.jpg只有几百像素宽。详情页里的真实地址可能是https://example.com/wallpapers/12345_4k.jpg。直接抓缩略图图省事但下载到本地你会后悔。所以我在设计脚本时把“解析列表页”和“解析详情页”写成两个函数列表页函数返回一组 dict包含详情页链接和图片标题详情页函数再根据链接返回原图地址。这样的结构让调试变得很舒服。2.3 下载策略命名、去重、断点续传下载看起来最简单其实最容易踩坑。图片 URL 里的文件名往往是一长串数字直接存下来下次整理时根本不知道哪张是哪张。我的命名规则是日期_分类_序号_原文件名。比如20250601_city_001_8291233.jpg。这样按名称排序时天然按日期归拢查找也方便。去重方面我维护了一个简单的历史名单。下载前先检查目标文件名是否存在于本地存在就跳过存在但文件体积为 0则删除后重新下载。另外对已经下载的文件也可以做一次哈希校验把相同 MD5 的重复文件合并不过这一步初期可以不做先保证不重复覆盖。断点续传策略是重点。下载图片时不要一次性把整个文件读进内存而是用流式请求按块写入磁盘并在临时文件后缀加.part。下载完成后把.part改名成正式文件。如果中途失败下次可以直接删除.part重新下载至少不会把坏文件当成好文件。3. 核心实现细节用到的库、代码与关键参数3.1 环境准备Python版本与依赖整个项目基于 Python 3.8 写的在 3.10 和 3.11 上实测都没问题。主要用到三个库requests负责发 HTTP 请求beautifulsoup4负责解析 HTMLlxml作为 BeautifulSoup 的解析引擎。pip install requests beautifulsoup4 lxml如果你还没把 Python 加入系统环境变量直接敲这条命令很容易遇到pip : 无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称这类报错。这时候别急着重装 Python先打开系统环境变量把 Python 安装目录和它的Scripts子目录加进Path再新开一个终端窗口试试pip --version。这个报错我在 Windows 上碰到太多次了基本就是这个原因。3.2 三步抓取请求头、解析、匹配抓取的第一步是让服务器觉得“你是个正常人”。直接用 Python 默认的 User-Agent很多壁纸站会直接返回 403。我的请求头一般这样设置headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, }第二步是解析。BeautifulSoup 的用法很直白从soup.find到select_one都行。我在代码里会优先用 CSS 选择器因为结构变化时只需要改一行字符串。第三步是匹配真实图片地址。壁纸站经常搞懒加载列表页里img标签的src是占位图真实地址藏在>import os import re import time 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 Safari/537.36, } BASE_URL https://example-wallpaper-site.com/category/city SAVE_DIR wallpapers MAX_PAGES 3 TIMEOUT 10 def get_soup(url): resp requests.get(url, headersHEADERS, timeoutTIMEOUT) resp.raise_for_status() resp.encoding resp.apparent_encoding return BeautifulSoup(resp.text, lxml) def extract_image_urls(soup): urls [] for a in soup.select(a img): src_candidates [ a.get(src), a.get(data-src), a.get(data-original), a.get(data-url), ] for src in src_candidates: if src and re.search(r\.(jpg|jpeg|png|webp)(\?|$), src, re.I): urls.append(src) break return urls def download_image(url, referer, save_path): headers HEADERS.copy() headers[Referer] referer resp requests.get(url, headersheaders, timeoutTIMEOUT, streamTrue) resp.raise_for_status() tmp_path save_path .part with open(tmp_path, wb) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk) if os.path.getsize(tmp_path) 0: os.rename(tmp_path, save_path) return True return False def main(): os.makedirs(SAVE_DIR, exist_okTrue) downloaded 0 for page in range(1, MAX_PAGES 1): page_url f{BASE_URL}?page{page} print(f正在处理第 {page} 页: {page_url}) soup get_soup(page_url) img_urls extract_image_urls(soup) print(f发现 {len(img_urls)} 张图片) for idx, img_url in enumerate(img_urls, 1): if not img_url.startswith(http): img_url requests.compat.urljoin(page_url, img_url) filename os.path.basename(img_url.split(?)[0]) save_path os.path.join(SAVE_DIR, f{time.strftime(%Y%m%d)}_{page}_{idx}_{filename}) if os.path.exists(save_path): print(f跳过已存在: {filename}) continue try: ok download_image(img_url, page_url, save_path) if ok: downloaded 1 print(f下载成功: {filename}) except Exception as e: print(f下载失败: {filename}, 错误: {e}) time.sleep(0.5) print(f本次共下载 {downloaded} 张图片) if __name__ __main__: main()4.2 关键函数逐段说明extract_image_urls是整个脚本能否抓对图的核心。我把它单独抽出来因为它最常需要改动。不同站点的图片标签不同有的站在a上面套一层div有的在picture标签里嵌套多个source。抽成函数后调试时可以直接在命令行里测试一段选择器不需要跑完整流程。download_image里有两个设计要点。第一是streamTrue图片下载是 IO 密集任务没必要一次性把整个响应读进内存分块写盘对大图更友好。第二是.part临时文件机制这个习惯来自我早期下载大文件经常下到一半断掉结果留下一个“看似能打开但其实是残缺”的图片临时文件方案能避免这种尴尬。主流程里我加了time.sleep(0.5)来控制请求频率。很多新人会觉得这是没用的延迟实际上这是防止被服务器限流的重要手段。对目标站点保持礼貌比歇斯底里地并发狂下更持久。5. 实际运行中踩过的坑与排查实录5.1 pip 识别不了先把环境变量理顺文章第二部分提过一次 pip 报错这里再展开讲。很多朋友刚装完 Python 就兴冲冲打开终端敲pip install requests结果 PowerShell 直接丢出来“无法将 pip 项识别为 cmdlet、函数、脚本文件或可运行程序的名称”。大多数人第一反应是卸载重装没必要。打开“系统属性 - 环境变量”在Path里追加两条C:\Users\你的用户名\AppData\Local\Programs\Python\Python311\ C:\Users\你的用户名\AppData\Local\Programs\Python\Python311\Scripts\保存后重新打开终端。如果还是不行再检查是否存在python.exe和pip.exe这两个文件。有时候只装了 Python但没有装 pip可以用python -m ensurepip --upgrade补上。另外一个隐藏很深的坑如果你的电脑装了多个 Python 版本pip可能指向旧版本而你的项目需要新版本才有的特性。这时候不要直接敲pip改用python -m pip这样能保证 pip 跟当前运行的 Python 是同一个环境。5.2 SSL证书报错、请求超时怎么处理运行脚本我遇到过最常见的报错是requests.exceptions.SSLError: HTTPSConnectionPool(host..., port443): Max retries exceeded with url: ... (Caused by SSLError(... certificate verify failed ...))。这个报错不一定是证书真的有问题更多是目标站点的证书链不完整或者本地没有安装对应的 CA 证书。牺牲一下安全级别在请求时加verifyFalse并屏蔽掉警告import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) requests.get(url, headersHEADERS, verifyFalse)同时还要给请求设置超时。我在代码里写的TIMEOUT 10不是拍脑袋而是基于实测超过 10 秒没能建立连接大概率是网络问题或站点限制再等下去只会让脚本卡死。如果连续失败三次脚本应该停止当前下载不要无限重试。5.3 图片重名、0字节文件的应对重名问题最容易出现在用原文件名保存的时候。同一个数字编号的图片在不同分类下会出现或者只改了图片格式后缀。我习惯在文件名里拼上“页码_序号”这样基本可以避免冲突。0 字节文件是另一个隐蔽问题。服务器可能返回了 200 状态码但实际内容是空的或者反爬机制给你返回一个空文件。如果写完文件后直接改名本地就多了一个废文件。所以在下载函数里我加了文件大小检查小于 1KB 的如果不是 GIF 这类特殊格式直接视为异常把临时文件删掉重来。6. 让脚本更像“产品”进阶优化玩法6.1 并发下载提速单个下载加 0.5 秒延迟下 50 张图就得等 25 秒。如果图源稳定可以改成线程池来提速。Python 自带concurrent.futures.ThreadPoolExecutor代码改动很小。from concurrent.futures import ThreadPoolExecutor def safe_download(item): url, page_url, save_path item try: return download_image(url, page_url, save_path) except Exception as e: print(f下载失败: {e}) return False with ThreadPoolExecutor(max_workers8) as executor: tasks [(img_url, page_url, save_path) for img_url, save_path in ...] results list(executor.map(safe_download, tasks))并发下载时更要注意不要一次开几十个线程否则轻则被服务器封 IP重则把你自己的带宽打满。我习惯控制在 5 到 8 个并发稳定性和速度比较平衡。6.2 定时任务实现壁纸自动更新脚本写好后我给它配了定时任务每周自动跑一次。Windows 上可以用“任务计划程序”新建一个基本任务触发条件选“每周”操作选“启动程序”程序填python.exe的完整路径参数填脚本完整路径。Linux 或 macOS 更简单直接用 crontab# 每周一早上 8 点执行下载 0 8 * * 1 /usr/bin/python3 /home/user/wallpaper_downloader.py定时任务跑起来之后脚本的日志输出很重要。我在代码里只用了print但实际挂机运行时建议把输出重定向到日志文件不然出错时根本不知道哪一步挂了。最省事的方式是给print加上时间戳然后让系统重定向输出。6.3 从固定源换成自定义配置脚本里最不应该写死的部分是BASE_URL和保存规则。我后来把配置抽到config.py里# config.py SAVE_DIR wallpapers MAX_PAGES 5 DOWNLOAD_CONCURRENCY 8 REQUEST_INTERVAL 0.5 SOURCE_URL https://example-wallpaper-site.com/category/city这样换源的时候只改配置不动主逻辑。更进一步的思路是把解析规则也做成配置项比如“图片标签的 CSS 选择器”“原图地址所在的属性名”。这样做的好处是同一个脚本可以适配多个站点不用为每个站点复制一份代码。我自己在实际操作里的体会是这类脚本不要一开始就追求大而全先跑通一个最小闭环再逐步加并发、加定时、加配置化比闷头写一整天更靠谱。每加一个功能都要跑一次真实下载验证否则很容易出现“改完代码旧图全废”的尴尬。最后再分享一个小技巧下载完成后抽几个样本文件用PIL打开看一下尺寸和格式能最快发现解析规则是否出错省得几百张图全下回来才发现全都是缩略图。
返回列表