Python爬虫实战:抓取网站图片区第一页所有图片

发布时间:2026/10/9 19:42:11
Python爬虫实战:抓取网站图片区第一页所有图片 1. 从图片区第一页这个需求说起抓图任务的边界到底在哪很多人拿到抓取某个网站图片区第一页所有图片这个需求时第一反应是打开浏览器、右键查看源码、找到img标签、复制链接、写个requests.get循环下载收工。但真正跑过几个站点的人都知道这套流程在真实环境里能一次跑通的概率不到三成。原因不在于代码写得对不对而在于第一页这三个字背后藏着一堆需要提前想清楚的问题。先把这个任务拆开看。所谓图片区通常指的是页面里一个相对独立的板块它可能是一个div classgallery也可能是一个ul列表甚至可能是JS动态渲染出来的瀑布流。所谓第一页意味着你只需要处理首页展示的那一批图片不需要翻页、不需要处理分页参数、不需要考虑去重跨页的问题——这其实大大降低了难度但也意味着你不能用反正后面会翻页来掩盖首页解析不完整的问题。所谓所有图片则涉及一个关键判断是只要img src里能直接看到的缩略图还是要点进详情页拿原图这两者的工作量差着数量级。我个人的经验是接到这类需求先别急着写代码花五分钟做三件事第一用浏览器开发者工具F12的Network面板刷新页面看图片请求是直接出现在HTML文档里还是通过XHR/fetch异步加载的第二看图片URL有没有规律比如是否带尺寸参数、是否走CDN、是否有防盗链Referer校验第三确认页面有没有反爬机制比如请求头校验、频率限制、Cookie验证。这三件事决定了你后面用requestsxpath能不能搞定还是必须上selenium或者分析接口。提示判断图片是否动态加载有个简单办法——在浏览器里禁用JavaScript后刷新页面如果图片区空了那基本就是JS渲染的纯requests拿不到。这个任务适合谁参考如果你是刚学完requests和xpath想做个小项目练手这个场景非常合适因为它足够小、足够聚焦不会一上来就被复杂的登录、验证码、分布式调度劝退。但如果你已经做过几个爬虫项目这里面的细节——比如URL规范化、文件名冲突处理、下载失败重试——依然值得对照检查一遍因为很多能跑的脚本其实经不起断网重跑。下面我会按照一个真实项目的推进顺序从环境准备、页面结构分析、代码实现、踩坑排查到优化扩展把这件事完整讲一遍。所有代码都是可以直接复制运行的涉及具体站点的地方我会用占位符和通用写法处理你替换成自己的目标即可。2. 动手前的环境与工具准备别在第一步就埋雷2.1 Python环境与依赖库的选择逻辑这个任务用到的库不多但每一个都有选择理由。核心是三个requests负责发HTTP请求lxml负责解析HTMLos和pathlib负责文件管理。有人会问为什么不用BeautifulSoup其实两者都能用但lxml配合xpath在解析速度和语法表达力上更占优势尤其是当你要精确定位图片区这个特定容器时xpath的路径表达式比CSS选择器更灵活。安装命令很简单pip install requests lxml如果你用的是较新的Python版本3.8以上这两个库的兼容性都没问题。这里有个小细节lxml在Windows上安装时偶尔会因为缺少编译环境报错遇到这种情况直接装预编译的wheel包即可或者用pip install --only-binary :all: lxml强制走二进制安装。至于要不要用selenium我的建议是先用requests试一次。如果返回的HTML里能搜到图片链接那就没必要上浏览器自动化因为selenium启动浏览器、等待渲染的开销大得多而且调试起来更麻烦。只有当确认图片是JS动态插入的才考虑换方案。2.2 请求头里必须带的几个字段很多新手写爬虫失败不是代码逻辑问题而是请求头太裸。默认的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, Referer: https://www.example.com/, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }这里Referer特别关键。很多图片站会对图片资源做防盗链服务器检查请求来源是不是自家页面如果不是就返回403或者一张占位图。加上Referer指向目标站点首页能绕过大部分基础防盗链。Accept和Accept-Language则是让请求看起来更像人虽然不是必须但加上没坏处。注意请求头里的Referer要和实际请求的域名一致不要照抄示例里的example.com否则反而会触发校验失败。2.3 目录结构规划别把图片全堆在一个文件夹在写下载逻辑之前先把文件存放结构想好。我见过太多脚本把所有图片直接丢在脚本同级目录跑一次下来几百个文件混在一起找都不好找。推荐的做法是按站点或按日期建子目录from pathlib import Path save_dir Path(downloads) / site_images save_dir.mkdir(parentsTrue, exist_okTrue)pathlib比传统的os.path更直观mkdir(parentsTrue, exist_okTrue)这一行就解决了目录不存在就创建、存在也不报错的问题。如果你后续要抓多个站点可以再按域名分一层这样管理起来清晰得多。3. 页面结构分析找到图片区并提取所有图片链接3.1 用开发者工具定位图片容器打开目标页面按F12切到Elements面板用左上角的选择箭头点一下图片区的任意一张图。这时候你会看到高亮的那行HTML通常长这样div classpic-list a href/detail/12345.htmlimg srchttps://cdn.example.com/thumb/12345.jpg alt.../a a href/detail/12346.htmlimg srchttps://cdn.example.com/thumb/12346.jpg alt.../a ... /div关键信息有三个容器元素的特征这里是classpic-list、图片标签的位置a里面包着img、图片URL的形态缩略图路径带thumb。你要做的是从这个容器里把所有img的src取出来。但这里有个常见陷阱有些站点的图片区里混着图标、广告图、占位图如果你无脑取所有img会把logo、按钮图标也下载下来。解决办法是观察图片URL的规律比如真实图片都在/thumb/或/upload/路径下而图标在/static/或/assets/下用xpath的contains函数过滤。3.2 写一条能稳定命中的XPathXPath的写法直接决定了脚本的健壮性。最粗糙的写法是//img/src取全页所有图片肯定包含杂质。稍微好一点的是限定容器//div[classpic-list]//img/src。但class属性有时候会带多个值或者动态变化更稳的写法是用containsimg_urls tree.xpath(//div[contains(class, pic-list)]//img/src)如果图片链接不是放在src里而是放在># 兼容懒加载的写法 img_urls tree.xpath(//div[contains(class, pic-list)]//img/data-src) if not img_urls: img_urls tree.xpath(//div[contains(class, pic-list)]//img/src)这种先试data-src没有再回退到src的写法在实际项目里非常实用因为不同页面模板可能不一致多一层兜底就少一次翻车。3.3 处理相对路径与协议缺失提取出来的URL不一定是完整的。常见的有三种形态完整URLhttps://...、协议相对URL//cdn.example.com/...、根路径相对URL/upload/2024/...。如果你直接拿这些去requests.get后两种会报错。所以必须做一次规范化from urllib.parse import urljoin base_url https://www.example.com full_urls [urljoin(base_url, u) for u in img_urls]urljoin会自动处理各种相对路径的情况比手动拼接字符串靠谱得多。这一步看起来简单但漏掉的话脚本会在下载阶段报一堆MissingSchema或InvalidURL排查起来还挺费时间。4. 完整代码实现从请求到落盘的每一步4.1 主流程代码与逐行说明把前面的准备串起来一个完整可运行的脚本大概是这样import requests from lxml import etree from urllib.parse import urljoin from pathlib import Path import time BASE_URL https://www.example.com LIST_URL https://www.example.com/pic/ # 图片区所在页面 SAVE_DIR Path(downloads) / site_images SAVE_DIR.mkdir(parentsTrue, exist_okTrue) 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: BASE_URL, Accept-Language: zh-CN,zh;q0.9, } def fetch_page(url): resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text def parse_images(html): tree etree.HTML(html) urls tree.xpath(//div[contains(class, pic-list)]//img/data-src) if not urls: urls tree.xpath(//div[contains(class, pic-list)]//img/src) return [urljoin(BASE_URL, u) for u in urls] def download_image(url, index): try: resp requests.get(url, headersHEADERS, timeout15, streamTrue) resp.raise_for_status() ext url.split(.)[-1].split(?)[0] if ext.lower() not in (jpg, jpeg, png, gif, webp): ext jpg filename SAVE_DIR / f{index:03d}.{ext} with open(filename, wb) as f: for chunk in resp.iter_content(chunk_size8192): f.write(chunk) print(f[OK] {filename}) return True except Exception as e: print(f[FAIL] {url} - {e}) return False def main(): html fetch_page(LIST_URL) urls parse_images(html) print(f共找到 {len(urls)} 张图片) for i, u in enumerate(urls, 1): download_image(u, i) time.sleep(0.3) if __name__ __main__: main()这段代码里有几个地方值得单独说。resp.encoding resp.apparent_encoding这行是为了解决中文乱码问题requests默认用响应头里的编码但有些站点声明不规范用apparent_encoding让它自己猜更稳。streamTrue配合iter_content是下载大图的标准姿势避免一次性把整个图片读进内存。time.sleep(0.3)是给服务器留喘息时间也是降低被封风险的最简单手段。4.2 文件名策略为什么用序号而不是原始文件名你可能注意到我用{index:03d}.{ext}做文件名而不是用URL里的原始名字。这是有意的。原始文件名经常重复不同目录下同名文件、可能包含非法字符?、、中文、可能超长导致系统报错。用序号命名简单可靠配合前面的目录规划文件管理一目了然。如果你需要保留原始信息可以在下载时同时写一个manifest.txt记录序号和URL的对应关系。with open(SAVE_DIR / manifest.txt, a, encodingutf-8) as f: f.write(f{index}\t{url}\n)这个清单文件在后续排查哪张图没下下来时特别有用比翻控制台日志方便。4.3 异常处理哪些错误必须捕获哪些可以放过下载环节的异常大致分三类。第一类是网络层面的比如Timeout、ConnectionError这类必须捕获并记录因为可能是临时抖动重试一次往往就好了。第二类是HTTP状态码异常raise_for_status()会把4xx和5xx抛出来403通常是防盗链或封禁404是链接失效这两种处理方式不同——403要考虑换请求头或加延迟404直接跳过即可。第三类是文件写入异常比如磁盘满、权限不足这类比较少见但一旦发生整个脚本会崩用try包住更稳妥。我一般会给下载函数加一个简单的重试def download_with_retry(url, index, retries3): for attempt in range(retries): if download_image(url, index): return True time.sleep(1) return False三次重试加一秒间隔能覆盖绝大多数临时故障。注意不要无限重试否则遇到死链会卡住整个流程。5. 实测中容易翻车的几个点与排查链路5.1 图片下载下来是0字节或者一张小图这是最典型的防盗链症状。表现是requests.get返回200但文件打开是空白或者一张写着禁止盗链的占位图。排查链路是这样的先用浏览器直接打开图片URL能正常显示说明图片本身没问题然后在代码里打印resp.headers看Content-Type是不是image/jpeg如果是text/html那说明返回的是错误页最后检查请求头里的Referer是不是和目标站点匹配。九成情况下补上正确的Referer就能解决。还有一种情况是图片走了CDNCDN要求带特定的Referer白名单。这种如果补Referer还不行可以试试把Referer设成图片所在页面的完整URL而不是首页。5.2 XPath能匹配到但数量对不上明明页面上看到20张图XPath只取到12张。这种通常是懒加载导致的——未滚动到的区域>session requests.Session() session.headers.update(HEADERS) session.get(BASE_URL) # 先访问首页拿Cookie resp session.get(LIST_URL)用Session的好处是自动保持Cookie而且底层复用TCP连接速度也更快。这个技巧在需要登录态或者有前置访问要求的站点上几乎是必备的。提示如果遇到验证码说明频率已经触发了风控这时候继续硬跑只会让封禁时间更长。正确做法是停下来把间隔调大或者换一个时间段再跑。5.4 中文文件名乱码与路径过长虽然我推荐用序号命名但如果你确实需要保留原始文件名中文乱码是个坑。Windows下文件名编码和Python的字符串编码不一致时会出现乱码或者写入失败。解决办法是用urllib.parse.unquote先解码URL里的中文再用re.sub过滤掉非法字符import re from urllib.parse import unquote name unquote(url.split(/)[-1]) name re.sub(r[\\/:*?|], _, name)路径过长的问题在Windows上比较常见文件名加目录超过260字符就会报错。用序号命名基本能规避如果非要保留长名字可以考虑截断或者用哈希值。6. 脚本跑通之后的优化方向与扩展思路6.1 并发下载什么时候值得上什么时候是负担单线程下载20张图可能十几秒如果图片多或者单张体积大串行就会很慢。这时候可以用concurrent.futures的线程池from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers5) as executor: executor.map(lambda args: download_image(*args), enumerate(urls, 1))但并发不是越多越好。max_workers设太大一方面会给目标服务器造成压力容易触发风控另一方面本地磁盘IO也可能成为瓶颈。我的经验是5到8个线程比较合适既能提速又不会太激进。另外并发下time.sleep的位置要注意别在每个线程里都睡那样等于没并发。6.2 增量下载避免重复劳动脚本跑第二次的时候已经下载过的图片不应该再下一遍。最简单的做法是下载前检查文件是否存在filename SAVE_DIR / f{index:03d}.{ext} if filename.exists(): print(f[SKIP] {filename}) return True但序号命名有个问题如果第一次跑到一半中断第二次重新抓取时URL顺序可能变了序号对不上。更稳的做法是用URL的哈希值做文件名import hashlib name hashlib.md5(url.encode()).hexdigest()[:16]这样无论跑多少次同一张图永远对应同一个文件名天然支持增量。6.3 从第一页扩展到多页要注意什么虽然这个任务只要求第一页但很多人跑通之后会想扩展到多页。这里提前说几个坑第一分页URL的规律要摸清是?page2还是/pic/2.html不同站点不一样第二跨页去重同一张图可能出现在多个页面第三翻页的终止条件别写成死循环。如果真要扩展建议把获取列表页URL和解析图片拆成两个独立函数中间用一个生成器串联这样逻辑清晰也好调试。6.4 关于合规与礼貌抓取的个人体会最后说点实在的。抓取公开页面上的图片用于个人学习研究和批量抓取用于商业用途性质完全不同。我在实际项目里会做几件事控制请求频率不让服务器因为我的脚本而变慢遵守站点的robots.txt虽然它没有强制力但它是站点运营者表达意愿的方式不抓取需要登录才能访问的内容不绕过明确的技术保护措施。这些不是法律建议而是一个长期做数据采集的人应该有的基本意识——你今天怎么对待别人的站点决定了别人怎么对待你的站点。脚本本身不复杂难的是遇到问题时知道往哪个方向排查。上面这些坑我都实际踩过有些是熬夜调出来的有些是跑了几个月之后才暴露的。你把代码复制过去跑一遍大概率还会遇到我没提到的具体情况但只要掌握了看请求头、看响应、看页面结构这个排查顺序大部分问题都能自己定位。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询