news 2026/9/23 10:31:36

Python爬虫实战:requests+BeautifulSoup批量下载高清壁纸

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python爬虫实战:requests+BeautifulSoup批量下载高清壁纸

1. 项目缘起与整体设计思路

腾牛网这个站点,做过壁纸类爬虫的朋友应该不陌生。它的图片资源分类清晰、分辨率高、更新频率稳定,对于想练手图片采集的Python初学者来说,是一个比较理想的实战目标。我第一次接触这个需求,是帮一个做设计的朋友批量收集某一类风格的壁纸素材,手动右键保存了二十几张之后就彻底放弃了——重复劳动不说,还容易漏掉分页里的内容。于是就有了这个用requests + BeautifulSoup组合来批量获取高清壁纸的项目。

这个项目的核心目标很明确:给定一个壁纸分类入口,自动翻页、自动解析详情页、自动提取原图地址并下载到本地,同时做好去重和异常处理。它解决的问题本质上是“把人工重复点击变成脚本自动化执行”,适合有一定Python基础、想通过一个完整案例把HTTP请求、HTML解析、文件IO串起来的读者。哪怕你之前只写过几行print,跟着思路走也能理解整个链路。

在方案选型上,我最终选择了requests负责网络请求、BeautifulSoup负责HTML解析,而不是上来就上Scrapy或者Playwright。原因有几个:第一,腾牛网的页面结构相对规整,服务端渲染的HTML里直接就能拿到图片链接,不需要处理复杂的JavaScript动态加载,用重量级框架属于杀鸡用牛刀;第二,对于学习目的来说,requests和BeautifulSoup的代码透明度更高,每一步在做什么一目了然,出了问题也好定位;第三,依赖少、环境搭建快,不用折腾浏览器驱动和中间件。

整体设计思路可以拆成四层:入口层负责构造分类页的URL和翻页逻辑;解析层负责从列表页提取详情页链接、从详情页提取原图地址;下载层负责发起图片请求并写入本地文件;管理层负责去重、限速、日志和异常兜底。这四层分开写,好处是任何一层出问题都不会牵连其他层,比如解析规则变了,只需要改解析层,下载逻辑完全不用动。

提示:做任何图片类爬虫之前,先花十分钟把目标站点的robots.txt和页面结构看一遍,确认哪些路径可以访问、页面是静态还是动态渲染,这一步能省掉后面大量的返工。

这里还要强调一个容易被忽略的点:请求头(Headers)的构造。很多新手直接裸奔一个requests.get(url)就发出去了,结果要么被返回403,要么拿到一个空壳页面。腾牛网对User-Agent是有校验的,带上一个正常的浏览器UA是最基本的礼貌,也是保证请求成功的前提。我在项目里把UA、Referer、Accept这些字段统一封装成一个字典,所有请求复用,既规范又省事。

2. 核心细节解析与实操要点

2.1 请求头的构造与反爬基础应对

请求头这块,我踩过的坑最多。最开始我只加了User-Agent,能拿到列表页,但下载图片的时候偶尔会返回403。后来对比浏览器开发者工具里的请求发现,图片请求还带了一个Referer,指向图片所在的详情页。补上Referer之后,下载成功率明显提升。这说明站点对图片资源做了来源校验,虽然不算严格的反爬,但不注意就会莫名其妙失败。

我常用的请求头模板是这样的:

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", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9," "image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Connection": "keep-alive", }

下载图片时,再在副本里补上Referer。注意不要直接修改全局字典,用{**HEADERS, "Referer": detail_url}这种展开方式生成新字典,避免污染全局配置。

除了请求头,请求频率是另一个关键。我实测下来,如果连续快速请求,大概几十次之后就会遇到429状态码,也就是请求过于频繁。热词里出现的“exceeded retry limit, last status: 429 too many requests”说的就是这种情况。应对办法很简单:在每次请求之间加一个随机延时,比如time.sleep(random.uniform(1, 3))。别小看这一两秒,它能让你的脚本从“容易被封”变成“稳定跑完”。

2.2 列表页与详情页的解析策略

