1. 图片站抓取任务的整体拆解与思路设计
1.1 为什么选这个场景练手
图片站抓取几乎是每个学 Python 爬虫的人都会碰到的第一个"有点意思"的实战项目。它比抓新闻列表直观——抓下来的东西肉眼可见,成不成立刻就知道;又比抓动态接口简单——大部分图片站的列表页是服务端渲染的静态 HTML,不需要逆向接口签名。标题里提到的这个站点,结构上属于典型的"列表页 + 详情页 + 图片资源"三层结构,第一页的图片区通常就是一组缩略图加跳转链接,非常适合拿来把 requests、XPath、文件落盘这条链路完整走一遍。
我先把结论摆在这:抓第一页所有图片,核心就三件事——拿到列表页 HTML、从 HTML 里定位到图片区、把图片区的图片地址提取出来下载。听起来简单,但每一步都有坑,尤其是"图片区"这个限定词,意味着你不能无脑抓页面上所有 img 标签,得先做区域定位,再在区域内做提取。这个"先框范围再取内容"的思路,是后面所有复杂爬虫的基础,值得在这里就养成习惯。
适合谁看:装好了 Python、写过几行 print、想找个真实站点练手的入门者;也适合已经会写爬虫但总在"图片下载不全""文件名乱码""被拦"这些地方翻车的人。下面我会把每一步的选择理由、参数计算、踩坑经验都摊开讲,代码可以直接抄,但更希望你理解为什么这么写。
1.2 整体流程的四个阶段
把任务拆开,实际执行时是四个阶段串起来的:
- 请求阶段:用 requests 向列表页发 GET 请求,拿到 HTML 文本。这一步要处理编码、请求头、超时和重试。
- 解析阶段:用 lxml 的 XPath 定位"图片区"容器,再在容器内提取每个图片项的链接和缩略图地址。
- 补全阶段:列表页给的往往是缩略图或相对路径,需要拼成完整 URL,必要时进详情页拿原图。
- 落盘阶段:按规则生成文件名,流式下载,处理重名、格式、目录结构。
这四个阶段里,解析阶段是分水岭。很多人卡在"我明明看到页面上有图,代码就是抓不到",八成是定位写错了,或者图片是 JS 动态加载的。标题里说的是"图片区第一页",我默认它是静态渲染的列表,如果实测发现是动态的,那就要换思路(后面第 4 节会讲怎么判断和应对)。
1.3 工具选型:为什么是 requests + lxml
热词里出现了 requests、xpath、cv2、numpy 这些词,说明大家关心的工具面很广。针对这个任务,我的选型是requests 负责请求,lxml 负责解析,理由如下:
| 工具 | 用途 | 选它的理由 | 什么时候不用 |
|---|---|---|---|
| requests | 发 HTTP 请求 | API 直观,会话保持、超时、重试都好控制 | 需要执行 JS 时不够用 |
| lxml + XPath | 解析 HTML | 定位精准,支持按文本、按属性、按层级筛选 | 结构极乱时不如 BeautifulSoup 容错 |
| BeautifulSoup | 解析 HTML | 容错强,写法简单 | 大文档速度慢于 lxml |
| cv2 / numpy | 图片后处理 | 去重、裁剪、格式转换 | 单纯下载用不上 |
XPath 相比 CSS 选择器,优势在于能按"文本内容"定位,比如某个区块的标题文字是"图片区",我可以用contains(text(), '图片区')直接锚定,这在结构不规范的站点上特别好用。热词里那个"python xpath 爬虫 text 函数"说的就是这个点——text()取节点文本,配合contains()做模糊匹配,是定位中文区块标题的利器。
注意:选 lxml 要装
lxml库,Windows 上如果 pip 装不上,去下对应的 whl 文件本地安装,别硬编译。
2. 请求阶段的核心细节与实操要点
2.1 请求头到底要带哪些
裸奔的 requests 请求,User-Agent 是python-requests/x.x.x,稍微有点防护的站点一眼就认出来。所以第一件事是伪装请求头。但也不是越多越好,带多了反而暴露。我的经验是这四个够用:
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/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9", "Referer": "https://目标站点/", }Referer这个字段很多人忽略,但图片站经常校验它——你直接请求图片 URL 而不带 Referer,服务器可能返回 403。所以下载图片时,Referer 要设成图片所在页面的地址,而不是首页。这一点后面下载环节还会强调。
Accept-Encoding我一般不手动加 gzip,让 requests 自己处理解压,省得手动 decode 出乱码。如果你非要加,记得 requests 会自动解压,不用自己写解压逻辑。
2.2 编码问题:中文乱码的根源
抓中文站点,十有八九会遇到乱码。根源在于:HTTP 响应头里的Content-Type声明的编码,和 HTML 里<meta charset>声明的编码,可能不一致,甚至都不声明。
requests 的判断逻辑是:先看响应头,没有就看apparent_encoding(用 chardet 猜)。猜的经常错。稳妥做法是手动指定:
resp = requests.get(url, headers=headers, timeout=10) resp.encoding = resp.apparent_encoding # 或者直接写 'utf-8' / 'gbk' html = resp.text如果抓下来发现中文变成测试这种,说明编码判断错了,把resp.encoding改成'utf-8'试试;如果变成²âÊÔ这种,多半是'gbk'。判断技巧:看乱码的形态,拉丁字母带重音符号的是 UTF-8 被当 Latin-1 读了,方块问号的是 GBK 被当 UTF-8 读了。
实操心得:我习惯先
resp.content拿字节,再用chardet.detect()看一眼置信度,置信度低于 0.7 就手动指定。多花两行代码,省半小时排查。
2.3 超时、重试与会话保持
timeout必须设,不设的话网络一卡程序就挂死。我一般设(5, 15)这个元组,5 秒连接超时,15 秒读取超时。连接超时短一点,快速失败;读取超时长一点,给大页面留时间。
重试用requests.adapters.HTTPAdapter配合Retry:
from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session = requests.Session() retry = Retry(total=3, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504]) session.mount("http://", HTTPAdapter(max_retries=retry)) session.mount("https://", HTTPAdapter(max_retries=retry))backoff_factor=0.5意味着重试间隔是 0.5、1、2 秒递增,避免密集重试把对方惹毛。status_forcelist只对 5xx 重试,4xx 不重试——因为 404 你重试一百次还是 404,纯浪费。
用Session而不是每次requests.get,好处是 TCP 连接复用,抓多张图时速度快很多,而且 cookie 自动保持。图片站有时靠 cookie 做简单的访问计数,Session 能帮你维持住。
2.4 请求频率:别把人家站打挂
第一页图片数量有限,一般几十张,但下载图片时是逐个请求。我的做法是每张图之间 sleep 0.2 到 0.5 秒,加一点随机抖动:
import time, random time.sleep(random.uniform(0.2, 0.5))别小看这个抖动,固定间隔的请求模式很容易被识别成脚本。随机化之后,流量特征更接近真人浏览。这不是为了对抗什么,纯粹是基本的礼貌和自我保护——你也不想跑一半 IP 被限速。
3. 解析阶段:精准定位"图片区"
3.1 先看结构再写代码
写 XPath 之前,一定要先在浏览器里把目标页面的结构看清楚。按 F12,用元素选择器点一下图片区,看它的容器是什么标签、什么 class 或 id。假设结构大致是这样(不同站点不同,这里用通用结构示意):
<div class="main-content"> <div class="picture-area"> <h2>图片区</h2> <ul class="pic-list"> <li><a href="/detail/123.html"><img src="/thumb/123.jpg" alt="图1"></a></li> <li><a href="/detail/124.html"><img src="/thumb/124.jpg" alt="图2"></a></li> </ul> </div> <div class="sidebar">...</div> </div>关键点:图片区有独立的容器(这里是class="picture-area"),而且它和侧边栏、页脚是平级的。如果你直接//img,会把侧边栏的广告图、logo 全抓进来。所以第一步是框定容器。
3.2 XPath 定位容器的三种写法
定位"图片区"容器,我常用三种写法,按可靠性排序:
写法一:按 class 精确定位(最稳)
container = tree.xpath('//div[@class="picture-area"]')前提是你确认了 class 名。缺点是 class 可能带多个值,比如class="picture-area clearfix",这时@class="picture-area"就匹配不上了。
写法二:按 class 包含匹配(推荐)
container = tree.xpath('//div[contains(@class, "picture-area")]')contains()只要包含子串就匹配,容错性好很多。这是我用得最多的写法。
写法三:按区块标题文本锚定(最灵活)
container = tree.xpath('//h2[contains(text(), "图片区")]/parent::div')先找到文字是"图片区"的标题,再往上找它的父容器。这种写法在 class 名混淆、随机生成的站点上特别管用。热词里"python xpath 爬虫 text 函数"讲的就是这个技巧。
注意:
text()只取直接子文本节点。如果标题是<h2><span>图片区</span></h2>,text()取不到,得用//h2[contains(., "图片区")],点号.表示当前节点所有后代文本。这个坑我踩过不止一次。
3.3 在容器内提取图片项
容器拿到后,在它内部提取每个图片项。注意 XPath 的上下文——用container[0].xpath('.//li')而不是tree.xpath('//li'),前面的点号表示"从当前节点往下找",不加点号会从整个文档根找,容器就白框了。
items = container[0].xpath('.//li') for item in items: # 详情页链接 detail_url = item.xpath('.//a/@href') # 缩略图地址 thumb_url = item.xpath('.//img/@src') # 图片描述(alt 属性) alt = item.xpath('.//img/@alt')这里有个细节:xpath()返回的永远是列表,哪怕只有一个匹配。所以取值要[0],而且要判空,否则 IndexError 直接崩。我习惯写个辅助函数:
def first(lst, default=""): return lst[0].strip() if lst else default这样first(item.xpath('.//a/@href'))就安全了,取不到返回空字符串,不会崩。
3.4 相对路径补全:别下到一半发现 404
列表页里的src="/thumb/123.jpg"是相对路径,直接拿去请求会失败。必须拼成绝对 URL。用urljoin最稳:
from urllib.parse import urljoin full_url = urljoin(base_url, thumb_url)urljoin会自动处理各种情况:/thumb/123.jpg拼成https://站点/thumb/123.jpg,thumb/123.jpg拼成https://站点/当前目录/thumb/123.jpg,//cdn.xxx.com/123.jpg拼成https://cdn.xxx.com/123.jpg。别自己写字符串拼接,base + path这种写法遇到斜杠重复或缺失就出 bug。
还有一种情况:src里放的是占位图(比如data:image/gif;base64,...或者loading.gif),真实地址藏在>real_url = first(item.xpath('.//img/@data-src')) or first(item.xpath('.//img/@src'))
4. 实操过程:从零到图片落盘
4.1 完整代码骨架
把前面几节串起来,一个能跑的版本长这样:
import os import time import random import requests from lxml import etree from urllib.parse import urljoin from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry BASE_URL = "https://目标站点/list/1.html" SAVE_DIR = "downloads" def build_session(): session = requests.Session() retry = Retry(total=3, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504]) session.mount("http://", HTTPAdapter(max_retries=retry)) session.mount("https://", HTTPAdapter(max_retries=retry)) 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-Language": "zh-CN,zh;q=0.9", "Referer": BASE_URL, }) return session def get_html(session, url): resp = session.get(url, timeout=(5, 15)) resp.raise_for_status() resp.encoding = resp.apparent_encoding return resp.text def parse_images(html, base_url): tree = etree.HTML(html) containers = tree.xpath('//div[contains(@class, "picture-area")]') if not containers: return [] results = [] for item in containers[0].xpath('.//li'): href = item.xpath('.//a/@href') src = item.xpath('.//img/@data-src') or item.xpath('.//img/@src') alt = item.xpath('.//img/@alt') if not src: continue results.append({ "detail": urljoin(base_url, href[0]) if href else "", "img": urljoin(base_url, src[0]), "name": alt[0].strip() if alt else "", }) return results def download(session, url, path): resp = session.get(url, timeout=(5, 30), stream=True) resp.raise_for_status() with open(path, "wb") as f: for chunk in resp.iter_content(chunk_size=8192): if chunk: f.write(chunk) def safe_name(name, idx, url): if not name: name = f"img_{idx:03d}" name = "".join(c for c in name if c not in r'\/:*?"<>|').strip() ext = os.path.splitext(url.split("?")[0])[1] or ".jpg" return f"{idx:03d}_{name[:50]}{ext}" def main(): os.makedirs(SAVE_DIR, exist_ok=True) session = build_session() html = get_html(session, BASE_URL) images = parse_images(html, BASE_URL) print(f"共解析到 {len(images)} 张图片") for i, item in enumerate(images, 1): fname = safe_name(item["name"], i, item["img"]) fpath = os.path.join(SAVE_DIR, fname) try: download(session, item["img"], fpath) print(f"[{i}/{len(images)}] OK -> {fname}") except Exception as e: print(f"[{i}/{len(images)}] FAIL {item['img']} : {e}") time.sleep(random.uniform(0.2, 0.5)) if __name__ == "__main__": main()这段代码可以直接改 BASE_URL 和容器 XPath 就能用。下面拆解几个关键设计。
4.2 流式下载与 chunk 大小
stream=True配合iter_content(chunk_size=8192)是下载大文件的标配。为什么不用resp.content一次性读?因为图片可能几 MB,一次性读进内存,抓几百张时内存会飙。流式下载是边收边写,内存占用恒定。
chunk_size=8192(8KB)是个经验值。太小了系统调用频繁,太大了内存波动大。8KB 到 64KB 都行,我一般用 8192。注意iter_content可能返回空 chunk(keep-alive 的填充),所以要if chunk判一下再写。
4.3 文件名生成:三个必须处理的坑
文件名看着简单,实际是翻车重灾区。三个坑:
坑一:非法字符。图片的 alt 文本里可能有/、:、?、*这些,直接当文件名在 Windows 上会报错。所以safe_name里做了字符过滤。
坑二:重名覆盖。两张图 alt 一样,文件名就撞了,后下的覆盖先下的。解决办法是加序号前缀,f"{idx:03d}_"保证唯一。03d表示补零到 3 位,这样文件在资源管理器里是按顺序排的,看着舒服。
坑三:扩展名丢失。URL 可能带查询参数123.jpg?size=big,直接splitext会把.jpg?size=big当扩展名。所以先url.split("?")[0]去掉参数再取扩展名。如果 URL 里压根没扩展名(比如/image/123),就默认.jpg。
实操心得:alt 文本可能很长,我截断到 50 字符。太长的文件名在某些系统上有长度限制,而且看着乱。
4.4 下载时的 Referer 处理
前面提过,图片站常校验 Referer。上面代码里 session 的 Referer 设的是列表页地址,下载图片时用的还是这个 session,所以 Referer 自动带上了。但如果图片来自 CDN,Referer 可能要求是详情页地址。稳妥做法是下载每张图前,把 session 的 Referer 改成该图对应的详情页 URL:
session.headers["Referer"] = item["detail"] or BASE_URL这样更接近真实浏览行为——用户是从详情页看到大图再保存的。如果还是 403,试试把 Referer 去掉,或者换成站点首页,不同站点策略不同,多试几次就知道了。
5. 常见问题与排查技巧实录
5.1 抓不到图片:先判断静态还是动态
最常见的求助是"代码跑通了但一张图没抓到"。排查顺序:
第一步,确认 HTML 里有没有图片地址。把get_html拿到的html存成文件,用编辑器搜.jpg。搜得到,说明是静态的,问题在 XPath;搜不到,说明图片是 JS 动态加载的,requests 拿到的 HTML 里根本没有。
第二步,如果是静态的,把 XPath 拿到浏览器控制台里测。F12 的 Console 里输入$x('//div[contains(@class,"picture-area")]'),看能不能选中。选不中就调整 XPath,选得中就说明代码里的解析有问题(多半是编码或容器索引)。
第三步,如果是动态的,requests 这条路走不通了。要么找它背后的数据接口(F12 的 Network 面板,筛选 XHR,看有没有返回图片列表的 JSON 接口),直接请求接口;要么上 Selenium/Playwright 这类能执行 JS 的工具。热词里"关于前端的一些安全技术操作""怎么在前端进行防止爬虫"说的就是这类对抗——但我们的原则是只抓公开的、允许抓的内容,遇到明确的反爬就停手,不硬刚。
5.2 常见问题速查表
| 现象 | 可能原因 | 排查/解决 |
|---|---|---|
| 中文乱码 | 编码判断错 | 手动设resp.encoding,试 utf-8 / gbk |
| 抓到 0 张图 | XPath 错 / 动态加载 | 存 HTML 搜.jpg,控制台测 XPath |
| 图片 403 | 缺 Referer | 下载时设 Referer 为详情页地址 |
| 图片是占位图 | 懒加载 | 取>seen = set() if item["img"] in seen: continue seen.add(item["img"])技巧四:断点续传。抓大量图片时,中途挂了不想重头来。可以在下载成功后往一个 技巧五:验证图片完整性。下载完的图片可能因为网络中断只有一半。简单校验是看文件大小,小于 1KB 的基本是坏的。更严格的是用 6. 从第一页到可持续的抓取方案6.1 第一页跑通后怎么扩展第一页跑通只是起点。要抓多页,核心是找到翻页规律。常见的有三种:
前两种直接构造 URL 循环即可,第三种要写个循环:请求当前页 → 解析图片 → 解析下一页链接 → 请求下一页,直到没有下一页。注意设个最大页数上限,防止死循环。 6.2 并发下载:线程池的正确用法单线程下载几十张图还行,几百张就慢了。用
6.3 合规与边界:什么该抓什么不该抓最后必须说清楚边界。抓图片这件事,技术上能做,不代表什么都能做:
我个人在实际操作中的体会是:爬虫技术本身是中性的,关键看用在哪、怎么用。把目标定在"学习 HTTP 请求、HTML 解析、文件 IO 这条完整链路",抓第一页几十张图练手,完全够用,也完全正当。等技术练熟了,去抓自己有权访问的数据、做自己的数据分析,那才是这门手艺真正的价值所在。 代码写到最后,你会发现真正花时间的不是那几行 requests 和 XPath,而是编码、超时、重试、文件名、去重这些"脏活"。把这些细节处理干净,你的爬虫才算从"能跑"变成"能用"。
版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设
2026/10/16 0:26:27
Windows 64位Oracle 12c客户端安装配置与TNS解析避坑指南简介:Oracle 12c 客户端 Windows 64 位官方安装包,面向需要在 Windows 平台连接和管理 Oracle 数据库的 DBA、后端开发与运维人员。它提供 SQL*Plus、SQL Developer、Oracle Net Services 等核心组件,解决客户端与不同操作系统数据库服务器之…
网站建设
2026/10/20 1:33:02
Win7 64位打印机装不上?PrintSpooler服务排查与修复指南/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …
网站建设
2026/10/19 20:49:04
编译产物如何落地?从本地运行到手机端部署的完整指南编译成功之后,很多人会愣在原地。无论是用 GCC、MSVC 还是其他编译器,辛辛苦苦把源码撸成了可执行文件,结果下一个问题立刻冒出来:这东西到底怎么在本地跑起来?怎么塞进手机里用?网上教程往往只讲到“编译通…
网站建设
2026/10/13 1:45:40
字节跳动AI编程神器Trae实战:从0开发一个Google插件,TaoToken统一Key打通API调用/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …
网站建设
2026/10/16 5:18:57
Claude 拔网线后,Trae 用户的 AI 编程工作流还能怎么接 TaoToken/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …
网站建设
2026/10/16 5:29:22
微信聊天记录提取 + AI 分析:用 Python 与 MCP 把日常对话变成可挖掘的数据宝库/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … |