1. 项目概述:从零开始理解网络爬虫的基石
如果你刚开始接触Python爬虫,或者在网上搜索“requests爬虫”时被一堆零散的代码片段和报错信息搞得晕头转向,那么你来对地方了。今天我们不谈那些复杂的框架和高级技巧,就聚焦在“request基础”这四个字上。这里的“request”通常指的就是Python中那个大名鼎鼎的requests库,它是几乎所有Python爬虫的起点和核心。很多人觉得爬虫就是“请求-获取”这么简单,但真正上手后,却常常卡在“stream disconnected before completion”、“request returned 500”或者“由于触发安全风控策略,该次访问请求被拒绝”这类错误上。这恰恰说明,打好requests的基础,远比盲目复制粘贴代码要重要得多。
简单来说,requests库就是让你用Python模拟浏览器,向网站服务器发送一个“请求”(Request),然后接收服务器返回的“响应”(Response)。这个过程,就是爬虫最本质的动作。无论是抓取知乎的文章、抖音的视频列表,还是采集上市公司的报告数据,底层都是这个逻辑。但为什么你的代码会失败?为什么同样的网址别人能爬到你却被拒绝?核心往往在于你对“请求”这个动作的理解不够深入。本文将带你彻底拆解requests库的基础使用,不止是教你写出一行requests.get(),更要让你明白这行代码背后发生了什么,以及当它出错时,你该如何像侦探一样排查问题。无论你是想写一个简单的公众号文章采集脚本,还是为后续更复杂的“人狗大作战”这类趣味项目获取数据,扎实的requests基础都是你绕不开的第一步。
2. 核心需求解析:我们到底要用requests做什么?
在深入代码之前,我们必须先想清楚目标。使用requests进行爬虫,核心是为了自动化、程序化地获取互联网上的公开数据。这听起来简单,但衍生出几个层次的需求,理解这些需求能帮你更好地使用工具。
2.1 获取数据:最基本的“读”操作
这是最直观的需求。比如,你想获取某个博客首页的HTML内容,或者一个公开API返回的JSON数据(例如天气信息)。对应的就是requests.get()方法。你需要告诉它目标网址(URL),它帮你把内容拿回来。但这里有个关键点:你拿到的是服务器愿意给你的“原始响应”。如果网站需要登录才能看,或者数据是通过JavaScript动态加载的,一个简单的get可能就拿不到你想要的东西。这就引出了下一个需求。
2.2 模拟交互:让服务器“以为”你是真人浏览器
现代网站充满了交互。登录、点击按钮、翻页、搜索,这些操作在浏览器里会附带大量的额外信息给服务器,比如Cookies、特定的请求头(Headers)、表单数据(Form Data)或JSON参数。requests库的强大之处在于它能高度定制化每一个请求。你需要登录后才能爬取QQ空间或小红书?那就先用requests.post()模拟登录,保存服务器返回的Cookies,在后续请求中带上。你发现直接请求被拒绝,提示“安全风控策略”?很可能是因为你的请求头太“假”,缺少了User-Agent(告诉服务器你是什么浏览器)等关键信息。模拟一个真实的浏览器会话,是绕过基础反爬机制的关键。
2.3 处理响应:从一堆字节中提取有效信息
成功拿到响应(Response)对象后,工作才完成一半。响应里包含状态码(200表示成功,404表示找不到,500表示服务器内部错误)、响应头(包含了内容类型、编码等信息)和最重要的响应体(就是你想要的HTML或JSON数据)。你需要正确地解码这些数据(特别是处理中文可能遇到的乱码问题),然后根据内容类型(text/html,application/json)用不同的方式(如response.text获取文本,response.json()解析JSON)来提取信息。很多新手会在这里踩坑,比如对JSON数据用了text,或者没处理编码导致中文乱码。
2.4 应对异常与错误:构建健壮的爬虫
网络世界充满不确定性。连接超时、服务器宕机、IP被暂时封锁、请求频率过快被限制……一个健壮的爬虫必须能妥善处理这些异常。requests库内置了异常处理机制,比如requests.exceptions.Timeout,requests.exceptions.ConnectionError。你需要学会使用try...except来捕获它们,并设计重试、等待、切换代理等策略。看到“error sending request for url”或“connection failed”这类错误时,不至于手足无措。
3. 环境准备与requests库安装
工欲善其事,必先利其器。在开始写爬虫之前,一个干净、独立的Python环境是高效工作和避免依赖冲突的保障。我强烈建议新手从这一步开始,养成良好的习惯。
3.1 Python环境搭建与验证
首先,确保你的系统已经安装了Python。打开终端(Windows上是CMD或PowerShell,macOS/Linux上是Terminal),输入python --version或python3 --version。如果能看到类似“Python 3.8.10”的版本号,说明已安装。我推荐使用Python 3.6及以上版本,requests库对新版本支持更好。
注意:如果你在Windows上同时安装了Python 2和Python 3,命令可能是
py -3来启动Python 3。在命令行里多试试python、python3、py这几个命令,看看哪个能调出Python 3的解释器。
如果未安装,请前往Python官网(python.org)下载安装程序。安装时,务必勾选“Add Python to PATH”这个选项,这能让你在命令行中直接使用python命令,省去后续很多麻烦。
3.2 使用虚拟环境隔离项目
这是很多教程会跳过,但极其重要的一步。虚拟环境相当于为你的爬虫项目建立一个独立的“工作间”,在这个工作间里安装的requests库或其他库,不会影响到系统全局或其他项目。这能完美解决“项目A需要requests 2.25,项目B需要requests 2.28”的版本冲突问题。
创建虚拟环境非常简单。在你的项目文件夹下,打开终端执行:
# 对于Python 3.3+,可以使用内置的venv模块 python -m venv venv这条命令会在当前目录创建一个名为venv的文件夹,里面包含了一个独立的Python环境。
接下来,激活这个环境:
- Windows (CMD/PowerShell):
venv\Scripts\activate - macOS/Linux:
source venv/bin/activate
激活后,你的命令行提示符前面通常会显示(venv),表示你已经进入了虚拟环境。在这个环境下执行的所有pip install操作,都只会影响当前项目。
3.3 安装requests库
在激活的虚拟环境中,安装requests库只需要一行命令:
pip install requestspip是Python的包管理工具。执行后,它会自动从PyPI(Python官方的软件仓库)下载requests库及其依赖(如urllib3,chardet,certifi等)并安装。
为了验证安装是否成功,可以启动Python交互界面测试一下:
python在出现的>>>提示符后输入:
import requests print(requests.__version__)如果没有报错,并且打印出版本号(如2.28.1),那么恭喜你,requests库已经准备就绪。
实操心得:我习惯在项目根目录下创建一个
requirements.txt文件,里面写上requests>=2.25.1。这样,以后在任何新环境(比如部署到服务器)中,只需要运行pip install -r requirements.txt,就能一键安装所有指定版本的依赖,非常方便。这是管理项目依赖的行业通用做法。
4. requests库核心方法与参数全解
安装好环境,我们终于可以进入正题。requests库的API设计非常优雅,最常用的就是get(),post(),put(),delete()这几个方法,对应HTTP协议的几种主要请求方式。其中,get()和post()在爬虫中占据了99%的使用场景。理解它们的每一个关键参数,是你从“能用”到“精通”的必经之路。
4.1 GET请求:获取资源的主力军
requests.get()用于向指定URL请求数据,参数附加在URL之后。它的基本形式是:
import requests response = requests.get('https://www.example.com')但这行简单的代码背后,你可以通过参数进行精细控制。
核心参数详解:
params (字典或字节序列):用于构造查询字符串(URL中
?后面的部分)。这是GET请求传递参数的标准方式。payload = {'key1': 'value1', 'key2': 'value2'} r = requests.get('https://httpbin.org/get', params=payload) print(r.url) # 输出:https://httpbin.org/get?key1=value1&key2=value2为什么用它?它比手动拼接URL更安全、更清晰,
requests会自动处理特殊字符的编码(比如空格转成%20)。headers (字典):定制HTTP请求头。这是模拟浏览器、对抗基础反爬的重中之重。
headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 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,en;q=0.8', } r = requests.get('https://www.zhihu.com', headers=headers)关键点:
User-Agent是很多网站判断请求来源是程序还是浏览器的首要依据。使用一个常见的浏览器UA字符串能大幅降低被直接拒绝的风险。你可以通过浏览器的开发者工具(F12 -> Network -> 点击一个请求 -> Headers)找到真实的请求头信息进行复制。cookies (字典或RequestsCookieJar):发送Cookies。通常用于在已登录的状态下访问需要权限的页面。
cookies = {'session_id': '123456abcde'} r = requests.get('https://www.weibo.com', cookies=cookies)注意:更常见的做法是通过一个
requests.Session()对象来保持会话,它会自动管理Cookies,后面会详细讲。timeout (浮点数或元组):设置请求超时时间。这是必须设置的参数,否则你的程序可能会因为一个无响应的服务器而永远挂起。
# 连接超时和读取超时都设为5秒 r = requests.get('https://www.example.com', timeout=5) # 分别设置连接超时和读取超时 (连接超时3.05秒, 读取超时27秒) r = requests.get('https://www.example.com', timeout=(3.05, 27))经验之谈:对于不稳定的网站或网络环境,设置一个合理的
timeout(如10秒)并配合重试机制,是保证爬虫稳定运行的基础。超时是网络爬虫最常见的异常之一。proxies (字典):设置代理服务器。用于隐藏真实IP,应对IP访问频率限制。
proxies = { 'http': 'http://10.10.1.10:3128', 'https': 'http://10.10.1.10:1080', } r = requests.get('https://www.example.com', proxies=proxies)重要提示:代理服务器的稳定性和匿名性差异很大,需要谨慎选择和使用。公开的免费代理往往速度慢且不可靠。
allow_redirects (布尔值):是否允许重定向,默认为
True。如果设为False,请求在遇到3xx状态码时将不会自动跳转,而是返回这个状态码的响应。verify (布尔值或字符串):是否验证SSL证书,默认为
True。对于使用自签名证书的HTTPS网站,你可能需要将其设为False,但这会带来安全风险。更好的做法是指定一个CA证书包的路径。# 不推荐,除非你明确知道风险且目标网站可信 r = requests.get('https://internal.example.com', verify=False)
4.2 POST请求:提交数据的关键
requests.post()用于向指定URL提交数据,通常用于登录、提交表单、上传文件等操作。它的参数大部分与get()相同,但多了一个核心参数data或json。
核心参数详解:
data (字典、元组列表、字节或文件对象):发送表单数据(application/x-www-form-urlencoded)。
# 模拟登录 login_data = {'username': 'your_name', 'password': 'your_pass'} r = requests.post('https://example.com/login', data=login_data)这是最常见的POST数据格式,对应网页表单的提交。
json (字典):发送JSON格式的数据(application/json)。现代Web API(如很多手机App的后台接口)广泛使用JSON。
api_data = {'query': 'Python', 'page': 1} r = requests.post('https://api.example.com/search', json=api_data)使用
json参数,requests会自动将字典序列化为JSON字符串,并设置请求头Content-Type: application/json。这比手动用json.dumps()处理再放入data中要方便和安全得多。files (字典):上传文件。
files = {'file': open('report.xls', 'rb')} r = requests.post('https://httpbin.org/post', files=files)
GET vs POST 如何选择?简单来说:获取数据用GET,提交/修改数据用POST。GET请求的参数在URL中可见,有长度限制,且可以被浏览器缓存、收藏。POST请求的参数在请求体内,更安全(相对),无长度限制。在爬虫中,你通常通过观察浏览器实际发出的请求来决定用哪个。在开发者工具的Network面板中,查看请求的“Method”字段。
4.3 其他请求方法
requests.put()、requests.delete()、requests.head()、requests.patch()等方法使用频率较低,但原理相通。head()方法只获取响应头,不下载响应体,适合快速检查资源是否存在或是否被修改过。
5. 响应对象解析与数据提取
发送请求后,你会得到一个Response对象。这个对象包含了服务器返回的所有信息。能否正确地从它身上提取出你需要的数据,是爬虫成功的关键。
5.1 响应状态码与请求历史
首先,必须检查请求是否成功。
r = requests.get('https://httpbin.org/status/404') print(r.status_code) # 输出:404 print(r.reason) # 输出:Not Found # 便捷的属性,状态码在200到400之间为True if r.ok: print('请求成功!') else: print(f'请求失败,状态码:{r.status_code}')常见的状态码:
- 200 OK: 请求成功。
- 301/302 Found: 重定向。
requests在allow_redirects=True时会自动跟随。 - 400 Bad Request: 客户端请求有语法错误。
- 401 Unauthorized: 请求未经授权,需要身份验证。
- 403 Forbidden: 服务器理解请求,但拒绝执行。常见于反爬策略生效。
- 404 Not Found: 请求的资源不存在。
- 500 Internal Server Error: 服务器内部错误。看到这个,通常是网站的问题,可以稍后重试。
- 502/503/504: 网关或服务暂时不可用。
如果发生了重定向,可以通过r.history查看重定向链:
r = requests.get('http://github.com', allow_redirects=True) # 会重定向到https print(r.history) # 包含一个Response对象的列表 print(r.url) # 最终URL: https://github.com/5.2 响应头信息
响应头包含了服务器关于响应的元信息,比如内容类型、编码、Cookies等。
print(r.headers) # 返回一个字典-like的对象 print(r.headers['Content-Type']) # 获取特定的头信息,如 'text/html; charset=utf-8' print(r.headers.get('Content-Type')) # 更安全的获取方式,如果不存在返回NoneContent-Type非常重要,它决定了你应该用r.text还是r.json()来解析内容。
5.3 响应体内容提取
这是我们的主要目标。根据Content-Type的不同,提取方式也不同。
文本内容 (HTML, XML, 纯文本等):使用
r.textr = requests.get('https://www.example.com') html_content = r.text print(html_content[:500]) # 打印前500个字符编码问题:
r.text会自动根据响应头中的编码信息(r.encoding)来解码字节流。如果响应头中没有指定,或者指定错误(导致中文乱码),你需要手动指定编码。# 如果出现乱码,可以尝试手动设置编码 r.encoding = 'gbk' # 或者 'utf-8', 'gb2312' 等 print(r.text)一个常见的技巧是使用
chardet库(requests已依赖)自动检测编码:import chardet r = requests.get('some_url') r.encoding = chardet.detect(r.content)['encoding']二进制内容 (图片、视频、文件):使用
r.contentr = requests.get('https://www.example.com/image.jpg') with open('image.jpg', 'wb') as f: # 必须以二进制写入模式打开文件 f.write(r.content)r.content是原始的字节流(bytes),适合保存非文本文件。JSON内容 (API接口返回):使用
r.json()r = requests.get('https://api.github.com/events') json_data = r.json() # 直接解析为Python字典或列表 print(json_data[0]['id']) # 访问数据重要:只有当响应内容确实是合法的JSON时,才能使用
.json(),否则会抛出requests.exceptions.JSONDecodeError异常。稳妥的做法是先判断状态码和内容类型,或者使用try...except。if r.headers.get('Content-Type', '').startswith('application/json'): try: data = r.json() except ValueError: print('响应内容不是有效的JSON')原始响应流:使用
r.raw和iter_content对于大文件(如视频),为了避免一次性加载到内存,可以流式下载。r = requests.get('https://example.com/big_file.zip', stream=True) with open('big_file.zip', 'wb') as f: for chunk in r.iter_content(chunk_size=8192): # 每次迭代返回指定大小的字节块 if chunk: # 过滤掉保持连接的空块 f.write(chunk)设置
stream=True后,requests不会立即下载整个响应体,而是先获取响应头。iter_content方法允许你按块迭代内容,非常适合下载大文件。
5.4 Cookies管理
服务器通过响应头Set-Cookie下发的Cookies,会被自动保存在Response对象中。
r = requests.get('https://www.example.com') print(r.cookies) # 这是一个RequestsCookieJar对象 print(r.cookies.get('session_id')) # 获取特定cookie的值RequestsCookieJar对象用起来和字典很像,但它提供了更完整的Cookie规范支持。你可以将它传递给下一个请求的cookies参数,以维持会话状态。
6. 会话维持与高级技巧:Session对象
如果你需要连续访问同一个网站的多个页面(比如先登录,再访问个人中心),那么使用requests.Session()是比手动传递Cookies更优雅、更强大的方式。
6.1 为什么需要Session?
一个Session对象会跨请求自动保持某些参数和状态,最典型的就是Cookies。它还会复用底层的TCP连接,从而在发起多个请求到同一主机时提升性能。
对比一下:
- 无Session (繁琐且易错):
# 第一次请求,登录 login_resp = requests.post('...', data=login_data) cookies_received = login_resp.cookies # 第二次请求,必须手动带上cookies profile_resp = requests.get('...', cookies=cookies_received) - 使用Session (简洁高效):
session = requests.Session() # 登录,cookies会自动保存在session中 session.post('...', data=login_data) # 后续请求自动使用session中的cookies profile_resp = session.get('...')
6.2 Session的实战应用
模拟一个完整的登录并访问受保护页面的流程:
import requests # 1. 创建会话 s = requests.Session() # 2. 可选:为本次会话的所有请求设置统一的请求头 s.headers.update({ 'User-Agent': 'Mozilla/5.0...', 'Accept-Language': 'zh-CN,zh;q=0.9' }) # 3. 首先,可能访问登录页获取一些初始token或cookie(某些网站需要) # 例如,有些网站登录时需要先获取一个csrf_token login_page = s.get('https://example.com/login') # 这里假设我们需要从登录页HTML中解析出一个csrf_token (实际中可用BeautifulSoup) # csrf_token = extract_csrf_token(login_page.text) # 4. 构造登录数据并提交 login_data = { 'username': 'your_username', 'password': 'your_password', # 'csrf_token': csrf_token, } login_response = s.post('https://example.com/login_action', data=login_data) # 5. 检查登录是否成功(根据实际情况判断,如状态码、响应内容、跳转等) if login_response.status_code == 200 and '登录成功' in login_response.text: print('登录成功!') # 6. 使用同一个session访问需要登录的页面 dashboard = s.get('https://example.com/dashboard') print(dashboard.text[:200]) # 打印部分内容 else: print('登录失败!')6.3 Session级别的配置
你可以在创建Session时或之后,为其设置默认参数,这些参数会应用到该Session发起的所有请求。
s = requests.Session() s.proxies = {'http': 'http://proxy.example.com:8080'} s.verify = False # 谨慎使用!仅为示例,会禁用所有请求的SSL验证 s.timeout = 10 # 为所有请求设置默认超时 # 后续的 s.get() 或 s.post() 都会自动使用这些配置这非常适合需要统一代理、统一超时时间等场景。
注意事项:
Session对象会长期保持连接。如果你的爬虫程序运行时间很长,并且向很多不同的主机发送请求,可能会占用大量系统资源。对于一次性或简单的请求,直接使用requests.get()更轻量。对于复杂的、需要保持状态的连续操作,Session是首选。
7. 异常处理与错误排查实战
网络请求充满了不确定性。一个健壮的爬虫必须能妥善处理各种异常,并从错误信息中快速定位问题。requests库定义了一系列清晰的异常类型。
7.1 常见异常类型及处理
使用try...except块来捕获和处理异常是最佳实践。
import requests from requests.exceptions import Timeout, ConnectionError, HTTPError, RequestException url = 'https://www.example.com' try: response = requests.get(url, timeout=5) # 如果响应状态码不是200,主动抛出HTTPError异常 response.raise_for_status() # 处理响应内容 print(response.text[:100]) except Timeout: print(f"请求 {url} 超时。可能是网络慢或服务器无响应。") # 可以加入重试逻辑 # for i in range(3): # try: ... except Timeout: ... else: break except ConnectionError: print(f"连接 {url} 失败。可能是DNS解析失败、服务器拒绝连接或网络中断。") # 检查网络连接,或更换代理/重试 except HTTPError as e: print(f"HTTP错误发生: {e}。状态码: {response.status_code}") # 处理特定的状态码,如403(禁止访问)可能需要更换User-Agent或IP if response.status_code == 403: print("访问被拒绝,可能触发了反爬机制。") elif response.status_code == 404: print("请求的资源不存在。") elif response.status_code == 500: print("服务器内部错误,可稍后重试。") except RequestException as e: print(f"请求发生未知错误: {e}") # RequestException是所有requests库异常的基类,可以捕获所有相关错误 except Exception as e: print(f"发生了其他非requests异常: {e}")关键点解析:
response.raise_for_status(): 这是一个非常实用的方法。如果响应状态码是4xx(客户端错误)或5xx(服务器错误),它会抛出一个HTTPError异常。这让你能把错误处理统一到异常捕获流程中,代码更清晰。Timeout: 务必为每个请求设置timeout参数,这是防止程序无限期挂起的基本保障。ConnectionError: 涵盖从DNS解析失败到TCP连接被拒等各种底层网络问题。
7.2 错误排查实战指南
当你的爬虫脚本报错时,不要慌张,按照以下步骤进行排查,就像医生问诊一样:
看错误信息(Traceback):Python的错误信息会告诉你异常发生在哪一行,是什么类型的错误。这是第一手资料。
检查URL:是不是写错了?是不是需要
https而你写了http?手动在浏览器中打开这个URL,看看是否正常。检查网络和代理:你的电脑能正常上网吗?如果使用了代理,代理是否还有效?尝试用
curl或wget命令测试一下连通性。打印请求详情:在发送请求前后,打印出关键信息,这是最有效的调试手段。
import requests import json # 更详细的调试:启用requests的日志(可选,信息量很大) # import logging # logging.basicConfig(level=logging.DEBUG) url = 'https://httpbin.org/post' data = {'test': 'data'} headers = {'Custom-Header': 'my-value'} # 发送请求前,打印你准备发送的内容 print(f"[准备请求] URL: {url}") print(f"[准备请求] Data: {data}") print(f"[准备请求] Headers: {headers}") try: r = requests.post(url, json=data, headers=headers, timeout=10) # 收到响应后,打印关键信息 print(f"[响应状态] 状态码: {r.status_code}") print(f"[响应头] Content-Type: {r.headers.get('Content-Type')}") print(f"[响应体] 前500字符: {r.text[:500]}") # httpbin.org会返回我们发送的请求信息,非常利于调试 if 'application/json' in r.headers.get('Content-Type', ''): print(f"[响应JSON] {json.dumps(r.json(), indent=2, ensure_ascii=False)}") except Exception as e: print(f"[请求异常] {type(e).__name__}: {e}")通过对比你发送的和服务器返回的,往往能发现端倪。比如,你发现服务器返回
403,而你的请求头里缺少User-Agent。模拟浏览器行为:用浏览器(如Chrome)的开发者工具(F12 -> Network),正常访问一次目标页面。查看浏览器实际发送的请求:
- Request Method: 是GET还是POST?
- Request Headers: 复制完整的请求头,特别是
User-Agent,Cookie,Referer,Content-Type等。 - Request Payload/Form Data: 如果是POST,查看它提交了什么数据。
- Response: 查看服务器返回的真实数据格式。 然后,尽量让你的
requests代码复现浏览器的这个请求。这是破解很多反爬机制的起点。
处理常见反爬:
- User-Agent检测:添加常见的浏览器UA。
- 频率限制:在请求间加入随机延时(
time.sleep(random.uniform(1, 3))),避免请求过快。 - IP封锁:使用代理IP池轮换。
- 验证码:遇到验证码通常意味着需要更复杂的处理(如打码平台、机器学习识别)或考虑放弃该网站。
- 动态加载(JavaScript渲染):
requests只能获取初始HTML。如果数据是JS加载的,你需要分析其背后的API接口(仍在Network中找XHR/Fetch请求),或者使用Selenium、Playwright等浏览器自动化工具。
实操心得:我习惯为重要的爬虫项目写一个简单的“调试模式”。在脚本开头设置一个
DEBUG = True的变量。当它为True时,会打印出每个请求的URL、状态码和耗时;当它为False时,则安静运行。这能帮助我在开发阶段快速定位问题,上线时又不会产生冗余输出。
8. 综合实战案例:构建一个简单的图片爬虫
理论讲得再多,不如动手写一个。我们设计一个简单的实战案例:爬取一个图片网站(例如,我们以免费的示例图片网站https://picsum.photos为例,它提供随机图片)的图片列表,并将前10张图片下载到本地。这个案例会综合运用我们讲到的GET请求、参数传递、异常处理、文件保存等知识点。
8.1 目标分析与步骤拆解
- 目标:从
https://picsum.photos/v2/list获取图片列表(JSON格式),并下载列表中的前10张图片。 - 分析:该API返回一个JSON数组,每个元素是一个图片对象,包含
id,author,download_url等字段。我们需要解析JSON,提取download_url,然后用requests下载图片二进制内容并保存。 - 步骤: a. 发送GET请求获取图片列表数据。 b. 解析JSON响应,提取前10个图片的下载链接。 c. 遍历这10个链接,发送GET请求下载图片。 d. 将图片的二进制内容(
response.content)保存为本地文件。 e. 加入异常处理和友好提示。
8.2 代码实现与逐行解析
import requests import time import os from requests.exceptions import RequestException def download_images_from_picsum(limit=10, save_dir='./downloaded_images'): """ 从Picsum.photos下载指定数量的图片。 Args: limit (int): 要下载的图片数量,默认10张。 save_dir (str): 图片保存的本地目录。 """ # 0. 创建保存目录(如果不存在) if not os.path.exists(save_dir): os.makedirs(save_dir) print(f"[信息] 创建保存目录: {save_dir}") # 1. 定义图片列表API的URL和参数 list_api_url = "https://picsum.photos/v2/list" params = { 'page': 1, 'limit': limit # 限制返回的数量,正好符合我们的需求 } headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } print(f"[步骤1] 正在请求图片列表...") try: # 2. 发送GET请求获取图片列表 list_response = requests.get(list_api_url, params=params, headers=headers, timeout=15) list_response.raise_for_status() # 如果状态码不是200,抛出HTTPError # 3. 解析JSON数据 image_list = list_response.json() print(f"[信息] 成功获取到 {len(image_list)} 张图片的信息。") # 4. 遍历列表,下载每张图片 for idx, image_info in enumerate(image_list, start=1): image_url = image_info.get('download_url') image_id = image_info.get('id', 'unknown') author = image_info.get('author', 'unknown') if not image_url: print(f"[警告] 第{idx}张图片缺少下载链接,跳过。") continue print(f"[步骤2] 正在下载第 {idx}/{len(image_list)} 张: ID={image_id}, Author={author}") try: # 5. 发送请求下载图片(流式传输,适合可能的大文件) # 注意:这里我们直接使用原图URL,有些API可能需要拼接或处理 img_response = requests.get(image_url, stream=True, timeout=30) img_response.raise_for_status() # 6. 从URL或响应头中提取文件名 # 简单处理:使用图片ID作为文件名,并假设是jpg格式(根据实际情况调整) # 更健壮的做法是从响应头`Content-Disposition`或URL后缀获取 file_extension = '.jpg' # 假设格式 filename = f"{image_id}_{author.replace(' ', '_')}{file_extension}" filepath = os.path.join(save_dir, filename) # 7. 以二进制写入模式保存图片 with open(filepath, 'wb') as f: # 使用iter_content分块写入,避免大文件占用过多内存 for chunk in img_response.iter_content(chunk_size=8192): if chunk: f.write(chunk) print(f" -> 已保存至: {filepath}") except RequestException as e: print(f"[错误] 下载图片 {image_url} 失败: {e}") continue # 跳过这张,继续下一张 except IOError as e: print(f"[错误] 保存文件 {filename} 失败: {e}") continue # 8. 礼貌性延时,避免对服务器造成过大压力(即使对示例网站也应保持良好习惯) time.sleep(0.5) # 暂停0.5秒 print(f"[完成] 所有图片下载任务结束。文件保存在 '{save_dir}' 目录。") except RequestException as e: print(f"[严重错误] 获取图片列表失败,请检查网络或API地址: {e}") except ValueError as e: print(f"[严重错误] 解析JSON响应失败,API可能返回了错误格式: {e}") # 运行函数 if __name__ == '__main__': download_images_from_picsum(limit=10, save_dir='./picsum_images')8.3 代码要点与避坑指南
- 参数化与函数化:我们将功能封装成函数,并接受
limit和save_dir参数。这使得代码更灵活、可复用。在实际项目中,这应该是基本操作。 - 目录创建:使用
os.makedirs(save_dir, exist_ok=True)可以安全地创建目录,如果目录已存在也不会报错。exist_ok=True参数在Python 3.2+中可用,更简洁。 - 使用params传递参数:构造API查询参数时,使用
params字典让requests自动编码,比手动拼接URL更规范。 - 流式下载大文件:在下载图片的请求中,我们设置了
stream=True,并使用iter_content分块写入文件。这对于下载大图或文件至关重要,可以防止一次性将整个文件加载到内存中导致内存溢出。 - 异常处理的粒度:我们进行了两层异常处理。外层
try...except捕获获取列表时的致命错误(如网络不通、API失效)。内层try...except包裹每张图片的下载过程,这样即使某一张图片下载失败,也不会影响其他图片的下载,提高了程序的健壮性。 - 设置延时:
time.sleep(0.5)是一个简单的礼貌性延时。在爬取任何网站时,在请求间加入随机延时是一个好习惯,可以显著降低被服务器识别为恶意爬虫的风险。对于真实网站,延时应该更长且更随机(例如time.sleep(random.uniform(1, 3)))。 - 文件名处理:本例简单使用了
id和author组合成文件名。在实际爬取中,文件名可能包含非法字符(如/,:,?),需要清洗。最好从URL或响应头Content-Disposition中提取原始文件名,或者使用hashlib生成唯一文件名。
8.4 案例扩展思考
这个案例虽然简单,但涵盖了爬虫的核心流程。你可以基于此进行扩展:
- 爬取其他网站:将
list_api_url和解析逻辑换成其他图片网站或API。 - 增加代理支持:在
requests.get()中添加proxies参数。 - 增加重试机制:使用
tenacity库或自己写循环,在请求失败时重试几次。 - 异步加速:对于成百上千张图片,使用
aiohttp库进行异步请求可以极大提升下载速度。 - 图形界面:使用
tkinter或PyQt为这个小工具做一个简单的界面。
通过这个从分析到实现的完整过程,你应该对如何使用requests库构建一个实用的爬虫脚本有了更直观的认识。记住,爬虫的核心逻辑万变不离其宗:构造请求 -> 发送请求 -> 处理响应 -> 提取数据 -> 保存数据。requests库完美地解决了前三个步骤,为你打下了最坚实的基础。