腾牛网的壁纸列表页,每个条目通常包含一个缩略图和一个指向详情页的链接。解析列表页的目标就是把这些详情页链接收集起来。用BeautifulSoup定位的时候,我建议优先用结构化的选择器,比如根据class名或者标签层级来定位,而不是依赖那些看起来像随机字符串的id。因为随机id很可能随版本更新而变化,class名相对稳定。

一个典型的列表页解析逻辑是这样的:先找到所有条目容器,再遍历容器提取<a>标签的href属性。这里要注意href可能是相对路径,需要用urljoin拼成绝对路径,否则后续请求会失败。这个细节新手特别容易忽略,导致请求一个不存在的地址然后一头雾水。

详情页的解析是重头戏。原图地址往往藏在页面里某个<img>标签的src或者data-original属性中。有些站点为了懒加载,会把真实地址放在data-original里,src放一张占位图。腾牛网的部分页面也有类似处理,所以解析时要两个属性都检查一遍,优先取data-original,没有再取src。提取到地址后,同样要判断是不是相对路径,做一次拼接。

注意:解析规则不是一成不变的。站点改版后class名可能变化,所以我把所有选择器集中写在配置区,改版时只改这一处,不用满代码找。

2.3 图片下载与文件命名规范

下载环节看似简单,其实细节不少。首先是文件命名,如果直接用URL里的文件名,可能会遇到重名或者非法字符。我的做法是用“分类名_序号_原文件名”的组合,既保证唯一性,又方便后续检索。序号用递增计数器生成,原文件名从URL里截取,遇到非法字符就替换成下划线。

其次是写入方式。图片是二进制数据,必须用wb模式打开文件,写入response.content而不是response.text。这一点如果搞错,下载下来的文件打不开,是新手最常见的错误之一。另外,建议先下载到临时文件,确认内容完整后再重命名,避免中途失败留下半截损坏的文件。

还有一个实用技巧:在下载前先检查文件是否已存在,存在就跳过。这样脚本中断后重新跑,不会重复下载已经拿到的图片,节省时间和流量。判断依据可以是文件名,也可以是文件大小,我一般用文件名加大小双重判断,更稳妥。

3. 实操过程与核心环节实现

3.1 环境准备与依赖安装

动手之前先把环境理顺。Python版本建议3.8以上,太老的版本在第三方库兼容性上容易出问题。依赖就两个核心库:requests和beautifulsoup4,外加一个lxml解析器(比默认的html.parser快,容错性也好)。安装命令很直接:

pip install requests beautifulsoup4 lxml

如果你用的是虚拟环境,先激活再装。我习惯每个爬虫项目单独建一个venv,避免不同项目的依赖版本互相打架。装完之后可以跑一句import requests, bs4验证一下,没报错就说明环境OK。

目录结构我一般这样组织:项目根目录下建一个downloads文件夹存图片,一个logs文件夹存日志,主脚本放在根目录。这样跑完之后文件清清爽爽,不会满屏乱糟糟。

3.2 完整代码实现与逐段说明

下面把核心代码拆开讲。先看请求封装部分:

import os import time import random import requests from bs4 import BeautifulSoup from urllib.parse import urljoin 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", "Accept-Language": "zh-CN,zh;q=0.9", } def fetch(url, referer=None): headers = dict(HEADERS) if referer: headers["Referer"] = referer for attempt in range(3): try: resp = requests.get(url, headers=headers, timeout=10) if resp.status_code == 200: return resp elif resp.status_code == 429: wait = random.uniform(5, 10) print(f"触发限流,等待{wait:.1f}秒后重试") time.sleep(wait) else: print(f"状态码{resp.status_code},第{attempt+1}次尝试") except requests.RequestException as e: print(f"请求异常:{e}") time.sleep(random.uniform(1, 2)) return None

这段代码做了三件事:统一请求头、带Referer的图片请求、失败重试。重试次数设为3次,遇到429就多等一会儿,其他错误短暂等待后重试。超过次数返回None,由上层决定怎么处理。

列表页解析:

