☰
Python爬虫练手项目:从零实现宠物图片批量下载
2026/10/12 13:12:27 网站建设 项目流程

1. 从零拆解一个爬虫练手项目的完整思路

1.1 为什么选这个题材来练手

很多刚学Python的朋友都会卡在一个尴尬的阶段:语法学完了,循环、字典、函数都能看懂,但真让自己写点东西,脑子里一片空白。我当年也是这样,看教程觉得啥都会,一动手就废。后来一个老哥跟我说,你去爬点图片吧,图片这东西最直观,爬下来能看见,有成就感,而且逻辑简单,不容易劝退。

选宠物图片这个题材,其实是有讲究的。第一,图片资源是静态的,不像视频那样有分片、加密、防盗链一大堆坑,对新手友好。第二,这类页面结构相对规整,宠物图鉴通常按编号或者分类排列,规律性强,适合练习解析。第三,爬下来的东西有收藏价值,你能实实在在看到成果,这种正反馈对坚持学下去特别重要。

我见过太多人一上来就挑战某电商平台或者某社交网站,结果被反爬机制按在地上摩擦,三天就放弃了。练手项目一定要选软柿子捏,先把整个流程跑通,建立起“我能行”的信心,再去啃硬骨头。这个宠物图片项目就是典型的软柿子,但它麻雀虽小五脏俱全,请求发送、响应解析、文件存储、异常处理这些核心环节一个不少,跑完一遍你对爬虫的基本骨架就有概念了。

1.2 这个项目到底能学到什么

别看只是爬图片,里面涉及的知识点其实挺密集的。首先是HTTP请求的基本原理,你得知道浏览器访问网页时到底发生了什么,请求头里那些字段是干嘛的,为什么有的网站不加User-Agent就给你返回403。其次是HTML结构解析,你得学会用开发者工具去看页面的DOM树,找到图片地址藏在哪个标签的哪个属性里。然后是数据清洗与拼接,图片链接经常是相对路径,你得把它和域名拼成完整URL。最后是文件IO操作,怎么把二进制数据写进本地文件,怎么批量命名避免覆盖。

再往深一点,你还会接触到请求频率控制、异常重试机制、断点续爬这些工程化的思路。这些东西在教程里往往一笔带过,但实际写代码的时候,恰恰是这些细节决定了你的脚本是能跑一次还是能稳定跑一百次。我个人的经验是,新手写爬虫最大的问题不是不会写,而是写得太“脆”,网络一抖动就崩,页面结构一变就废,没有任何容错能力。这个项目正好可以让你从一开始就养成写健壮代码的习惯。

1.3 整体技术选型的考量

技术栈方面,我建议新手就用requests + BeautifulSoup这套组合,别一上来就上Scrapy。Scrapy确实强大,但它的学习曲线陡峭,各种中间件、管道、调度器的概念会把新手绕晕。而requests负责发请求,BeautifulSoup负责解析HTML,职责清晰,代码量少,调试方便,出问题了你能一眼看出是哪一步挂了。

至于图片下载,直接用requests的流式下载就行,不需要额外的库。有人会推荐用urllib,但requests的API更人性化,异常处理也更清晰。存储方面,就用最朴素的本地文件夹,按分类或者编号建子目录,别整数据库那一套,杀鸡用牛刀反而增加复杂度。

提示:不要为了炫技而堆砌技术栈。练手项目的核心目标是理解流程,不是展示你会的库多。用最简单的工具把事办成,才是真本事。

2. 动手前的环境准备与目标分析

2.1 开发环境搭建清单

环境这块没什么好纠结的,Python 3.7以上版本都行,我习惯用3.9,稳定且兼容性好。装好Python之后,打开终端执行下面两条命令把依赖装上:

pip install requests pip install beautifulsoup4

如果你还想让输出好看一点,可以顺手装个lxml作为BeautifulSoup的解析器,速度比内置的html.parser快不少:

pip install lxml

编辑器方面,VS Code或者PyCharm社区版都行,看个人习惯。我推荐VS Code,轻量,装个Python插件就能跑,调试也方便。新建一个项目文件夹,里面建一个images子文件夹用来存图片,再建一个pet_spider.py作为主脚本,结构就搭好了。

2.2 目标页面的结构侦查方法

这一步是整个项目最关键的前期工作,很多人急着写代码,结果写到一半发现找不到图片地址,又回头翻页面,来回折腾。正确的做法是先花十分钟把页面结构摸清楚。

打开浏览器,进入目标图鉴页面,按F12打开开发者工具,切到Elements面板。然后点击左上角那个小箭头图标,再去点页面上的宠物图片,这时候DOM树会自动定位到对应的img标签。你会看到类似这样的结构:

<img src="//xxx.com/images/pet/001.png" alt="宠物名称" class="pet-img">

