尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

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/xhtmlxml,application/xml;q0.9, image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.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;q0.9, } def fetch(url, refererNone): headers dict(HEADERS) if referer: headers[Referer] referer for attempt in range(3): try: resp requests.get(url, headersheaders, timeout10) 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}第{attempt1}次尝试) 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_okTrue) 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, refererreferer) 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, flist_{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, refererlink, indexindex) 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读本地文件反复试选择器确认没问题了再接入网络请求。这样调试速度快也不会因为频繁请求给对方服务器添麻烦。等规则稳定了再让脚本联网跑效率高得多。
返回列表