def parse_list(html, base_url): soup = BeautifulSoup(html, "lxml") links = [] for a in soup.select("a.pic-link"): # 选择器按实际结构调整 href = a.get("href") if href: links.append(urljoin(base_url, href)) return links

详情页解析原图:

def parse_detail(html, base_url): soup = BeautifulSoup(html, "lxml") img = soup.select_one("div.pic img") if not img: return None src = img.get("data-original") or img.get("src") return urljoin(base_url, src) if src else None

下载函数:

def download(img_url, save_dir, referer, index): os.makedirs(save_dir, exist_ok=True) name = img_url.split("/")[-1].split("?")[0] name = "".join(c if c.isalnum() or c in "._-" else "_" for c in name) filename = f"{index:04d}_{name}" path = os.path.join(save_dir, filename) if os.path.exists(path) and os.path.getsize(path) > 0: print(f"已存在,跳过:{filename}") return resp = fetch(img_url, referer=referer) if resp and resp.content: with open(path, "wb") as f: f.write(resp.content) print(f"下载完成:{filename}")

主流程把上面串起来,加上翻页和延时:

def main(): base = "https://www.xxx.com/wallpaper/list_1.html" # 按实际入口替换 save_dir = "downloads" index = 1 for page in range(1, 11): list_url = base.replace("list_1", f"list_{page}") resp = fetch(list_url) if not resp: continue detail_links = parse_list(resp.text, list_url) for link in detail_links: detail = fetch(link) if not detail: continue img_url = parse_detail(detail.text, link) if img_url: download(img_url, save_dir, referer=link, index=index) index += 1 time.sleep(random.uniform(1, 2)) time.sleep(random.uniform(2, 4))

跑起来之后,控制台会一行行打印下载进度,downloads文件夹里的图片逐渐增多。整个过程不需要人工干预,翻页、解析、下载、去重全自动完成。

3.3 参数选择与限速计算

限速参数不是拍脑袋定的。我做过一个小测试:不加延时连续请求,平均每30到50次请求就会触发一次429;加上1到2秒的随机延时后,连续跑几百次请求都没有再触发。所以我把单次请求间隔定在1到2秒,翻页间隔定在2到4秒。这个节奏下,一个包含10页、每页20个条目的分类,大概需要十几分钟跑完,速度可以接受,稳定性也有保障。

重试等待时间设为5到10秒,是因为429通常是短时限流,等几秒就能恢复。如果等太久,整体效率下降;等太短,可能还在限流窗口内,重试也是白搭。这个区间是我多次实测后觉得比较平衡的值。

4. 常见问题与排查技巧实录

4.1 高频问题速查表

问题现象可能原因排查与解决
返回403缺少User-Agent或Referer补全请求头,图片请求带上详情页Referer
返回429请求过于频繁增加随机延时,降低并发,重试时多等几秒
解析不到链接选择器失效或页面结构变化用开发者工具重新确认class名,更新选择器
下载的图片打不开用了text模式写入改用wb模式,写入response.content
文件名乱码或报错URL含非法字符过滤文件名,只保留字母数字和常见符号
脚本中断后重复下载没有去重判断下载前检查文件是否存在且大小大于0
请求超时网络波动或站点响应慢设置timeout,配合重试机制

4.2 独家避坑经验

第一个坑是选择器写得太死。我一开始用了一长串层级选择器,结果站点稍微调整一下结构就全废了。后来改成用相对稳定的class名定位,容错性好了很多。经验就是:能用class就用class,少用那种依赖多层嵌套的路径。

第二个坑是忽略编码问题。requests拿到响应后,如果页面编码识别错误,中文会变成乱码,解析自然失败。稳妥的做法是手动指定resp.encoding = resp.apparent_encoding,让requests根据内容自动判断编码,比默认的猜测准确得多。

第三个坑是没有日志。脚本跑了几百张图,中途失败了几张,如果没有日志,你根本不知道哪张失败了、为什么失败。我后来加了简单的日志输出,把每次请求的URL、状态码、结果都记下来,排查问题时一目了然。

提示:如果你的目标是长期、大规模采集,建议把已下载的URL记录到一个文本文件或数据库里,下次运行先读取记录做比对,避免重复劳动。小规模练手用文件名去重就够了。

