Python爬虫实战:requests与XPath抓取图片站第一页

发布时间:2026/10/9 13:16:56

Python爬虫实战:requests与XPath抓取图片站第一页 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容错强写法简单大文档速度慢于 lxmlcv2 / 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/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.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, headersheaders, timeout10) 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配合Retryfrom requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session requests.Session() retry Retry(total3, backoff_factor0.5, status_forcelist[500, 502, 503, 504]) session.mount(http://, HTTPAdapter(max_retriesretry)) session.mount(https://, HTTPAdapter(max_retriesretry))backoff_factor0.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 classmain-content div classpicture-area h2图片区/h2 ul classpic-list lia href/detail/123.htmlimg src/thumb/123.jpg alt图1/a/li lia href/detail/124.htmlimg src/thumb/124.jpg alt图2/a/li /ul /div div classsidebar.../div /div关键点图片区有独立的容器这里是classpicture-area而且它和侧边栏、页脚是平级的。如果你直接//img会把侧边栏的广告图、logo 全抓进来。所以第一步是框定容器。3.2 XPath 定位容器的三种写法定位图片区容器我常用三种写法按可靠性排序写法一按 class 精确定位最稳container tree.xpath(//div[classpicture-area])前提是你确认了 class 名。缺点是 class 可能带多个值比如classpicture-area clearfix这时classpicture-area就匹配不上了。写法二按 class 包含匹配推荐container tree.xpath(//div[contains(class, picture-area)])contains()只要包含子串就匹配容错性好很多。这是我用得最多的写法。写法三按区块标题文本锚定最灵活container tree.xpath(//h2[contains(text(), 图片区)]/parent::div)先找到文字是图片区的标题再往上找它的父容器。这种写法在 class 名混淆、随机生成的站点上特别管用。热词里python xpath 爬虫 text 函数讲的就是这个技巧。注意text()只取直接子文本节点。如果标题是h2span图片区/span/h2text()取不到得用//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.jpgthumb/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(total3, backoff_factor0.5, status_forcelist[500, 502, 503, 504]) session.mount(http://, HTTPAdapter(max_retriesretry)) session.mount(https://, HTTPAdapter(max_retriesretry)) 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;q0.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), streamTrue) resp.raise_for_status() with open(path, wb) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk) def safe_name(name, idx, url): if not name: name fimg_{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_okTrue) 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 大小streamTrue配合iter_content(chunk_size8192)是下载大文件的标配。为什么不用resp.content一次性读因为图片可能几 MB一次性读进内存抓几百张时内存会飙。流式下载是边收边写内存占用恒定。chunk_size81928KB是个经验值。太小了系统调用频繁太大了内存波动大。8KB 到 64KB 都行我一般用 8192。注意iter_content可能返回空 chunkkeep-alive 的填充所以要if chunk判一下再写。4.3 文件名生成三个必须处理的坑文件名看着简单实际是翻车重灾区。三个坑坑一非法字符。图片的 alt 文本里可能有/、:、?、*这些直接当文件名在 Windows 上会报错。所以safe_name里做了字符过滤。坑二重名覆盖。两张图 alt 一样文件名就撞了后下的覆盖先下的。解决办法是加序号前缀f{idx:03d}_保证唯一。03d表示补零到 3 位这样文件在资源管理器里是按顺序排的看着舒服。坑三扩展名丢失。URL 可能带查询参数123.jpg?sizebig直接splitext会把.jpg?sizebig当扩展名。所以先url.split(?)[0]去掉参数再取扩展名。如果 URL 里压根没扩展名比如/image/123就默认.jpg。实操心得alt 文本可能很长我截断到 50 字符。太长的文件名在某些系统上有长度限制而且看着乱。4.4 下载时的 Referer 处理前面提过图片站常校验 Referer。上面代码里 session 的 Referer 设的是列表页地址下载图片时用的还是这个 session所以 Referer 自动带上了。但如果图片来自 CDNReferer 可能要求是详情页地址。稳妥做法是下载每张图前把 session 的 Referer 改成该图对应的详情页 URLsession.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])技巧四断点续传。抓大量图片时中途挂了不想重头来。可以在下载成功后往一个done.txt里追加 URL启动时先读这个文件已下载的跳过。这个习惯在抓几百上千张图时能救命。技巧五验证图片完整性。下载完的图片可能因为网络中断只有一半。简单校验是看文件大小小于 1KB 的基本是坏的。更严格的是用PIL或cv2尝试打开打不开就删掉重下。热词里python 下载 cv2就是这个用途——cv2.imread返回 None 说明图片损坏。6. 从第一页到可持续的抓取方案6.1 第一页跑通后怎么扩展第一页跑通只是起点。要抓多页核心是找到翻页规律。常见的有三种URL 规律/list/1.html、/list/2.html直接改数字循环。参数规律/list?page1改 query 参数。无规律翻页链接在页面里得先解析出下一页的 href再请求。前两种直接构造 URL 循环即可第三种要写个循环请求当前页 → 解析图片 → 解析下一页链接 → 请求下一页直到没有下一页。注意设个最大页数上限防止死循环。6.2 并发下载线程池的正确用法单线程下载几十张图还行几百张就慢了。用concurrent.futures.ThreadPoolExecutor并发下载from concurrent.futures import ThreadPoolExecutor, as_completed with ThreadPoolExecutor(max_workers5) as pool: futures {pool.submit(download, session, it[img], path): it for it, path in tasks} for fut in as_completed(futures): try: fut.result() except Exception as e: print(fail:, e)max_workers5是保守值。别开太大一是对站点压力大二是 requests 的 Session 在多线程下共享要注意线程安全一般读操作没问题但保险起见可以每个线程一个 Session。热词里python 线程嵌套线程python 协程讲的就是并发这块入门阶段用线程池足够协程aiohttp性能更好但学习曲线陡等有需求再上。6.3 合规与边界什么该抓什么不该抓最后必须说清楚边界。抓图片这件事技术上能做不代表什么都能做只抓公开可访问的内容需要登录、付费、有明确访问限制的不碰。遵守站点的 robots.txt它声明了哪些路径不允许抓取看一眼不费事。控制频率别把人家服务器打挂这是最基本的尊重。抓下来的内容仅用于个人学习研究不二次分发、不商用。遇到明确的反爬机制就停手不研究绕过手段。热词里那些防止查看页面源码防止打开的话题从站点角度是合理的自我保护我们作为抓取方应该尊重。我个人在实际操作中的体会是爬虫技术本身是中性的关键看用在哪、怎么用。把目标定在学习 HTTP 请求、HTML 解析、文件 IO 这条完整链路抓第一页几十张图练手完全够用也完全正当。等技术练熟了去抓自己有权访问的数据、做自己的数据分析那才是这门手艺真正的价值所在。代码写到最后你会发现真正花时间的不是那几行 requests 和 XPath而是编码、超时、重试、文件名、去重这些脏活。把这些细节处理干净你的爬虫才算从能跑变成能用。
延伸阅读

更多相关文章

2026/10/9 13:16:56

北理工数据库实验包:SQLite驱动的可验证教学沙盒

简介:本资源是北京理工大学计算机学院“数据库原理与设计”课程配套上机实验材料,面向高校计算机专业本科生及数据库初学者,聚焦关系数据库理论落地与SQL工程实践能力培养。压缩包共12个文件,含4个核心SQL脚本(覆盖建库…

2026/10/9 13:11:55

便携设备电源管理:PMIC与MCU协同设计实战解析

前阵子给某便携数据终端项目做电源部分,主控是 PIC32MZ2048EFM100,电源管理芯片用了 PCA9422。整套系统从锂电池充电、多路电压输出、ADC 电压电流监测到低功耗切换,最后都压在这两颗芯片上。这篇文章把这段完整经历梳理了一遍:为…

2026/10/9 13:11:55

大模型应用落地实战:需求判断、模型选型与提示词工程

1. 先问清楚一个问题:你真的需要大模型吗?2026年聊AI大模型应用,最不缺的就是新鲜玩意儿。多模态模型能看图能听音,智能体动不动就给你整一套自动化流程,看起来什么都能干。但我用了两年多,最大的体会反而是…

2026/10/9 14:22:13

NodeJS旅游网站源码实战:从环境配置到部署上线的完整指南

很多读者拿到这套 NodeJS 旅游网站源码(项目编号 27648)之后,第一反应是赶紧解压、装依赖、跑起来,结果被一堆环境问题卡住;就算侥幸跑通了,想加个功能或者改个页面,又不知道该动哪个文件。我花…

2026/10/9 14:22:13

C# OnnxRuntime 部署 RMBG-2.0 人像抠图工程实践

简介:本资源是一套基于C#与ONNX Runtime部署RMBG-2.0模型的完整人像抠图解决方案,面向图像处理开发者、AI应用集成工程师及.NET生态下的算法落地实践者,解决高精度、低延迟背景去除在桌面端或轻量级服务中的工程化难题。压缩包共234个文件&am…

2026/10/9 14:22:13

电平标准详解:从TTL到LVDS,嵌入式硬件电平匹配与转换实战

1. 电平到底是什么:从一盏灯说起如果你拆过任何一块数字电路板,或者翻过通信原理的教材,一定绕不开“电平”这个词。它听起来像物理课上的术语,实际上却是数字世界最底层的“语言”。简单说,电平就是电压的高低状态&am…

2026/10/9 14:22:13

基于YOLOv8的基建裂缝目标检测系统:从数据集到部署全流程

简介:这份资源是面向计算机、电子信息等专业学生与项目实战学习者的YOLOv8基建裂缝目标检测完整项目包,可直接用于毕业设计、课程设计或期末大作业。项目经导师指导并通过评审,获得98分高分,涵盖从数据准备到模型训练与推理的全流…

2026/10/9 14:22:13

LM2576T-12开关稳压器实战:电感、二极管、电容选型与散热布局

1. 从一颗老芯片说起:LM2576T-12到底是个什么角色如果你拆过十几年前的工控板、老式路由器电源、车载充电器,或者某些工业仪表的供电模块,大概率会在板子上看到一颗TO-220封装、五个引脚、背面带散热片的黑色元件,丝印上写着LM257…

2026/10/8 10:03:18

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/8 10:03:20

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/8 6:05:44

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

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

2026/10/9 0:04:27

毕业论文初稿完成后首次进行AIGC疑似度自查的摸底与分流策略

毕业论文初稿完成后首次进行AIGC疑似度自查的摸底与分流策略当数万字的学位论文初稿经历开题、实验、问卷与多轮文献梳理最终成形时,绝大多数研究生都会面临一道全新的形式审查关卡:AIGC 疑似度排查。在高校毕业审核流程中,盲审前的文本检测通…

2026/10/9 0:04:27

食堂节能改造源头工厂,商用厨房设备焕新方案广受好评

商用厨房作为餐饮经营、单位供餐的核心后勤阵地,其设备配置、动线规划与运维体系直接决定后厨作业效率、运营成本与合规性。从基础的灶具、制冷存储设备,到油烟净化、水处理等配套系统,每一个环节的合理性都与食品安全、能耗管控、消防安全挂…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