图片是网络上最重量级、最有用的文件之一:用于商品目录的产品照片、研究数据集的图表、机器学习流水线的素材。当您需要的不止几张时,点击"图片另存为"便不再是选项,一个简短的 Python 脚本可以在几秒内完成原本需要一下午的工作。
本指南介绍六种实用的 Python 下载图片方法,从使用 requests 下载单个文件,到用 BeautifulSoup 抓取页面上的所有图片,再到分块流式传输大文件、整理保存结果,以及通过 Crawlbase Crawling API 访问受保护的来源。每段代码均可实际运行,您可以直接复制并根据自己的目标进行调整。
您将构建的内容
学完本指南,您将拥有一套涵盖常见场景的小型函数工具集,以及一个将它们整合在一起的简短脚本。
-
单文件下载。通过
requests按 URL 获取一张图片并写入磁盘。 -
标准库下载。使用
urllib.request实现同样功能,无需第三方包。 -
页面爬取。用 BeautifulSoup 找到页面上的每个
<img>标签并下载各自来源。 - 分块流式传输。分段保存大文件,使内存占用保持平稳。
- 命名与整理。生成安全的文件名,并将下载文件分类存放到目录中。
- 受保护来源。通过 Crawling API 访问渲染后或受机器人防护的图片。
为什么普通下载在某些网站上会失败
最简单的图片下载就是一个 HTTP GET。对于从可预测 URL 提供的静态文件,这完全没问题。麻烦出现在真实网站上。有些页面在初始 HTML 加载后才通过 JavaScript 构建图片网格,因此裸请求返回的标记中根本没有图片标签。另一些则处于反机器人防护之后,会挑战或屏蔽来自数据中心 IP 或任何不像真实浏览器的请求,您只能得到一个 403 或 HTML 错误页,而非预期的 JPEG。
对于简单文件,下面前五种方法已经足够。对于棘手的情况,最后一种方法将请求路由通过渲染层和可信 IP 池,使文件能够完整返回。我们先介绍简单路径,因为大多数下载从不需要更多。
前提条件
跟进本指南所需不多。
Python 3.8 或更高版本。使用 python --version 检查您的版本。如果尚未安装,请从 python.org 安装。
基础 Python 知识。您应当能够运行脚本并使用 pip 安装包。函数、循环和 with 语句就足够了。
Crawlbase 账户(仅最后一种方法需要)。前五种方法仅使用 requests、urllib 和 BeautifulSoup。对于受保护来源方法,您需要一个免费的 Crawlbase 账户及其 API token。
搭建项目
创建虚拟环境以隔离项目依赖,然后安装本指南用到的两个第三方库。
python --version python -m venv image_env source image_env/bin/activate pip install requests beautifulsoup4
在 Windows 上,请使用 image_env\Scripts\activate 代替 source 命令。requests 是获取每个文件的 HTTP 客户端,beautifulsoup4 解析页面 HTML 以便您找到图片标签。urllib、os 和 hashlib 是标准库的一部分,无需安装。
技巧一:使用 requests 下载单张图片
最常见的情况是下载给定 URL 的一张图片。发送 GET 请求,确认响应确实是图片,然后以二进制模式将字节写入文件。在写入之前检查状态码和内容类型,可以避免将 HTML 错误页以 .jpg 为名保存。
import requests url = "https://www.python.org/static/img/python-logo.png" headers = {"User-Agent": "Mozilla/5.0 (image downloader)"} response = requests.get(url, headers=headers, timeout=10) if response.status_code == 200 and "image" in response.headers.get("Content-Type", ""): with open("python-logo.png", "wb") as f: f.write(response.content) print("Saved python-logo.png") else: print(f"Skipped: {response.status_code} {response.headers.get('Content-Type')}")
这里有三个细节值得关注。文件以 "wb"(写二进制)模式打开,因为图片数据是字节而非文本,以文本方式写入会损坏文件。Content-Type 检查确认服务器确实返回了图片,而非一个恰好返回状态码 200 的错误页。timeout 防止脚本在服务器停滞时永久挂起。运行此代码,您应该能看到 Saved python-logo.png,脚本旁边有一个真实的 PNG 文件。这就是一次成功的下载。
上面的单文件下载在文件通过普通 URL 提供时有效。一旦页面将图片隐藏在 JavaScript 后面或屏蔽数据中心请求,该 GET 请求就会返回错误页而非字节。Crawling API 在真实浏览器中渲染页面,在服务器端轮换住宅 IP,然后返回完整响应,让您无需自行运行无头浏览器集群和代理池。在构建该基础设施之前,先在免费套餐上试用。
技巧二:使用标准库中的 urllib 下载
如果您不想添加依赖,标准库也能完成同样的工作。urllib.request 随 Python 附带,因此这种方法无需安装任何东西。通过 Request 对象设置 User-Agent 有助于应对拒绝默认 urllib 代理的服务器。
import urllib.request url = "https://www.python.org/static/img/python-logo.png" req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (image downloader)"}) with urllib.request.urlopen(req, timeout=10) as resp, open("logo_urllib.png", "wb") as f: f.write(resp.read()) print("Saved logo_urllib.png")
with 语句同时打开连接和输出文件,并在代码块结束时整洁地关闭它们,即使在写入过程中发生错误也是如此。还有一个单行快捷方式 urllib.request.urlretrieve(url, "logo.png"),对于快速脚本很方便,但无法控制请求头或错误处理,因此上面的显式形式是更安全的默认选项。requests 和 urllib 都能将相同的字节写入磁盘;requests 只是拥有更友好的 API,这也是本指南其余部分使用它的原因。
技巧三:使用 BeautifulSoup 下载页面上的所有图片
下载一个文件很简单。真正的工作是自动提取页面上的所有图片。模式分两步:获取页面 HTML,然后用 BeautifulSoup 解析它,收集每个 <img> 标签的 src,最后循环遍历这些 URL,复用技巧一中的单文件下载逻辑。
import requests from bs4 import BeautifulSoup from urllib.parse import urljoin page_url = "https://en.wikipedia.org/wiki/Python_(programming_language)" headers = {"User-Agent": "Mozilla/5.0 (image downloader)"} page = requests.get(page_url, headers=headers, timeout=10) soup = BeautifulSoup(page.text, "html.parser") image_urls = [] for img in soup.select("img"): src = img.get("src") if src: image_urls.append(urljoin(page_url, src)) print(f"Found {len(image_urls)} images")
这里有两点值得细说。选择器 img 抓取页面上的每个图片标签,使用 img.get("src") 而非 img["src"] 可以在属性缺失时返回 None 而非抛出异常。另一个关键点是 urljoin:图片来源通常是相对路径(如 /images/photo.jpg),将其与页面 URL 拼接后就变成了完整的可下载地址。如需深入了解元素选择方法,请参阅 如何在 Python 中使用 BeautifulSoup。
有了绝对 URL 列表,下载循环复用了技巧一中的二进制写入模式,并为每个文件赋予自己的名称。
import os os.makedirs("downloads", exist_ok=True) for i, img_url in enumerate(image_urls): try: r = requests.get(img_url, headers=headers, timeout=10) if r.status_code == 200 and "image" in r.headers.get("Content-Type", ""): path = os.path.join("downloads", f"image_{i}.jpg") with open(path, "wb") as f: f.write(r.content) except requests.RequestException as e: print(f"Failed {img_url}: {e}")
一旦下载多个文件,每个请求周围的 try/except 就不是可选项了。在数十张图片中,总有一张会超时、返回重定向循环或消失,捕获 requests.RequestException 可以让循环跳过有问题的那张并继续,而非在第三十个文件时崩溃。os.makedirs(..., exist_ok=True) 调用会创建输出文件夹一次,如果已存在则什么都不做。
技巧四:分块流式传输大文件
读取 response.content 会在写入之前将整个文件加载到内存中。对于 Logo 来说没问题,但对于高分辨率照片或多兆字节素材则很浪费,在连续下载大量文件时可能耗尽内存。流式传输响应并以固定大小的块写入可以使内存占用保持平稳,无论文件大小如何。
def download_stream(url, path, chunk_size=8192): with requests.get(url, headers=headers, stream=True, timeout=30) as r: r.raise_for_status() with open(path, "wb") as f: for chunk in r.iter_content(chunk_size=chunk_size): f.write(chunk) return path download_stream( "https://upload.wikimedia.org/wikipedia/commons/c/c3/Python-logo-notext.svg", "downloads/large_logo.svg", )
传入 stream=True 告诉 requests 不要提前下载正文;iter_content 会将其以 chunk_size 字节的片段拉取,并将每个片段直接写入磁盘。峰值内存仅保持在一个块的大小(这里是八千字节),无论文件多大。raise_for_status() 将 4xx 或 5xx 响应转换为异常,防止您静默保存错误正文;请求上的外层 with 确保文件写入后连接被释放。
技巧五:命名与整理文件
把所有文件保存为 image_0.jpg、image_1.jpg 固然可行,但会丢弃原始文件名和文件扩展名, 如果您混合使用 PNG、JPEG 和 SVG,这一点很重要。一个小型辅助函数从 URL 推导出干净的名称,并在 URL 中没有可用名称时回退到内容哈希,这既保证唯一性,又避免覆盖碰巧同名的两个文件。
import os import hashlib from urllib.parse import urlparse def filename_for(url, content, folder="downloads"): name = os.path.basename(urlparse(url).path) if not name or "." not in name: digest = hashlib.md5(content).hexdigest()[:12] name = f"{digest}.jpg" return os.path.join(folder, name) # Example: turn a messy URL into a tidy path r = requests.get(image_urls[0], headers=headers, timeout=10) print(filename_for(image_urls[0], r.content))
urlparse(...).path 从 URL 中剥离查询字符串和片段,os.path.basename 只取最后一段,因此 .../photo.jpg?size=large 变为 photo.jpg。当 URL 中没有真正的文件名时,文件字节的 MD5 哈希提供了一个简短、稳定、抗碰撞的名称。同样的哈希也是查找重复项的最简方法:两个相同的图片产生相同的摘要,您可以在写入之前跳过已保存的文件。
如果您同时从多个页面或类别下载,请为每个来源传入不同的 folder,使文件落入各自的目录。结合 os.makedirs(folder, exist_ok=True),这可以让大型任务在磁盘上保持整洁,便于之后快速找到特定图片,而无需扫描一个巨大的文件夹。
技巧六:通过 Crawling API 从受保护来源下载
上面五种方法涵盖了您可以通过普通请求访问的所有图片。有些网站则不那么配合。页面可能使用 JavaScript 渲染图片网格,导致请求返回的标记中没有 <img> 标签;或者图片主机屏蔽数据中心 IP,返回 403 而非文件。您可以自己运行无头浏览器并维护轮换住宅代理池来解决这两个问题,但构建和维护这套设施才是大部分工程工作,与您想要的图片毫无关系。
Crawling API 将渲染和 IP 轮换整合进一次请求。您安装官方客户端,然后通过它路由页面请求;返回的 HTML 是完整渲染的,因此技巧三中的 BeautifulSoup 解析无需修改即可使用。
pip install crawlbase
from crawlbase import CrawlingAPI from bs4 import BeautifulSoup from urllib.parse import urljoin api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) page_url = "https://example.com/gallery" result = api.get(page_url, {"ajax_wait": "true", "page_wait": 5000}) html = result["body"].decode("utf-8") if result["status_code"] == 200 else None # Same parsing as Tip 3, now against rendered HTML soup = BeautifulSoup(html, "html.parser") image_urls = [urljoin(page_url, img["src"]) for img in soup.select("img[src]")] print(f"Found {len(image_urls)} images on the rendered page")
两个等待选项对于客户端渲染的图库至关重要。ajax_wait 告知 API 等待异步内容加载完成,page_wait 则在加载后额外等待固定毫秒数,以便延迟出现的图片在捕获前出现。对于在浏览器中构建图片网格的网站使用 JavaScript token;对于静态图片主机则使用普通 token 更快。image_urls 填充后,直接将其传入技巧四中的 download_stream 辅助函数。如果您只需要 IP 轮换而不需要渲染(例如页面 HTML 没问题但图片主机屏蔽您时),Smart AI Proxy 通过对代理设置进行一行更改,将普通 requests 流量路由通过同一可信池。
输出示例
页面爬取完成后,一个您保存内容的小清单对于审计和跳过下次运行中的重复项非常有用。每个文件写一条记录,可以在图片旁边生成结构化日志。
[ { "source_url": "https://example.com/img/photo-01.jpg", "saved_as": "downloads/photo-01.jpg", "content_type": "image/jpeg", "bytes": 184213 }, { "source_url": "https://example.com/img/diagram.svg", "saved_as": "downloads/diagram.svg", "content_type": "image/svg+xml", "bytes": 9042 } ]
每条记录将原始 URL 与本地文件关联,记录内容类型以了解真实格式,并记录字节大小。保留 source_url 意味着后续运行可以在下载之前检查您已有的内容。
扩展至批量图片
对于几十个文件,技巧三中的顺序循环就够用了。当您需要下载数千张图片时,逐一下载就成了瓶颈,因为大部分时间花在等待网络,而非您的 CPU。线程池允许多个下载并发运行,同时比完整的异步代码简单得多。
from concurrent.futures import ThreadPoolExecutor def save_one(url): try: r = requests.get(url, headers=headers, timeout=15) if r.status_code == 200 and "image" in r.headers.get("Content-Type", ""): path = filename_for(url, r.content) with open(path, "wb") as f: f.write(r.content) return path except requests.RequestException: return None with ThreadPoolExecutor(max_workers=8) as pool: saved = list(pool.map(save_one, image_urls)) print(f"Saved {len([p for p in saved if p])} of {len(image_urls)} images")
八个工作线程是合理的起点;设置过高则有可能对主机造成过大压力,这既不礼貌,也是快速触发速率限制的方式。每个 save_one 调用都是自包含的,并吞下自己的错误,使单次失败不会拖垮整个批次。如果您从受保护来源大量下载,请通过 Crawling API 或 Smart AI Proxy 路由 save_one,让轮换 IP 承担负载,而非您的单个地址。有关保持大型任务运行的更多信息,请参阅 如何在不被屏蔽的情况下爬取网站。
负责任地下载图片
图片不是自由流通的数据,它们是创意作品,您在网上找到的几乎每张图片都受到创作者版权保护。能够下载文件并不等于被允许使用它。在将脚本指向某个网站之前,请阅读其服务条款并检查其 robots.txt,将请求量保持在不给服务器造成负担的水平,并且只下载您确实有权使用的内容。登录后的照片、个人图片以及未获授权的图库素材都属于禁区。
同样的注意事项也适用于您之后的操作。不要将他人图片作为自己的内容重新分发,也不要在未获权利持有人许可的情况下将受版权保护的媒体输入模型或数据集。当网站为其媒体提供官方 API 或授权途径时,请使用它:这是获取可以在法律层面依赖的图片的最清晰方式,通常还附带明确说明您可以如何使用这些图片的条款。
核心要点
-
二进制模式不可妥协。始终以
"wb"打开输出文件,并在写入前检查响应确实是图片,这样您就永远不会将错误页保存为 JPEG。 -
整页下载时先查找后获取。用 BeautifulSoup 解析 HTML 收集每个
<img>来源,用urljoin解析相对路径,然后在有保护的循环中下载每个 URL。 -
分块流式传输大文件。使用
stream=True和iter_content,无论文件大小如何,峰值内存保持平稳。 - 有意命名和去重。从 URL 推导干净的文件名,回退到内容哈希,并使用该哈希跳过重复项。
- 通过 API 访问受保护来源。当页面用 JavaScript 渲染图片或屏蔽您的 IP 时,Crawling API 返回渲染后的 HTML,使您现有的解析器和下载代码继续正常工作。
常见问题
Python 中下载图片最简单的方法是什么?
使用 requests 发送 GET 请求,然后将 response.content 写入以二进制模式("wb")打开的文件。对于单个文件只需三行代码。先检查状态码和 Content-Type 请求头,确保只保存真实图片数据,而非一个恰好返回 200 状态的 HTML 错误页。
我应该使用 requests 还是 urllib?
两者下载的字节相同。requests 拥有更简洁的 API、更易用的请求头处理和内置流式传输,这也是大多数代码使用它的原因。当您需要零第三方依赖时使用 urllib.request,因为它随 Python 附带。除此之外,下载逻辑完全相同。
如何下载网页上的所有图片?
获取页面 HTML,用 BeautifulSoup 解析,收集每个 <img> 标签的 src。使用 urljoin 将相对路径解析为绝对 URL,然后遍历列表用单图片模式下载每个文件。将每次下载包裹在 try/except 中,使一个错误 URL 不会停止整个批次,如技巧三所示。
为什么我下载的文件打开后显示为损坏图片?
通常有两个原因之一。要么文件以文本模式而非二进制模式打开,这会损坏字节,因此请确保使用 "wb"。要么服务器返回的是 HTML 错误页而非图片,这就是为什么在写入前检查状态码和 Content-Type 很重要。在 JavaScript 密集型页面上,图片可能根本不存在于初始 HTML 中,此时您需要一个渲染步骤。
如何在不被屏蔽的情况下下载图片?
发送真实的 User-Agent,用短暂延迟限制请求速率,避免用大量并发线程冲击同一主机。在较大请求量时,您还需要看起来像真实访客的 IP,而单台机器无法提供这一点。通过 Crawling API 或 Smart AI Proxy 轮换住宅 IP 路由流量,是防止大规模图片下载触发速率限制的关键。
如何避免两次下载相同的图片?
用 hashlib 计算每个文件字节的哈希,并将您已见过的摘要保存在集合中。在写入新文件之前,检查其哈希是否已在集合中;如果是,则跳过。当 URL 中没有可用的名称时,同样的摘要也可以作为可靠、抗碰撞的文件名。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