第四个坑是对429的处理太粗暴。早期我遇到429就直接退出,结果整个任务半途而废。后来改成遇到429就等待并重试,配合整体限速,任务完成率大幅提升。这个思路其实适用于大多数图片类采集场景:宁可慢一点,也要稳一点。

4.3 关于合规与边界的个人看法

做爬虫这些年,我越来越觉得“能爬”和“该爬”是两回事。技术上跑通一个脚本不难,难的是知道什么时候该停下来。我的原则是:只采集公开可见的资源,控制请求频率不给对方服务器造成压力,采集到的内容仅用于个人学习或已获授权的用途,不二次分发、不商用。这个边界感,比任何技术技巧都重要。热词里出现的“因爬虫入狱”虽然是个极端说法,但它提醒我们,技术行为要有底线意识。

5. 可扩展方向与个人实操体会

这个项目跑通之后,其实还有很多可以往下做的空间。比如把下载记录存进SQLite,做成断点续传;比如加一个简单的可视化界面,用tkinter或者PySimpleGUI做个输入框,让不懂代码的人也能用;再比如把解析规则抽成配置文件,换一个站点只需要改配置不用改代码。这些都是很自然的演进方向,我在其他项目里也陆续实践过。

我个人在实际操作中的体会是:爬虫项目的难点从来不在写代码,而在应对变化。页面会改版、反爬会升级、网络会抖动,你的脚本必须具备一定的容错和自适应能力。所以与其追求一次写出完美代码,不如先把主流程跑通,再一点点加固。先让它能跑,再让它跑得稳,最后才考虑跑得快。这个顺序反了,很容易在细节里迷失方向。

最后分享一个小技巧:调试解析规则的时候,别每次都发真实请求。先把页面HTML保存到本地,用BeautifulSoup读本地文件反复试选择器,确认没问题了再接入网络请求。这样调试速度快,也不会因为频繁请求给对方服务器添麻烦。等规则稳定了,再让脚本联网跑,效率高得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 10:29:58

nRF51822芯片烧录产线配置指南:从烧录检测到转包装

前阵子一个做蓝牙BLE标签的客户找到我&#xff0c;说他们的nRF51822芯片月用量一下子从几千片涨到十几万片&#xff0c;靠几个人围着几台下载器手动烧录&#xff0c;烧完还得人工换管、点数量、贴标签&#xff0c;整个流程乱成一锅粥。这其实是很多做量产硬件的人都会撞上的墙&…

作者头像 李华
网站建设 2026/9/23 10:29:51

STM32第一个工程实战:CubeMX+VS Code+GCC点灯全链路

1. 为什么第一个STM32工程值得认真对待很多人学STM32&#xff0c;第一步就卡在环境搭建上。装Keil、装芯片包、找注册机、配调试器&#xff0c;一套流程走下来&#xff0c;代码还没写一行&#xff0c;人已经累了。更麻烦的是&#xff0c;网上教程版本参差不齐&#xff0c;有的还…

作者头像 李华
网站建设 2026/9/23 10:25:04

ToClaw 实战手册:11 个技巧让 AI Agent 配置 TaoToken 更顺手

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/23 10:24:50

sensor OV9728的参数

OV9728 是 OmniVision&#xff08;豪威科技&#xff09;推出的一款 720p 高清 CMOS 图像传感器。需要特别留意的是&#xff0c;该产品目前状态为“已停产”&#xff08;End-of-Life&#xff09;。&#x1f4f7; 核心参数光学格式&#xff1a;1/6.5 英寸像素尺寸&#xff1a;1.7…

作者头像 李华
网站建设 2026/9/23 10:23:29

电竞选手国际锦标赛实战经验与策略优化

1. 比赛背景与整体回顾2026年2月2日这场赛事对我来说意义非凡——这是我转型专业选手后参加的首次国际级锦标赛。作为一项综合了策略规划、实时应变与心理博弈的竞技项目&#xff0c;这场比赛云集了32个国家的128名顶尖选手&#xff0c;赛程持续14小时&#xff0c;包含5个阶段的…

作者头像 李华