资讯动态

Python图片采集实战:BeautifulSoup+requests从入门到避坑

发布时间:2026/10/16 9:26:07 来源:尧图企业网站定制
1. 为什么图片采集值得单独拎出来讲很多人第一次接触网络数据采集都是从抓图片开始的。原因很简单图片是网页上最直观、最容易验证成果的内容抓下来能立刻看到文件成就感来得快。但真正动手之后才会发现事情没那么简单——有的网站图片是懒加载的源码里根本没有src有的图片地址藏在>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/, Accept: image/avif,image/webp,image/apng,image/*,*/*;q0.8, }这里有几个细节值得说。User-Agent是最基本的不带它很多站点直接 403。Referer是图片防盗链的关键有些站点会检查请求图片时的来源页如果 Referer 不对或者为空就返回一张防盗链占位图。Accept声明你接受图片类型虽然不是必须但加上更接近真实浏览器行为。我个人的经验是请求页面和请求图片用的请求头可以略有不同。请求页面时 Referer 可以是站点首页请求图片时 Referer 最好是图片所在的那个页面地址。这一点在防盗链严格的站点上差别很明显。3.2 图片地址的三种藏法图片地址在 HTML 里的存在形式远比想象中复杂。常见的有三种第一种标准 src。最老实的一种img srchttps://example.com/a.jpg直接取src属性就行。第二种懒加载属性。为了加快首屏速度很多站点把真实地址放在>def extract_img_url(img_tag): # 按优先级尝试多个属性 for attr in [data-src, data-original, data-lazy-src, src]: url img_tag.get(attr) if url and not url.startswith(data:image): return url # 兜底处理 srcset srcset img_tag.get(srcset) if srcset: # 取最后一个通常尺寸最大 return srcset.split(,)[-1].strip().split( )[0] return None这段代码里有个关键判断not url.startswith(data:image)。因为占位图经常是 base64 内联的形如data:image/gif;base64,...这种要直接跳过。3.3 相对地址补全urljoin 的正确用法提取出来的地址经常是相对路径比如/images/a.jpg或者../pic/b.png。直接拿去请求会失败必须拼成绝对地址。很多人用字符串拼接base path这在简单情况下能用但遇到../这种上级目录符号就错了。正确做法是用urllib.parse.urljoinfrom urllib.parse import urljoin base_url https://example.com/list/page1.html relative ../images/a.jpg absolute urljoin(base_url, relative) # 结果https://example.com/images/a.jpgurljoin会自动处理协议、域名、路径层级的各种情况比手写拼接可靠得多。这里有个容易忽略的点base_url 应该是当前页面的完整地址而不是站点根地址。因为../是相对于当前页面路径的用根地址去拼会算错层级。3.4 去重与过滤别把同一张图存十遍一个页面上同一张图片可能出现多次比如缩略图和详情图指向同一个文件。如果不去重你会下载一堆重复文件浪费时间和空间。去重的思路有两种。一种是按 URL 去重用一个 set 记录已经处理过的地址。另一种是按文件内容去重下载后计算哈希值比对。前者简单高效适合大多数场景后者更彻底能识别不同 URL 指向同一张图的情况但要多花一次哈希计算。过滤则是另一回事。页面上除了内容图还有大量图标、logo、按钮背景图。这些通常尺寸很小可以通过文件大小或者 URL 特征过滤掉。比如下载前先发一个 HEAD 请求看 Content-Length小于某个阈值比如 10KB就跳过。seen set() def is_duplicate(url): if url in seen: return True seen.add(url) return False3.5 文件命名与保存路径保存文件时命名是个容易被忽视的细节。直接用 URL 里的文件名可能重复也可能包含非法字符。我一般用序号 原始扩展名的方式或者对 URL 做一次哈希作为文件名。import os import hashlib def build_filename(url, index, save_dir): ext os.path.splitext(url.split(?)[0])[1] or .jpg # 用 URL 哈希保证唯一性 name_hash hashlib.md5(url.encode()).hexdigest()[:10] return os.path.join(save_dir, f{index:04d}_{name_hash}{ext})注意url.split(?)[0]这一步因为很多图片地址带查询参数直接取扩展名会把参数也带进去。另外保存目录要提前用os.makedirs(save_dir, exist_okTrue)创建否则写入会报错。注意扩展名不要盲目相信 URL。有些站点图片地址没有扩展名或者扩展名和实际格式不符。稳妥做法是下载后根据响应头的 Content-Type 判断真实格式或者用图片库读取文件头来确认。4. 完整实操流程与核心环节实现4.1 环境准备与依赖安装先把环境搭起来。Python 3.7 以上都行依赖就两个核心库再加一个可选的图片处理库。pip install requests beautifulsoup4 pip install pillow # 可选用于校验图片完整性requests负责网络请求beautifulsoup4负责解析。pillow不是必须的但如果你想在下载后验证图片是否完整、获取真实尺寸它会很有用。安装完可以跑一句import requests, bs4确认没问题。4.2 第一步请求页面并拿到 HTML先写一个健壮的请求函数把超时、重试、编码都处理好。import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def build_session(): session requests.Session() retry Retry( total3, backoff_factor0.5, status_forcelist[500, 502, 503, 504], ) adapter HTTPAdapter(max_retriesretry) session.mount(http://, adapter) session.mount(https://, adapter) 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, }) return session def fetch_html(session, url, timeout10): try: resp session.get(url, timeouttimeout) resp.raise_for_status() # 让 requests 自动推断编码避免中文乱码 resp.encoding resp.apparent_encoding return resp.text except requests.RequestException as e: print(f[请求失败] {url} - {e}) return None这里用Session而不是直接requests.get好处是连接复用和统一管理请求头。Retry配置让遇到 5xx 错误时自动重试三次backoff_factor让重试间隔递增避免给服务器造成压力。apparent_encoding是 requests 根据内容猜测的编码比默认的resp.encoding更准能有效避免中文页面乱码。4.3 第二步解析 HTML 并提取图片地址拿到 HTML 后交给 BeautifulSoup 解析然后遍历所有 img 标签。from bs4 import BeautifulSoup from urllib.parse import urljoin def parse_images(html, page_url): soup BeautifulSoup(html, html.parser) results [] for img in soup.find_all(img): raw extract_img_url(img) if not raw: continue absolute urljoin(page_url, raw) results.append(absolute) return resultshtml.parser是 Python 内置的解析器不需要额外安装速度也够用。如果追求更快的解析速度可以装lxml然后换成BeautifulSoup(html, lxml)但要注意 lxml 对不规范 HTML 的容错性和 html.parser 略有差异。find_all(img)会返回页面上所有 img 标签。如果只想抓某个区域内的图片可以先定位父容器再查找比如soup.find(div, class_content).find_all(img)这样能过滤掉导航栏、侧边栏的图标。4.4 第三步下载图片并保存下载环节的核心是把二进制内容写进文件同时做好异常处理。import os def download_image(session, url, filepath, refererNone, timeout15): headers {} if referer: headers[Referer] referer try: resp session.get(url, headersheaders, timeouttimeout, streamTrue) resp.raise_for_status() # 校验内容类型避免把 HTML 错误页当成图片存下来 content_type resp.headers.get(Content-Type, ) if image not in content_type and octet-stream not in content_type: print(f[跳过] 非图片内容: {url} - {content_type}) return False with open(filepath, wb) as f: for chunk in resp.iter_content(chunk_size8192): f.write(chunk) # 校验文件大小 if os.path.getsize(filepath) 1024: os.remove(filepath) print(f[跳过] 文件过小: {url}) return False return True except requests.RequestException as e: print(f[下载失败] {url} - {e}) return False这里有几个关键点。streamTrue让响应内容不一次性加载到内存而是分块读取处理大图时更省内存。iter_content(chunk_size8192)按 8KB 一块写入兼顾效率和内存。Content-Type校验能挡住那些返回 HTML 错误页的情况——有些站点图片不存在时返回一个 200 状态码的 HTML 页面不校验的话你会存下一堆 HTML 文件。最后的大小校验能过滤掉占位图和损坏文件。4.5 第四步串起完整流程把上面的函数组装起来形成一个可运行的脚本。def crawl_images(start_url, save_dir, max_pages1): os.makedirs(save_dir, exist_okTrue) session build_session() seen set() index 0 html fetch_html(session, start_url) if not html: print(首页请求失败退出) return image_urls parse_images(html, start_url) print(f共发现 {len(image_urls)} 个图片地址) for url in image_urls: if url in seen: continue seen.add(url) filepath build_filename(url, index, save_dir) if download_image(session, url, filepath, refererstart_url): index 1 print(f[{index}] 已保存 {filepath}) print(f完成共下载 {index} 张图片) if __name__ __main__: crawl_images(https://example.com/gallery, ./downloads)这个脚本已经能处理单页面的图片采集了。如果要翻页可以在外面套一层循环根据下一页链接或者 URL 规律构造后续页面地址。翻页时记得在请求之间加一点延时time.sleep(1)左右既是对服务器的尊重也能降低被限流的概率。4.6 参数选择背后的计算逻辑脚本里有几个参数不是随便定的说一下背后的考量。超时时间 10 到 15 秒。太短容易误判网络稍微抖动就超时太长则卡住整个流程。10 秒对大多数正常站点足够图片下载因为文件大可以放宽到 15 秒。重试次数 3 次。这是经验值。第一次失败可能是偶发网络问题第二次可能还是第三次基本能确认是对方的问题了。再多就是浪费时间。分块大小 8192 字节。这是 8KB是磁盘块大小的常见倍数读写效率比较好。太小会增加系统调用次数太大则失去流式的意义。文件大小阈值 1KB。正常图片很少有小于 1KB 的小于这个值基本可以判定是占位图或者错误响应。这个阈值可以根据实际情况调整比如抓图标时可以调小。5. 常见问题与排查技巧实录5.1 抓到的全是占位图怎么办这是最常见的问题。现象是下载下来的图片都是同一张或者尺寸极小。原因基本可以确定是懒加载——真实地址在>from PIL import Image def verify_image(filepath): try: with Image.open(filepath) as img: img.verify() return True except Exception: return FalsePIL的verify()方法能检测图片是否完整。校验失败的直接删掉重下或者记录下来后续补抓。5.3 请求被拒绝返回 403403 基本就是请求头的问题。按可能性排序User-Agent 缺失或太假、Referer 不对、缺少 Cookie。先确认 User-Agent 是不是完整的浏览器标识别用python-requests/2.x这种默认值。然后检查 Referer请求图片时 Referer 应该是图片所在页面的地址。如果还不行可能需要带上 Cookie——用浏览器登录后复制 Cookie 到请求头里。还有一种情况是请求频率太高被临时封了。这时候降低频率、加延时或者换个时间段再试。5.4 中文文件名乱码如果 URL 里包含中文保存时可能出现乱码或者报错。解决办法是对文件名做 URL 解码或者干脆不用原始文件名改用哈希命名。from urllib.parse import unquote filename unquote(url.split(/)[-1])不过更推荐哈希命名彻底避开编码问题也避免了文件名过长、包含非法字符等一堆麻烦。5.5 常见问题速查表现象可能原因排查方向解决手段全是占位图懒加载查看 img 标签属性优先取>

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