注意这里的src属性,它可能是相对路径,也可能是协议相对路径(以//开头),这两种情况处理方式不一样。相对路径要拼上域名,协议相对路径要拼上https:。这个细节不注意,下载的时候就会报错。

接着你要观察整个列表的规律。宠物图鉴通常是一个大列表,每个宠物对应一个li或者div,里面包含图片和名称。你要找到包裹所有宠物的那个父容器,然后定位到每个宠物的重复单元。用CSS选择器或者XPath把它描述出来,比如.pet-list .pet-item img这样的选择器。

2.3 图片URL规律的挖掘技巧

有些网站的图片地址是有规律的,比如按编号递增:001.png、002.png、003.png。这种情况下你甚至不需要解析HTML,直接构造URL循环下载就行,效率极高。但更多时候,图片地址是散落在HTML里的,必须解析才能拿到。

怎么判断有没有规律?我的方法是先手动下载几张,对比URL。如果只有最后几位数字在变,那基本就是有规律的。如果中间夹杂着哈希值或者随机字符串,那就老老实实解析页面。

还有一种情况是图片用了懒加载,src属性里放的是占位图,真实地址藏在>import requests from bs4 import BeautifulSoup import os import time 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", "Referer": "https://example.com/" } def fetch_page(url): try: resp = requests.get(url, headers=HEADERS, timeout=10) resp.raise_for_status() resp.encoding = resp.apparent_encoding return resp.text except requests.RequestException as e: print(f"请求失败: {url}, 原因: {e}") return None

这里有几个点值得展开说。User-Agent是必须加的,不加的话很多网站直接返回403,因为默认的python-requests UA太明显了,等于告诉人家“我是爬虫”。Referer有些网站会校验,加上更保险。timeout一定要设,不然网络卡住的时候脚本会一直挂着,你以为它在跑,其实早就死了。

resp.apparent_encoding这行是自动识别编码,比手动指定resp.encoding = 'utf-8'更稳妥。有些网站用的是gbk或者gb2312,你硬按utf-8解会乱码,用apparent_encoding让它自己猜,省心。

raise_for_status()会在状态码不是200的时候抛异常,配合try-except就能优雅地处理404、500这些情况,而不是让脚本直接崩掉。

3.2 解析逻辑的构建与容错

拿到HTML之后,用BeautifulSoup解析,提取图片地址:

def parse_images(html, base_url): soup = BeautifulSoup(html, "lxml") img_tags = soup.select(".pet-list .pet-item img") results = [] for img in img_tags: src = img.get("data-src") or img.get("src") if not src: continue if src.startswith("//"): src = "https:" + src elif src.startswith("/"): src = base_url.rstrip("/") + src name = img.get("alt", "unknown").strip() results.append((name, src)) return results

这段代码的容错逻辑是重点。img.get("data-src") or img.get("src")这行先尝试取懒加载地址,取不到再取常规src,兼容两种情况。if not src: continue跳过没有地址的标签,避免后面拼接时报错。

URL拼接那部分,处理了协议相对路径和根相对路径两种情况。base_url.rstrip("/")是为了防止域名末尾有斜杠导致拼接出双斜杠。这些细节看着琐碎,但实际跑起来能省掉大量调试时间。

名称提取用alt属性,取不到就给个默认值unknown。后面存文件的时候会用名称做文件名,所以最好再做一次清洗,把特殊字符替换掉,不然Windows下会报错:

import re safe_name = re.sub(r'[\\/:*?"<>|]', "_", name)

3.3 图片下载与本地存储

下载和存储这块,核心是流式写入,避免大图占内存:

def download_image(name, url, save_dir): os.makedirs(save_dir, exist_ok=True) ext = os.path.splitext(url)[-1] or ".png" filepath = os.path.join(save_dir, f"{name}{ext}") if os.path.exists(filepath): print(f"已存在,跳过: {name}") return try: resp = requests.get(url, headers=HEADERS, timeout=15, stream=True) resp.raise_for_status() with open(filepath, "wb") as f: for chunk in resp.iter_content(chunk_size=8192): f.write(chunk) print(f"下载成功: {name}") except requests.RequestException as e: print(f"下载失败: {name}, 原因: {e}")

stream=True配合iter_content是标准的大文件下载姿势,每次只读8KB,内存占用极低。os.path.exists做去重判断,脚本中断后重跑不会重复下载,这就是最简单的断点续爬。

os.makedirs(save_dir, exist_ok=True)的exist_ok=True参数很关键,不加的话文件夹已存在时会抛异常。这种小细节新手经常忽略,然后被莫名其妙的报错卡住。

3.4 主流程串联与频率控制

把上面的模块串起来,加上翻页逻辑和延时:

def main(): base_url = "https://example.com" save_dir = "images" for page in range(1, 11): url = f"{base_url}/pets?page={page}" print(f"正在处理第 {page} 页") html = fetch_page(url) if not html: continue items = parse_images(html, base_url) for name, img_url in items: download_image(name, img_url, save_dir) time.sleep(0.5) time.sleep(1) if __name__ == "__main__": main()

time.sleep(0.5)是每张图之间的间隔,time.sleep(1)是每页之间的间隔。这个延时不是可有可无的,它是你作为爬虫写作者的“礼貌”。请求太密集会给对方服务器造成压力,轻则被封IP,重则被请去喝茶。0.5到1秒是个比较稳妥的区间,既不会太慢,也不会太激进。

4. 实战中踩过的坑与排查手册

4.1 常见报错与对应解法

下面这张表是我自己踩坑总结出来的,基本覆盖了新手会遇到的八成问题:

报错信息根本原因解决办法
403 Forbidden缺少User-Agent或Referer补全请求头,模拟浏览器
404 Not FoundURL拼接错误或图片已删除检查拼接逻辑,加异常跳过
ConnectionError网络不通或目标拒绝连接加重试机制,检查网络
Timeout响应太慢或服务器限速增大timeout,降低请求频率
UnicodeDecodeError编码识别错误用apparent_encoding自动识别
下载的图片打不开存的是占位图或HTML检查data-src,验证响应类型

其中“下载的图片打不开”这个坑最隐蔽。你看着文件下下来了,大小也有,但打开一看是灰色的占位图,或者干脆是一段HTML。原因通常是懒加载没处理,或者服务器返回了错误页面但状态码还是200。解决办法是在下载前检查resp.headers.get("Content-Type"),确认是image/开头再写入。

4.2 反爬机制的识别与应对

新手最怕的就是被封。怎么知道自己被封了?几个信号:状态码突然变成403或者429,返回内容变成验证页面,或者连接直接被重置。遇到这种情况,先别慌,停下来分析。

最常见的原因是请求频率太高。解决办法就是加延时,把time.sleep的值调大,比如从0.5秒调到2秒。如果加了延时还是被封,那可能是IP被拉黑了,这时候需要换网络环境,或者用代理池。但代理池对新手来说太重了,我建议先降低频率试试,大部分小网站不会做得太绝。

还有一种情况是网站用了Cookie校验,你第一次访问会给你发个Cookie,后续请求必须带上。这种用requests的Session对象就能自动处理:

session = requests.Session() session.headers.update(HEADERS) resp = session.get(url)

Session会自动保存和发送Cookie,省去手动处理的麻烦。

提示:不要试图用多线程或者异步去暴力提速。新手阶段,稳定比快重要得多。一个能跑完的慢脚本,胜过一个跑一半就崩的快脚本。

4.3 代码健壮性的几个实用技巧

第一个技巧是日志代替print。新手喜欢用print调试,但print多了之后输出乱糟糟的,出了问题根本找不到。用logging模块,把信息分级,出错的时候能快速定位:

import logging logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") logging.info("开始下载") logging.error("下载失败")

第二个技巧是失败重试。网络抖动是常态,一次失败不代表真的失败。用简单的循环重试就能大幅提升成功率:

def download_with_retry(name, url, save_dir, retries=3): for i in range(retries): if download_image(name, url, save_dir): return True time.sleep(2) return False

第三个技巧是把配置抽出来。URL、选择器、延时这些容易变的东西,全部放到文件开头的常量区,改的时候一目了然,不用在代码里翻来翻去。

5. 项目扩展与进阶方向

5.1 从单页到全站的爬取策略

跑通单页之后,你自然会想爬全站。这时候要考虑的是翻页逻辑。有的网站是URL参数翻页,比如?page=2,这种最简单,循环改参数就行。有的是点击按钮加载,URL不变,这种就要分析它背后的接口请求,在Network面板里找XHR请求,看它请求的是哪个接口,参数是什么。

还有一种是无缝滚动的瀑布流,翻页靠滚动触发。这种通常也有对应的接口,找到接口规律后,直接请求接口拿JSON数据,比解析HTML还简单。学会看Network面板,是爬虫进阶的分水岭。

5.2 数据整理与去重思路

图片爬多了之后,管理就成了问题。我的做法是按分类建子文件夹,文件名用“编号_名称”的格式,这样排序之后一目了然。去重方面,除了用文件名判断,还可以用图片的MD5值来判断,防止同一张图换了名字重复下载:

import hashlib def get_md5(content): return hashlib.md5(content).hexdigest()

维护一个已下载MD5的集合,下载前先算一下,在集合里就跳过。这个思路在处理大量图片的时候特别有用。

5.3 把脚本改造成可复用工具

最后一步,把这个一次性脚本改造成能复用的工具。用argparse接收命令行参数,把目标URL、保存目录、延时时间都做成可配置的:

import argparse parser = argparse.ArgumentParser() parser.add_argument("--url", required=True) parser.add_argument("--dir", default="images") parser.add_argument("--delay", type=float, default=0.5) args = parser.parse_args()

这样下次遇到类似的图鉴网站,改个URL和选择器就能直接用,不用重写一遍。写代码的最高境界不是写得快,而是写得能复用。一个项目做完,沉淀下来的应该是可复用的模块和思路,而不是一堆只能跑一次的代码。

我个人在实际操作中的体会是,爬虫这东西,技术门槛其实不高,真正拉开差距的是对细节的把控和对规则的敬畏。同样的需求,有人写出来的脚本跑一次就废,有人写出来的能稳定跑半年,区别就在异常处理、频率控制、日志记录这些不起眼的地方。新手阶段多花点时间打磨这些细节,比多学几个花哨的库有价值得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询